diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000000000..6337e60e0c7ff --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,260 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + + - name: Setup system services + run: | + set -e + # Start X server + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ + xvfb \ + libgtk-3-0 \ + libxkbfile-dev \ + libkrb5-dev \ + libgbm1 \ + rpm + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux x64 $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache/restore@v4 + with: + path: .build/node_modules_cache + key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + working-directory: build + run: | + set -e + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: x64 + VSCODE_ARCH: x64 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + - name: Create .build folder + run: mkdir -p .build + + - name: Prepare built-in extensions cache key + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + + - name: Restore built-in extensions cache + id: cache-builtin-extensions + uses: actions/cache/restore@v4 + with: + enableCrossOsArchive: true + path: .build/builtInExtensions + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" + + - name: Download built-in extensions + if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' + run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Transpile client and extensions + # run: npm run gulp transpile-client-esbuild transpile-extensions + + - name: Download Electron and Playwright + run: | + set -e + + for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) + if npm exec -- npm-run-all -lp "electron x64" "playwright-install"; then + echo "Download successful on attempt $i" + break + fi + + if [ $i -eq 3 ]; then + echo "Download failed after 3 attempts" >&2 + exit 1 + fi + + echo "Download failed on attempt $i, retrying..." + sleep 5 # optional: add a small delay between retries + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: 🧪 Run unit tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 15 + # run: ./scripts/test.sh --tfs "Unit Tests" + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run unit tests (node.js) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 15 + # run: npm run test-node + + # - name: 🧪 Run unit tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 30 + # run: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" + # env: + # DEBUG: "*browser*" + + # - name: Build integration tests + # run: | + # set -e + # npm run gulp \ + # compile-extension:configuration-editing \ + # compile-extension:css-language-features-server \ + # compile-extension:emmet \ + # compile-extension:git \ + # compile-extension:github-authentication \ + # compile-extension:html-language-features-server \ + # compile-extension:ipynb \ + # compile-extension:notebook-renderers \ + # compile-extension:json-language-features-server \ + # compile-extension:markdown-language-features \ + # compile-extension-media \ + # compile-extension:microsoft-authentication \ + # compile-extension:typescript-language-features \ + # compile-extension:vscode-api-tests \ + # compile-extension:vscode-colorize-tests \ + # compile-extension:vscode-colorize-perf-tests \ + # compile-extension:vscode-test-resolver + + # - name: 🧪 Run integration tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-integration.sh --tfs "Integration Tests" + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run integration tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-web-integration.sh --browser chromium + + # - name: 🧪 Run integration tests (Remote) + # if: ${{ inputs.remote_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-remote-integration.sh + # env: + # DISPLAY: ":10" + + # - name: Compile smoke tests + # working-directory: test/smoke + # run: npm run compile + + # - name: Compile extensions for smoke tests + # run: npm run gulp compile-extension-media + + # - name: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + # run: | + # set -e + # ps -ef + # cat /proc/sys/fs/inotify/max_user_watches + # lsof | wc -l + # continue-on-error: true + # if: always() + + # - name: 🧪 Run smoke tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --tracing + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run smoke tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --web --tracing --headless + + # - name: 🧪 Run smoke tests (Remote) + # if: ${{ inputs.remote_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --remote --tracing + # env: + # DISPLAY: ":10" + + # - name: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + # run: | + # set -e + # ps -ef + # cat /proc/sys/fs/inotify/max_user_watches + # lsof | wc -l + # continue-on-error: true + # if: always() diff --git a/build/azure-pipelines/common/publish-artifact.yml b/build/azure-pipelines/common/publish-artifact.yml index b18dc8d4c7f5c..ba4d9f1335509 100644 --- a/build/azure-pipelines/common/publish-artifact.yml +++ b/build/azure-pipelines/common/publish-artifact.yml @@ -18,9 +18,6 @@ parameters: - name: sbomPackageVersion type: string default: "" - - name: isProduction - type: boolean - default: true - name: condition type: string default: succeeded() @@ -80,7 +77,6 @@ steps: targetPath: ${{ parameters.targetPath }} artifactName: $(ARTIFACT_NAME) sbomEnabled: ${{ parameters.sbomEnabled }} - isProduction: ${{ parameters.isProduction }} ${{ if ne(parameters.sbomBuildDropPath, '') }}: sbomBuildDropPath: ${{ parameters.sbomBuildDropPath }} ${{ if ne(parameters.sbomPackageName, '') }}: diff --git a/build/azure-pipelines/darwin/cli-build-darwin.yml b/build/azure-pipelines/darwin/cli-build-darwin.yml index 730918f5da109..fca7fb033410d 100644 --- a/build/azure-pipelines/darwin/cli-build-darwin.yml +++ b/build/azure-pipelines/darwin/cli-build-darwin.yml @@ -71,9 +71,7 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_x64_cli.zip artifactName: unsigned_vscode_cli_darwin_x64_cli displayName: Publish unsigned_vscode_cli_darwin_x64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code macOS x64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false - ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}: - template: ../common/publish-artifact.yml@self @@ -81,6 +79,4 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_arm64_cli.zip artifactName: unsigned_vscode_cli_darwin_arm64_cli displayName: Publish unsigned_vscode_cli_darwin_arm64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code macOS arm64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 3d1dfdf8ea34f..f2b5e697c4d80 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -132,7 +132,6 @@ steps: ${{ else }}: artifactName: crash-dump-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Crash Reports" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -147,7 +146,6 @@ steps: ${{ else }}: artifactName: node-modules-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Node Modules" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -160,7 +158,6 @@ steps: ${{ else }}: artifactName: logs-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Log Files" - isProduction: false sbomEnabled: false continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 4e882b78d2581..e4dbfecd91b24 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -147,7 +147,6 @@ steps: ${{ else }}: artifactName: crash-dump-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Crash Reports" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -162,7 +161,6 @@ steps: ${{ else }}: artifactName: node-modules-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Node Modules" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -175,7 +173,6 @@ steps: ${{ else }}: artifactName: logs-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Log Files" - isProduction: false sbomEnabled: false continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/win32/cli-build-win32.yml b/build/azure-pipelines/win32/cli-build-win32.yml index 1914cb7cf6c49..679a4e05a5eae 100644 --- a/build/azure-pipelines/win32/cli-build-win32.yml +++ b/build/azure-pipelines/win32/cli-build-win32.yml @@ -74,9 +74,7 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_arm64_cli.zip artifactName: unsigned_vscode_cli_win32_arm64_cli displayName: Publish unsigned_vscode_cli_win32_arm64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code Windows arm64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - template: ../common/publish-artifact.yml@self @@ -84,6 +82,4 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_x64_cli.zip artifactName: unsigned_vscode_cli_win32_x64_cli displayName: Publish unsigned_vscode_cli_win32_x64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code Windows x64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 7d5222e347fd8..154ddcf448548 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -149,7 +149,6 @@ steps: artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) ${{ else }}: artifactName: crash-dump-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false sbomEnabled: false displayName: "Publish Crash Reports" continueOnError: true @@ -164,7 +163,6 @@ steps: artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) ${{ else }}: artifactName: node-modules-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false sbomEnabled: false displayName: "Publish Node Modules" continueOnError: true @@ -177,7 +175,6 @@ steps: artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) ${{ else }}: artifactName: logs-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false sbomEnabled: false displayName: "Publish Log Files" continueOnError: true diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index aefdbc56bf6bc..131cbf437320a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -16,25 +16,41 @@ import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; +export interface IChatAttachmentsContentPartOptions { + readonly variables: IChatRequestVariableEntry[]; + readonly contentReferences?: ReadonlyArray; + readonly domNode?: HTMLElement; + readonly limit?: number; +} + export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); private readonly _onDidChangeVisibility = this._register(new Emitter()); private readonly _contextResourceLabels: ResourceLabels; + private _showingAll = false; + + private readonly variables: IChatRequestVariableEntry[]; + private readonly contentReferences: ReadonlyArray; + private readonly limit?: number; + public readonly domNode: HTMLElement | undefined; public contextMenuHandler?: (attachment: IChatRequestVariableEntry, event: MouseEvent) => void; constructor( - private readonly variables: IChatRequestVariableEntry[], - private readonly contentReferences: ReadonlyArray = [], - public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), + options: IChatAttachmentsContentPartOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this.variables = options.variables; + this.contentReferences = options.contentReferences ?? []; + this.limit = options.limit; + this.domNode = options.domNode ?? dom.$('.chat-attached-context'); + this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); - this.initAttachedContext(domNode); - if (!domNode.childElementCount) { + this.initAttachedContext(this.domNode); + if (!this.domNode.childElementCount) { this.domNode = undefined; } } @@ -44,75 +60,130 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.clear(); const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); - for (const attachment of this.variables) { - const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - const correspondingContentReference = this.contentReferences.find((ref) => (typeof ref.reference === 'object' && 'variableName' in ref.reference && ref.reference.variableName === attachment.name) || (URI.isUri(ref.reference) && basename(ref.reference.path) === attachment.name)); - const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; - const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; - - let widget; - if (attachment.kind === 'tool' || attachment.kind === 'toolset') { - widget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isElementVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isImageVariableEntry(attachment)) { - attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; - widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPromptFileVariableEntry(attachment)) { - if (attachment.automaticallyAdded) { - continue; - } - widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPromptTextVariableEntry(attachment)) { - if (attachment.automaticallyAdded) { - continue; - } - widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { - widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPasteVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (resource && isNotebookOutputVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else { - widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + const visibleAttachments = this.getVisibleAttachments(); + const hasMoreAttachments = this.limit && this.variables.length > this.limit && !this._showingAll; + + for (const attachment of visibleAttachments) { + this.renderAttachment(attachment, container, hoverDelegate); + } + + if (hasMoreAttachments) { + this.renderShowMoreButton(container); + } + } + + private getVisibleAttachments(): IChatRequestVariableEntry[] { + if (!this.limit || this._showingAll) { + return this.variables; + } + return this.variables.slice(0, this.limit); + } + + private renderShowMoreButton(container: HTMLElement) { + const remainingCount = this.variables.length - (this.limit ?? 0); + + // Create a button that looks like the attachment pills + const showMoreButton = dom.$('div.chat-attached-context-attachment.chat-attachments-show-more-button'); + showMoreButton.setAttribute('role', 'button'); + showMoreButton.setAttribute('tabindex', '0'); + showMoreButton.style.cursor = 'pointer'; + + // Add pill icon (ellipsis) + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-ellipsis')); + + // Add text label + const textLabel = dom.$('span.chat-attached-context-custom-text'); + textLabel.textContent = `${remainingCount} more`; + + showMoreButton.appendChild(pillIcon); + showMoreButton.appendChild(textLabel); + + // Add click and keyboard event handlers + const clickHandler = () => { + this._showingAll = true; + this.initAttachedContext(container); + }; + + this.attachedContextDisposables.add(dom.addDisposableListener(showMoreButton, 'click', clickHandler)); + this.attachedContextDisposables.add(dom.addDisposableListener(showMoreButton, 'keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + clickHandler(); } + })); + + container.appendChild(showMoreButton); + this.attachedContextDisposables.add({ dispose: () => showMoreButton.remove() }); + } - let ariaLabel: string | null = null; + private renderAttachment(attachment: IChatRequestVariableEntry, container: HTMLElement, hoverDelegate: any) { + const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + const correspondingContentReference = this.contentReferences.find((ref) => (typeof ref.reference === 'object' && 'variableName' in ref.reference && ref.reference.variableName === attachment.name) || (URI.isUri(ref.reference) && basename(ref.reference.path) === attachment.name)); + const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; + const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; - if (isAttachmentPartialOrOmitted) { - widget.element.classList.add('warning'); + let widget; + if (attachment.kind === 'tool' || attachment.kind === 'toolset') { + widget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isElementVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isImageVariableEntry(attachment)) { + attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; + widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPromptFileVariableEntry(attachment)) { + if (attachment.automaticallyAdded) { + return; // Skip automatically added prompt files } - const description = correspondingContentReference?.options?.status?.description; - if (isAttachmentPartialOrOmitted) { - ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; - for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { - const element = widget.label.element.querySelector(selector); - if (element) { - element.classList.add('warning'); - } - } + widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPromptTextVariableEntry(attachment)) { + if (attachment.automaticallyAdded) { + return; // Skip automatically added prompt text } + widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { + widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPasteVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && isNotebookOutputVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else { + widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } - this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e))); + let ariaLabel: string | null = null; - if (this.attachedContextDisposables.isDisposed) { - widget.dispose(); - return; + if (isAttachmentPartialOrOmitted) { + widget.element.classList.add('warning'); + } + const description = correspondingContentReference?.options?.status?.description; + if (isAttachmentPartialOrOmitted) { + ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = widget.label.element.querySelector(selector); + if (element) { + element.classList.add('warning'); + } } + } - if (ariaLabel) { - widget.element.ariaLabel = ariaLabel; - } + this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e))); + + if (this.attachedContextDisposables.isDisposed) { + widget.dispose(); + return; + } - this.attachedContextDisposables.add(widget); + if (ariaLabel) { + widget.element.ariaLabel = ariaLabel; } + + this.attachedContextDisposables.add(widget); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts index 8907667d3dbc2..b52a35e54eefd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts @@ -239,9 +239,12 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { const attachments = this._register(this._instantiationService.createInstance( ChatAttachmentsContentPart, - entries, - undefined, - undefined, + { + variables: entries, + limit: 5, + contentReferences: undefined, + domNode: undefined + } )); attachments.contextMenuHandler = (attachment, event) => { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 99e438866b208..7c9af6dba3c94 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -1451,7 +1451,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, templateData: IChatListItemTemplate) { - return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); + return this.instantiationService.createInstance(ChatAttachmentsContentPart, { + variables, + contentReferences, + domNode: undefined + }); } private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 50b6757f9651a..9e48e0c80d13b 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -3010,3 +3010,24 @@ have to be updated for changes to the rules above, or to support more deeply nes .editor-instance .chat-todo-list-widget { background-color: var(--vscode-editor-background); } + +/* Show more attachments button styling */ +.chat-attachments-show-more-button { + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.chat-attachments-show-more-button:hover { + opacity: 1; + background-color: var(--vscode-list-hoverBackground) !important; +} + +.chat-attachments-show-more-button:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.chat-attachments-show-more-button .chat-attached-context-custom-text { + font-style: italic; + color: var(--vscode-descriptionForeground); +}