diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index e840bd4ed51611..d801e0ee76abd5 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -92,6 +92,8 @@ Whenever the user flags a wrong pattern, rejects an approach, or gives design/ru - **Name semantic layout operations after the user-facing surface**: a shared operation must use the stable UI concept (`toggleSecondarySideBar()`), not the implementation term (`AuxiliaryBar`) that happens to back it in classic layouts. This keeps single-pane mappings clear and avoids leaking layout internals through the API. +- **Semantic layout commands need matching visibility and focus semantics**: when a shared command maps to a different surface, expose a semantic visibility query for its toggled state and transfer focus before hiding the currently focused mapped surface. Otherwise menu labels lie about the action and keyboard users retain focus in hidden content. + - **Expose a mapped visible surface through a named public operation**: when a shared layout operation maps to a different surface in the single-pane workbench, delegate through a public method named for that surface (for example, `toggleEditorPane()`), rather than embedding its implementation in the unrelated override. Use the surface's established user-facing terminology in accessibility announcements. - **Default/main chat title persistence differs per provider — don't unify them**: For the **agent host**, the default (main) chat title is independent of the session title (`AgentHostSessionAdapter._defaultChatTitleOverride`, persisted on the host as `customChatTitle:`); it must be seeded back on restore via `restoreSession`/`_ensureDefaultChat` (mirroring `_restorePeerChats`), or it reverts to the session title after a process restart / idle eviction. For the **local chat sessions provider** (`localChatSessionsProvider`), the primary chat and the session intentionally **share one `_title` observable**, so renaming the first/main chat updates the session title live — this is by design; do not "fix" it to be independent. Only additional (non-primary) local chats have their own title. diff --git a/build/filters.ts b/build/filters.ts index 9a5766c7e62706..11e2b749fd2374 100644 --- a/build/filters.ts +++ b/build/filters.ts @@ -39,7 +39,7 @@ export const unicodeFilter = Object.freeze([ '!LICENSES.chromium.html', '!**/LICENSE', - '!**/*.{dll,exe,png,bmp,jpg,scpt,cur,ttf,woff,eot,template,ico,icns,opus,wasm}', + '!**/*.{dll,exe,png,bmp,jpg,gif,scpt,cur,ttf,woff,eot,template,ico,icns,opus,wasm}', '!**/test/**', '!**/*.test.ts', '!**/*.{d.ts,json,md}', @@ -144,7 +144,7 @@ export const indentationFilter = Object.freeze([ '!src/vs/*/**/*.d.ts', '!src/typings/**/*.d.ts', '!extensions/**/*.d.ts', - '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,psm1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', + '!**/*.{svg,exe,png,bmp,jpg,gif,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,psm1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', '!build/{lib,download,linux,darwin}/**/*.js', '!build/**/*.sh', '!build/azure-pipelines/**/*.js', @@ -181,6 +181,7 @@ export const copyrightFilter = Object.freeze([ '!**/*.bat', '!**/*.cmd', '!**/*.ico', + '!**/*.gif', '!**/*.opus', '!**/*.mp3', '!**/*.icns', diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 183ee230bb10ad..7d21b8b8b4bc4b 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1103,6 +1103,10 @@ "--slide-from-y" ], "sizes": [ + "--segmented-icon-toggle-cell-width", + "--segmented-icon-toggle-height", + "--segmented-icon-toggle-single-width", + "--segmented-icon-toggle-width", "--vscode-agents-fontSize-body1", "--vscode-agents-fontSize-body2", "--vscode-agents-fontSize-heading1", diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index f53cead67afd31..70499cbe49afdc 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -13,7 +13,7 @@ "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", - "@github/copilot": "^1.0.72", + "@github/copilot": "^1.0.73", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", @@ -2969,9 +2969,9 @@ "license": "MIT" }, "node_modules/@github/copilot": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.72.tgz", - "integrity": "sha512-muxp9clYtDTGB+KaaL3B5YJM/UqMoCKOU1vFoxWB63zPzeDrl2vlRIYc/pQwCCEVf6Agf9KAe4rvzVoOsRrPeg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.73.tgz", + "integrity": "sha512-8I2Ejg2CX/PQA3c2H8W1zuqhniCeR1q1/bD8CrV53/ZLw8GF7DAV0xQpwa8ELYvFgjXb6AADojafCKwdbVef+A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -2980,20 +2980,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.72", - "@github/copilot-darwin-x64": "1.0.72", - "@github/copilot-linux-arm64": "1.0.72", - "@github/copilot-linux-x64": "1.0.72", - "@github/copilot-linuxmusl-arm64": "1.0.72", - "@github/copilot-linuxmusl-x64": "1.0.72", - "@github/copilot-win32-arm64": "1.0.72", - "@github/copilot-win32-x64": "1.0.72" + "@github/copilot-darwin-arm64": "1.0.73", + "@github/copilot-darwin-x64": "1.0.73", + "@github/copilot-linux-arm64": "1.0.73", + "@github/copilot-linux-x64": "1.0.73", + "@github/copilot-linuxmusl-arm64": "1.0.73", + "@github/copilot-linuxmusl-x64": "1.0.73", + "@github/copilot-win32-arm64": "1.0.73", + "@github/copilot-win32-x64": "1.0.73" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.72.tgz", - "integrity": "sha512-hUYF/MwE9dji6XVmZ9DkZ8emV/n1Y/8zuAPI8Yfa4mo7smHcRF3LIUUZMVOwdhl0IZj7rvFJJpjfGjXtP5VGcg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.73.tgz", + "integrity": "sha512-5jv7t2sw35/zI0cPze38hG6239NT5/q/Emjx6gLibYkolDqMDJjpm17Ps7tc8oafUEOiMQMb+ar7+qi6rSiGJA==", "cpu": [ "arm64" ], @@ -3007,9 +3007,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.72.tgz", - "integrity": "sha512-4M5qlL5Tf+DVXBcMEBW8v6fGCQKl2Dnpcj5qhFQbPdARHCZNtkBxg2lHWmbHeNmlMU85tndy2Kzb+iAIALTUAw==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.73.tgz", + "integrity": "sha512-l794k6Ahb11AG2FQT/P4TEWxWblzM1h8aQQCzG8jBWp8dfwjhyYjJ+d+0CWQzM3Fc1ddNUZRjKXCUsfvFjiZhQ==", "cpu": [ "x64" ], @@ -3023,9 +3023,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.72.tgz", - "integrity": "sha512-+3Xzfm6Rb+g/oKlD1ZhYzZnrlRnaLkpMYN/9PBwIvBzj3t+c5sH3TDnCEA17Y5oSJeWhuXEyGr9YhuAIuZPemw==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.73.tgz", + "integrity": "sha512-Zu0W5nupJjNeem0brqU/pG+VY0IWr6EWr/FsC90g5SEDiaM4VhVNVWcz8t0E3DQCSYetV6IBaNMtjs/3uIIiDQ==", "cpu": [ "arm64" ], @@ -3042,9 +3042,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.72.tgz", - "integrity": "sha512-PjEJXRoR+SXwFo+GUgogC9DKrl/ZhT+/u1Jd3Sa0SdhWGRRqUjPUBzmNeJN9tcT8Q9VeZ1qSk1beD7JszV57ng==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.73.tgz", + "integrity": "sha512-k33XIr6/PVp+K+5F/zv3No4PPaNImvHz73mcbIw63oxh5iiacXjgr0WqbBIS5s/rkhOWjNPIkbof/TTPZ7mQjA==", "cpu": [ "x64" ], @@ -3061,9 +3061,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.72.tgz", - "integrity": "sha512-9gQQkln+qmsmq80eYua9pbaxGOajKVRlYBB+0xYWM+yEUebZB52u/IbUGhWJImvbax8EWqGoTU6ngswrs/nYJA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.73.tgz", + "integrity": "sha512-HJWzhfD3oaiIgfRAHkNWzp17fELtshqM9HVN5n+lFEmSO2EETCEh0P1lhJc4m+FYfXSJnL0raAqVuyaNMuPoPw==", "cpu": [ "arm64" ], @@ -3080,9 +3080,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.72.tgz", - "integrity": "sha512-t0mowX6LJSbILBNyJo3jYkSzRrATFTBzks2UUUDvDw1FR0k2VkLNCIq0V6LtdRYfNL/CJRKxzH1TqvdVCFCWcA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.73.tgz", + "integrity": "sha512-/BpOXSb16wHEu8I1SaKiLszQ4Kvu4+Z4uCn7W0bv4xI4fPZwTEG0u3zgaI2W9Ao3+aBl0XRpPmpWzE9ziYEq+w==", "cpu": [ "x64" ], @@ -3099,9 +3099,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.72.tgz", - "integrity": "sha512-kbaUKFH7/hZd1Y1WhtuXBRX0gBeIVutUvJ6+SRWD/SkOnRW68nS5RShuRogpXTNM5SmopfFaS3BBaMOu2dFLkg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.73.tgz", + "integrity": "sha512-DbPeXiYzQjpOy9oboaBvuCzjRwfcL987c3bG09cK1crdCDrKfkTJ7NXpcp1KWRPIRFO1FQm1qToNE89J+L3uvg==", "cpu": [ "arm64" ], @@ -3115,9 +3115,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.72.tgz", - "integrity": "sha512-5Jhb38Yk3nHnxwxgE/8vXDKg1b34ZmZ36EceWkLAO3ga9XQYi3njpphRI10G8UahyCBCdzLingY18WKQ28XRiA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.73.tgz", + "integrity": "sha512-8D3E1l5i+N5Eq8HIOQpx+Zbcb3MXdFxszksM2gqq175Z1S7Zna67oY4GoR3psxlbIpSyHKiLEBWYiaps6ayHWw==", "cpu": [ "x64" ], diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index fd1edb52c4be2e..8eada300a3cca9 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -7270,7 +7270,7 @@ "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", - "@github/copilot": "^1.0.72", + "@github/copilot": "^1.0.73", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", diff --git a/extensions/copilot/src/extension/prompts/node/agent/defaultAgentInstructions.tsx b/extensions/copilot/src/extension/prompts/node/agent/defaultAgentInstructions.tsx index f6b1794a39dfdf..782f35c152f37d 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/defaultAgentInstructions.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/defaultAgentInstructions.tsx @@ -487,7 +487,7 @@ export class ApplyPatchInstructions extends PromptElementIf you are stuck, you can fall back on the {ToolName.EditFile} tool, but {ToolName.ApplyPatch} is much faster and is the preferred tool.}
{isGpt5 && <>Prefer the smallest set of changes needed to satisfy the task. Avoid reformatting unrelated code; preserve existing style and public APIs unless the task requires changes. When practical, complete all edits for a file within a single message.
} {!useSimpleInstructions && <> - The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following:
+ The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following:

NEVER print this out to the user, instead call the tool and the edits will be applied and shown to the user.
} diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-default/all_tools.spec.snap b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-default/all_tools.spec.snap index a48f31c1b16415..373d4f66a04fb8 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-default/all_tools.spec.snap +++ b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-default/all_tools.spec.snap @@ -43,7 +43,7 @@ Tools can be disabled by the user. You may see tools used previously in the conv To edit files in the workspace, use the apply_patch tool. If you have issues with it, you should first try to fix your patch and continue using apply_patch. If you are stuck, you can fall back on the insert_edit_into_file tool, but apply_patch is much faster and is the preferred tool. -The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following: +The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following: *** Update File: [file_path] [context_before] -> See below for further instructions on context. -[old_code] -> Precede each line in the old code with a minus sign. diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gemini-2.0-flash/all_tools.spec.snap b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gemini-2.0-flash/all_tools.spec.snap index e468cf9bb92205..5898d36baa8d0f 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gemini-2.0-flash/all_tools.spec.snap +++ b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gemini-2.0-flash/all_tools.spec.snap @@ -39,7 +39,7 @@ Tools can be disabled by the user. You may see tools used previously in the conv To edit files in the workspace, use the apply_patch tool. If you have issues with it, you should first try to fix your patch and continue using apply_patch. If you are stuck, you can fall back on the insert_edit_into_file tool, but apply_patch is much faster and is the preferred tool. -The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following: +The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following: *** Update File: [file_path] [context_before] -> See below for further instructions on context. -[old_code] -> Precede each line in the old code with a minus sign. diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-4.1/all_tools.spec.snap b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-4.1/all_tools.spec.snap index 65b9acb15d3b07..3cdc34624dfa87 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-4.1/all_tools.spec.snap +++ b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-4.1/all_tools.spec.snap @@ -42,7 +42,7 @@ Tools can be disabled by the user. You may see tools used previously in the conv To edit files in the workspace, use the apply_patch tool. If you have issues with it, you should first try to fix your patch and continue using apply_patch. If you are stuck, you can fall back on the insert_edit_into_file tool, but apply_patch is much faster and is the preferred tool. -The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following: +The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following: *** Update File: [file_path] [context_before] -> See below for further instructions on context. -[old_code] -> Precede each line in the old code with a minus sign. diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5-mini/all_tools.spec.snap b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5-mini/all_tools.spec.snap index e97ca52114ed7a..a0dc988cc3fd30 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5-mini/all_tools.spec.snap +++ b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5-mini/all_tools.spec.snap @@ -151,7 +151,7 @@ The messages you send before tool calls should describe what is immediately abou To edit files in the workspace, use the apply_patch tool. If you have issues with it, you should first try to fix your patch and continue using apply_patch. If you are stuck, you can fall back on the insert_edit_into_file tool, but apply_patch is much faster and is the preferred tool. Prefer the smallest set of changes needed to satisfy the task. Avoid reformatting unrelated code; preserve existing style and public APIs unless the task requires changes. When practical, complete all edits for a file within a single message. -The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following: +The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following: *** Update File: [file_path] [context_before] -> See below for further instructions on context. -[old_code] -> Precede each line in the old code with a minus sign. diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5.1/all_tools.spec.snap b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5.1/all_tools.spec.snap index ac5afdea246362..c411a8f9e43afc 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5.1/all_tools.spec.snap +++ b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5.1/all_tools.spec.snap @@ -175,7 +175,7 @@ The class `Person` is in `src/models/person.ts`. To edit files in the workspace, use the apply_patch tool. If you have issues with it, you should first try to fix your patch and continue using apply_patch. If you are stuck, you can fall back on the insert_edit_into_file tool, but apply_patch is much faster and is the preferred tool. Prefer the smallest set of changes needed to satisfy the task. Avoid reformatting unrelated code; preserve existing style and public APIs unless the task requires changes. When practical, complete all edits for a file within a single message. -The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following: +The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following: *** Update File: [file_path] [context_before] -> See below for further instructions on context. -[old_code] -> Precede each line in the old code with a minus sign. diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5/all_tools.spec.snap b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5/all_tools.spec.snap index 2170ecb5e3442f..e2b72919f2d68d 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5/all_tools.spec.snap +++ b/extensions/copilot/src/extension/prompts/node/agent/test/__snapshots__/agentPrompts-gpt-5/all_tools.spec.snap @@ -151,7 +151,7 @@ The messages you send before tool calls should describe what is immediately abou To edit files in the workspace, use the apply_patch tool. If you have issues with it, you should first try to fix your patch and continue using apply_patch. If you are stuck, you can fall back on the insert_edit_into_file tool, but apply_patch is much faster and is the preferred tool. Prefer the smallest set of changes needed to satisfy the task. Avoid reformatting unrelated code; preserve existing style and public APIs unless the task requires changes. When practical, complete all edits for a file within a single message. -The input for this tool is a string representing the patch to apply, following a special format. For each snippet of code that needs to be changed, repeat the following: +The tool call requires both `input`, a string representing the patch to apply, and `explanation`, a short description of what the patch aims to achieve. The input follows a special format. For each snippet of code that needs to be changed, repeat the following: *** Update File: [file_path] [context_before] -> See below for further instructions on context. -[old_code] -> Precede each line in the old code with a minus sign. diff --git a/package-lock.json b/package-lock.json index c762bc39432cbb..37aa850108f69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.82.0", - "@github/copilot": "^1.0.72", - "@github/copilot-sdk": "^1.0.8-preview.0", + "@github/copilot": "^1.0.73", + "@github/copilot-sdk": "^1.0.8", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", @@ -1113,9 +1113,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.72.tgz", - "integrity": "sha512-muxp9clYtDTGB+KaaL3B5YJM/UqMoCKOU1vFoxWB63zPzeDrl2vlRIYc/pQwCCEVf6Agf9KAe4rvzVoOsRrPeg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.73.tgz", + "integrity": "sha512-8I2Ejg2CX/PQA3c2H8W1zuqhniCeR1q1/bD8CrV53/ZLw8GF7DAV0xQpwa8ELYvFgjXb6AADojafCKwdbVef+A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -1124,20 +1124,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.72", - "@github/copilot-darwin-x64": "1.0.72", - "@github/copilot-linux-arm64": "1.0.72", - "@github/copilot-linux-x64": "1.0.72", - "@github/copilot-linuxmusl-arm64": "1.0.72", - "@github/copilot-linuxmusl-x64": "1.0.72", - "@github/copilot-win32-arm64": "1.0.72", - "@github/copilot-win32-x64": "1.0.72" + "@github/copilot-darwin-arm64": "1.0.73", + "@github/copilot-darwin-x64": "1.0.73", + "@github/copilot-linux-arm64": "1.0.73", + "@github/copilot-linux-x64": "1.0.73", + "@github/copilot-linuxmusl-arm64": "1.0.73", + "@github/copilot-linuxmusl-x64": "1.0.73", + "@github/copilot-win32-arm64": "1.0.73", + "@github/copilot-win32-x64": "1.0.73" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.72.tgz", - "integrity": "sha512-hUYF/MwE9dji6XVmZ9DkZ8emV/n1Y/8zuAPI8Yfa4mo7smHcRF3LIUUZMVOwdhl0IZj7rvFJJpjfGjXtP5VGcg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.73.tgz", + "integrity": "sha512-5jv7t2sw35/zI0cPze38hG6239NT5/q/Emjx6gLibYkolDqMDJjpm17Ps7tc8oafUEOiMQMb+ar7+qi6rSiGJA==", "cpu": [ "arm64" ], @@ -1151,9 +1151,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.72.tgz", - "integrity": "sha512-4M5qlL5Tf+DVXBcMEBW8v6fGCQKl2Dnpcj5qhFQbPdARHCZNtkBxg2lHWmbHeNmlMU85tndy2Kzb+iAIALTUAw==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.73.tgz", + "integrity": "sha512-l794k6Ahb11AG2FQT/P4TEWxWblzM1h8aQQCzG8jBWp8dfwjhyYjJ+d+0CWQzM3Fc1ddNUZRjKXCUsfvFjiZhQ==", "cpu": [ "x64" ], @@ -1167,9 +1167,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.72.tgz", - "integrity": "sha512-+3Xzfm6Rb+g/oKlD1ZhYzZnrlRnaLkpMYN/9PBwIvBzj3t+c5sH3TDnCEA17Y5oSJeWhuXEyGr9YhuAIuZPemw==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.73.tgz", + "integrity": "sha512-Zu0W5nupJjNeem0brqU/pG+VY0IWr6EWr/FsC90g5SEDiaM4VhVNVWcz8t0E3DQCSYetV6IBaNMtjs/3uIIiDQ==", "cpu": [ "arm64" ], @@ -1186,9 +1186,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.72.tgz", - "integrity": "sha512-PjEJXRoR+SXwFo+GUgogC9DKrl/ZhT+/u1Jd3Sa0SdhWGRRqUjPUBzmNeJN9tcT8Q9VeZ1qSk1beD7JszV57ng==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.73.tgz", + "integrity": "sha512-k33XIr6/PVp+K+5F/zv3No4PPaNImvHz73mcbIw63oxh5iiacXjgr0WqbBIS5s/rkhOWjNPIkbof/TTPZ7mQjA==", "cpu": [ "x64" ], @@ -1205,9 +1205,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.72.tgz", - "integrity": "sha512-9gQQkln+qmsmq80eYua9pbaxGOajKVRlYBB+0xYWM+yEUebZB52u/IbUGhWJImvbax8EWqGoTU6ngswrs/nYJA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.73.tgz", + "integrity": "sha512-HJWzhfD3oaiIgfRAHkNWzp17fELtshqM9HVN5n+lFEmSO2EETCEh0P1lhJc4m+FYfXSJnL0raAqVuyaNMuPoPw==", "cpu": [ "arm64" ], @@ -1224,9 +1224,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.72.tgz", - "integrity": "sha512-t0mowX6LJSbILBNyJo3jYkSzRrATFTBzks2UUUDvDw1FR0k2VkLNCIq0V6LtdRYfNL/CJRKxzH1TqvdVCFCWcA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.73.tgz", + "integrity": "sha512-/BpOXSb16wHEu8I1SaKiLszQ4Kvu4+Z4uCn7W0bv4xI4fPZwTEG0u3zgaI2W9Ao3+aBl0XRpPmpWzE9ziYEq+w==", "cpu": [ "x64" ], @@ -1243,12 +1243,12 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "1.0.8-preview.0", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.8-preview.0.tgz", - "integrity": "sha512-qX/8ibp8VItOVLiBNI8iSMbZQd9m8SxL9+iYjLZb6w7dATNDE6MppiKiKTjepKeLu5GywGOlTCC6f13m4itGRw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.8.tgz", + "integrity": "sha512-dbahVsyt2aX8qqtOOtmYNe40MnvzSvOSHYFFgoFK7gHZSTNz9QgOht8b1sCCJlcXaFAn/w+5qNc7CwWoCjpQ0g==", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.72", + "@github/copilot": "^1.0.73", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -1267,9 +1267,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.72.tgz", - "integrity": "sha512-kbaUKFH7/hZd1Y1WhtuXBRX0gBeIVutUvJ6+SRWD/SkOnRW68nS5RShuRogpXTNM5SmopfFaS3BBaMOu2dFLkg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.73.tgz", + "integrity": "sha512-DbPeXiYzQjpOy9oboaBvuCzjRwfcL987c3bG09cK1crdCDrKfkTJ7NXpcp1KWRPIRFO1FQm1qToNE89J+L3uvg==", "cpu": [ "arm64" ], @@ -1283,9 +1283,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.72.tgz", - "integrity": "sha512-5Jhb38Yk3nHnxwxgE/8vXDKg1b34ZmZ36EceWkLAO3ga9XQYi3njpphRI10G8UahyCBCdzLingY18WKQ28XRiA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.73.tgz", + "integrity": "sha512-8D3E1l5i+N5Eq8HIOQpx+Zbcb3MXdFxszksM2gqq175Z1S7Zna67oY4GoR3psxlbIpSyHKiLEBWYiaps6ayHWw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 1adea686ed3225..1ea5ac2c0832a4 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.82.0", - "@github/copilot": "^1.0.72", - "@github/copilot-sdk": "^1.0.8-preview.0", + "@github/copilot": "^1.0.73", + "@github/copilot-sdk": "^1.0.8", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/dev-tunnels-connections": "^1.3.41", diff --git a/remote/package-lock.json b/remote/package-lock.json index c7a1134c52899b..840bc7a80b296d 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -8,8 +8,8 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { - "@github/copilot": "^1.0.72", - "@github/copilot-sdk": "^1.0.8-preview.0", + "@github/copilot": "^1.0.73", + "@github/copilot-sdk": "^1.0.8", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/mxc-sdk": "0.6.1", @@ -61,9 +61,9 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.72.tgz", - "integrity": "sha512-muxp9clYtDTGB+KaaL3B5YJM/UqMoCKOU1vFoxWB63zPzeDrl2vlRIYc/pQwCCEVf6Agf9KAe4rvzVoOsRrPeg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.73.tgz", + "integrity": "sha512-8I2Ejg2CX/PQA3c2H8W1zuqhniCeR1q1/bD8CrV53/ZLw8GF7DAV0xQpwa8ELYvFgjXb6AADojafCKwdbVef+A==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" @@ -72,20 +72,20 @@ "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.72", - "@github/copilot-darwin-x64": "1.0.72", - "@github/copilot-linux-arm64": "1.0.72", - "@github/copilot-linux-x64": "1.0.72", - "@github/copilot-linuxmusl-arm64": "1.0.72", - "@github/copilot-linuxmusl-x64": "1.0.72", - "@github/copilot-win32-arm64": "1.0.72", - "@github/copilot-win32-x64": "1.0.72" + "@github/copilot-darwin-arm64": "1.0.73", + "@github/copilot-darwin-x64": "1.0.73", + "@github/copilot-linux-arm64": "1.0.73", + "@github/copilot-linux-x64": "1.0.73", + "@github/copilot-linuxmusl-arm64": "1.0.73", + "@github/copilot-linuxmusl-x64": "1.0.73", + "@github/copilot-win32-arm64": "1.0.73", + "@github/copilot-win32-x64": "1.0.73" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.72.tgz", - "integrity": "sha512-hUYF/MwE9dji6XVmZ9DkZ8emV/n1Y/8zuAPI8Yfa4mo7smHcRF3LIUUZMVOwdhl0IZj7rvFJJpjfGjXtP5VGcg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.73.tgz", + "integrity": "sha512-5jv7t2sw35/zI0cPze38hG6239NT5/q/Emjx6gLibYkolDqMDJjpm17Ps7tc8oafUEOiMQMb+ar7+qi6rSiGJA==", "cpu": [ "arm64" ], @@ -99,9 +99,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.72.tgz", - "integrity": "sha512-4M5qlL5Tf+DVXBcMEBW8v6fGCQKl2Dnpcj5qhFQbPdARHCZNtkBxg2lHWmbHeNmlMU85tndy2Kzb+iAIALTUAw==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.73.tgz", + "integrity": "sha512-l794k6Ahb11AG2FQT/P4TEWxWblzM1h8aQQCzG8jBWp8dfwjhyYjJ+d+0CWQzM3Fc1ddNUZRjKXCUsfvFjiZhQ==", "cpu": [ "x64" ], @@ -115,9 +115,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.72.tgz", - "integrity": "sha512-+3Xzfm6Rb+g/oKlD1ZhYzZnrlRnaLkpMYN/9PBwIvBzj3t+c5sH3TDnCEA17Y5oSJeWhuXEyGr9YhuAIuZPemw==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.73.tgz", + "integrity": "sha512-Zu0W5nupJjNeem0brqU/pG+VY0IWr6EWr/FsC90g5SEDiaM4VhVNVWcz8t0E3DQCSYetV6IBaNMtjs/3uIIiDQ==", "cpu": [ "arm64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.72.tgz", - "integrity": "sha512-PjEJXRoR+SXwFo+GUgogC9DKrl/ZhT+/u1Jd3Sa0SdhWGRRqUjPUBzmNeJN9tcT8Q9VeZ1qSk1beD7JszV57ng==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.73.tgz", + "integrity": "sha512-k33XIr6/PVp+K+5F/zv3No4PPaNImvHz73mcbIw63oxh5iiacXjgr0WqbBIS5s/rkhOWjNPIkbof/TTPZ7mQjA==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.72.tgz", - "integrity": "sha512-9gQQkln+qmsmq80eYua9pbaxGOajKVRlYBB+0xYWM+yEUebZB52u/IbUGhWJImvbax8EWqGoTU6ngswrs/nYJA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.73.tgz", + "integrity": "sha512-HJWzhfD3oaiIgfRAHkNWzp17fELtshqM9HVN5n+lFEmSO2EETCEh0P1lhJc4m+FYfXSJnL0raAqVuyaNMuPoPw==", "cpu": [ "arm64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.72.tgz", - "integrity": "sha512-t0mowX6LJSbILBNyJo3jYkSzRrATFTBzks2UUUDvDw1FR0k2VkLNCIq0V6LtdRYfNL/CJRKxzH1TqvdVCFCWcA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.73.tgz", + "integrity": "sha512-/BpOXSb16wHEu8I1SaKiLszQ4Kvu4+Z4uCn7W0bv4xI4fPZwTEG0u3zgaI2W9Ao3+aBl0XRpPmpWzE9ziYEq+w==", "cpu": [ "x64" ], @@ -191,12 +191,12 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "1.0.8-preview.0", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.8-preview.0.tgz", - "integrity": "sha512-qX/8ibp8VItOVLiBNI8iSMbZQd9m8SxL9+iYjLZb6w7dATNDE6MppiKiKTjepKeLu5GywGOlTCC6f13m4itGRw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.8.tgz", + "integrity": "sha512-dbahVsyt2aX8qqtOOtmYNe40MnvzSvOSHYFFgoFK7gHZSTNz9QgOht8b1sCCJlcXaFAn/w+5qNc7CwWoCjpQ0g==", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.72", + "@github/copilot": "^1.0.73", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -215,9 +215,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.72.tgz", - "integrity": "sha512-kbaUKFH7/hZd1Y1WhtuXBRX0gBeIVutUvJ6+SRWD/SkOnRW68nS5RShuRogpXTNM5SmopfFaS3BBaMOu2dFLkg==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.73.tgz", + "integrity": "sha512-DbPeXiYzQjpOy9oboaBvuCzjRwfcL987c3bG09cK1crdCDrKfkTJ7NXpcp1KWRPIRFO1FQm1qToNE89J+L3uvg==", "cpu": [ "arm64" ], @@ -231,9 +231,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.72", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.72.tgz", - "integrity": "sha512-5Jhb38Yk3nHnxwxgE/8vXDKg1b34ZmZ36EceWkLAO3ga9XQYi3njpphRI10G8UahyCBCdzLingY18WKQ28XRiA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.73.tgz", + "integrity": "sha512-8D3E1l5i+N5Eq8HIOQpx+Zbcb3MXdFxszksM2gqq175Z1S7Zna67oY4GoR3psxlbIpSyHKiLEBWYiaps6ayHWw==", "cpu": [ "x64" ], diff --git a/remote/package.json b/remote/package.json index a99ba78c139992..04951a553f757c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "private": true, "dependencies": { - "@github/copilot": "^1.0.72", - "@github/copilot-sdk": "^1.0.8-preview.0", + "@github/copilot": "^1.0.73", + "@github/copilot-sdk": "^1.0.8", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@microsoft/mxc-sdk": "0.6.1", diff --git a/src/vs/base/browser/ui/segmentedIconToggle/segmentedIconToggle.css b/src/vs/base/browser/ui/segmentedIconToggle/segmentedIconToggle.css new file mode 100644 index 00000000000000..1240ad846a22e7 --- /dev/null +++ b/src/vs/base/browser/ui/segmentedIconToggle/segmentedIconToggle.css @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Generic "segmented icon toggle" chrome: a constant-size, rounded pill housing that holds a + * horizontal reel of equal-width icon cells. Cells that aren't present collapse their width to + * 0 and the housing centers whatever remains, so icons slide smoothly into place. Each cell + * clips its own content (no clipping on the housing) so hover backgrounds never get cut into a + * "half pill" by the rounded edge. + * + * This is toolbar-agnostic: apply these classes to build any small segmented icon control, then + * layer feature-specific classes (colors, glyph animations, etc.) on top. Tunable metrics are + * exposed as custom properties so a host toolbar can size the pill without overriding rules. + */ + +.monaco-segmented-icon-toggle-container { + display: flex; + align-items: center; +} + +.monaco-segmented-icon-toggle { + display: inline-flex; + justify-content: center; + align-items: stretch; + box-sizing: border-box; + width: var(--segmented-icon-toggle-width, 56px); + height: var(--segmented-icon-toggle-height, 24px); + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 8px; +} + +.monaco-segmented-icon-toggle-reel { + display: flex; + align-items: stretch; +} + +.monaco-segmented-icon-toggle-cell { + position: relative; + flex: 0 0 auto; + box-sizing: border-box; + width: var(--segmented-icon-toggle-cell-width, 27px); + height: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + background: transparent; + color: var(--vscode-icon-foreground); + cursor: pointer; + overflow: hidden; + transition: width 0.3s cubic-bezier(0.2, 0.9, 0.2, 1), opacity 0.22s ease, color 0.2s ease; +} + +/* Icon content sits above the hover background. */ +.monaco-segmented-icon-toggle-cell > * { + position: relative; + z-index: 1; +} + +/* Hover background — a rounded-rectangle (squircle) filling the whole cell, edge-to-edge and full height, so it fully covers the housing interior with no surrounding gap. */ +.monaco-segmented-icon-toggle-cell::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 7px; + background: transparent; + transition: background 0.15s ease; + z-index: 0; +} + +.monaco-segmented-icon-toggle-cell:hover::before { + background: var(--vscode-toolbar-hoverBackground, transparent); +} + +/* Single-icon view: the lone button fills the whole pill, so its click target and hover background take up the entire pill. */ +.monaco-segmented-icon-toggle-container.single .monaco-segmented-icon-toggle-cell:not(.collapsed) { + width: var(--segmented-icon-toggle-single-width, 54px); +} + +/* Absent cells collapse to zero width (icons slide into place as the housing recenters). + * `visibility: hidden` (delayed until the width/opacity transition finishes) also removes + * the collapsed native button from the Tab order and accessibility tree, so it can't be + * focused or keyboard-activated while invisible. */ +.monaco-segmented-icon-toggle-cell.collapsed { + width: 0; + opacity: 0; + pointer-events: none; + visibility: hidden; + transition: width 0.3s cubic-bezier(0.2, 0.9, 0.2, 1), opacity 0.22s ease, color 0.2s ease, visibility 0s linear 0.3s; +} + +/* Respect reduced-motion preferences: no sliding/fading of cells. Hosts that gate motion + * with their own class (e.g. VS Code's `.monaco-enable-motion`) can add an equivalent rule. */ +@media (prefers-reduced-motion: reduce) { + .monaco-segmented-icon-toggle-cell { + transition: none; + } +} + +.monaco-segmented-icon-toggle-cell:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} diff --git a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts index 6a1496eda96b8f..4a709da83f79b5 100644 --- a/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts +++ b/src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts @@ -25,7 +25,7 @@ import { agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../common/ import { AgentHostResourcePermissionError, IAgentHostResourceService } from '../common/agentHostResourceService.js'; import type { ClientNotificationMap, CommandMap, JsonRpcErrorResponse, JsonRpcRequest } from '../common/state/protocol/messages.js'; import { ActionType, type ActionEnvelope, type INotification, type IRootConfigChangedAction, type SessionAction, type ChatAction, type TerminalAction, type ClientAnnotationsAction } from '../common/state/sessionActions.js'; -import { SessionSummary, SessionStatus, ROOT_STATE_URI, StateComponents, isAhpRootChannel, type ClientPluginCustomization, type RootState } from '../common/state/sessionState.js'; +import { MessageAttachmentKind, SessionSummary, SessionStatus, ROOT_STATE_URI, StateComponents, isAhpRootChannel, type ClientPluginCustomization, type Message, type RootState } from '../common/state/sessionState.js'; import { PROTOCOL_VERSION } from '../common/state/protocol/version/registry.js'; import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, ProtocolError, ReconnectResultType, type ProtocolMessage, type IStateSnapshot } from '../common/state/sessionProtocol.js'; import { type IVscodeUpgradeResult } from '../common/state/protocolUpgrade.js'; @@ -44,6 +44,7 @@ import type { InitializeResult } from '../common/state/protocol/common/commands. import { dirname } from '../../../base/common/resources.js'; import { observableValue, type IObservable } from '../../../base/common/observable.js'; import { isFileResourceRead } from '../common/resourceReadLogging.js'; +import { ResourceSet } from '../../../base/common/map.js'; const AHP_CLIENT_CONNECTION_CLOSED = -32000; @@ -261,11 +262,10 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC private readonly _loadEstimator: ILoadEstimator; /** - * Comparison keys of customization URIs we have already granted implicit - * read access for on this connection. Dedupes repeat sends so we don't - * pile up grants per dispatch. Cleared with the connection. + * URIs we have already granted implicit read access for on this connection. + * Uses URI-aware comparison to dedupe repeat sends and is cleared with the connection. */ - private readonly _grantedCustomizationUris = new Set(); + private readonly _grantedImplicitReadUris = new ResourceSet(); get clientId(): string { return this._clientId; @@ -751,7 +751,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC return this._subscriptionManager.getActiveSubscriptions(); } - dispatch(channel: string, action: SessionAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction): void { + dispatch(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction): void { const seq = this._subscriptionManager.dispatchOptimistic(channel, action); this.dispatchAction(channel, action, this._clientId, seq); } @@ -796,7 +796,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC /** * Dispatch a client action to the server. Returns the clientSeq used. */ - private dispatchAction(channel: string, action: SessionAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction, _clientId: string, clientSeq: number): void { + private dispatchAction(channel: string, action: SessionAction | ChatAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction, _clientId: string, clientSeq: number): void { this._grantImplicitReadsForOutgoingAction(action); this._sendNotification('dispatchAction', { channel, clientSeq, action }); } @@ -974,14 +974,33 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } /** - * Inspect an outgoing client-dispatched action and grant implicit reads - * for any customization URIs it carries. Today this covers - * `SessionActiveClientSet`, which is the only client-dispatched - * action that ships customization URIs to the host. + * Inspect an outgoing client-dispatched action and grant implicit reads for + * resources that the host will need to read after receiving the action. */ private _grantImplicitReadsForOutgoingAction(action: SessionAction | ChatAction | TerminalAction | ClientAnnotationsAction | IRootConfigChangedAction): void { - if (action.type === ActionType.SessionActiveClientSet && action.activeClient.customizations) { - this._grantImplicitReadsForCustomizations(action.activeClient.customizations); + switch (action.type) { + case ActionType.SessionActiveClientSet: + if (action.activeClient.customizations) { + this._grantImplicitReadsForCustomizations(action.activeClient.customizations); + } + break; + case ActionType.ChatTurnStarted: + case ActionType.ChatPendingMessageSet: + this._grantImplicitReadsForMessage(action.message); + break; + } + } + + private _grantImplicitReadsForMessage(message: Message): void { + for (const attachment of message.attachments ?? []) { + if (attachment.type !== MessageAttachmentKind.Resource) { + continue; + } + try { + this._grantImplicitRead(URI.parse(attachment.uri)); + } catch { + continue; + } } } @@ -999,16 +1018,17 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } catch { continue; } - const grantUri = dirname(uri); - const key = grantUri.toString(); - if (this._grantedCustomizationUris.has(key)) { - continue; - } - this._grantedCustomizationUris.add(key); - // Disposable is owned by the resource service; cleared on - // connectionClosed. - this._resourceService.grantImplicitRead(this._address, grantUri); + this._grantImplicitRead(dirname(uri)); + } + } + + private _grantImplicitRead(uri: URI): void { + if (this._grantedImplicitReadUris.has(uri)) { + return; } + this._grantedImplicitReadUris.add(uri); + // The resource service also revokes these grants in connectionClosed. + this._resourceService.grantImplicitRead(this._address, uri); } /** @@ -1185,7 +1205,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC } this._rejectPendingRequests(error); this._resourceService.connectionClosed(this._address); - this._grantedCustomizationUris.clear(); + this._grantedImplicitReadUris.clear(); this._transitionTo({ kind: AgentHostClientState.Closed, error }); this._onDidClose.fire(); } diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 9cfca4c74d7c94..e29d761390efeb 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -682,7 +682,6 @@ export interface IAgentSessionMetadata { /** Human-readable description of what the session is currently doing. */ readonly activity?: string; readonly workingDirectory?: URI; - readonly customizationDirectory?: URI; readonly isRead?: boolean; readonly isArchived?: boolean; /** diff --git a/src/vs/platform/agentHost/node/claude/claudeAgent.ts b/src/vs/platform/agentHost/node/claude/claudeAgent.ts index 148ecc4447944a..ea9cb972d60d55 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgent.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgent.ts @@ -1654,17 +1654,11 @@ export class ClaudeAgent extends Disposable implements IAgent { } async listSessions(): Promise { - // Plan section 3.3.2: SDK is the source of truth; the per-session DB - // is a pure overlay/cache for Claude-namespaced fields like - // `customizationDirectory`. We deliberately do NOT filter - // entries that lack a DB — external Claude Code CLI sessions - // have no DB and must still surface (Phase-5 exit criterion). - // - // Each per-session overlay read is independently try/caught so a - // single corrupt DB cannot poison the wider listing. CopilotAgent's - // `Promise.all`-with-throwing-mapper pattern at copilotAgent.ts:519 - // has a latent bug; we follow AgentService.listSessions's resilient - // pattern (`agentService.ts:188-204`) instead. + // Plan section 3.3.2: SDK is the source of truth; we deliberately do + // NOT filter entries that lack a per-session DB — external Claude Code + // CLI sessions have no DB and must still surface (Phase-5 exit + // criterion). The projected metadata is derived purely from the SDK + // entry, so no per-session overlay read is needed here. // // `AgentService.listSessions` fans out across all providers via // `Promise.all` (agentService.ts:202-204). If our SDK dynamic @@ -1687,17 +1681,7 @@ export class ClaudeAgent extends Disposable implements IAgent { this._logService.warn('[Claude] SDK listSessions failed; surfacing empty list', err); return []; } - return Promise.all(sdkEntries.map(async entry => { - try { - const sessionUri = AgentSession.uri(this.id, entry.sessionId); - const overlay = await this._metadataStore.read(sessionUri); - return this._metadataStore.project(entry, overlay); - } catch (err) { - this._logService.warn(`[Claude] Overlay read failed for session ${entry.sessionId}`, err); - } - // External session, or DB read failed: surface what the SDK gave us. - return this._metadataStore.project(entry, {}); - })); + return sdkEntries.map(entry => this._metadataStore.project(entry)); } /** @@ -1706,12 +1690,12 @@ export class ClaudeAgent extends Disposable implements IAgent { * external-CLI case: a session that exists on disk via the raw * Anthropic CLI has no per-session DB, so we MUST NOT gate on the * sidecar (the way Copilot's variant does). The SDK is the source - * of truth for existence; the overlay merely decorates. + * of truth for existence. * - * Failures in the overlay read are swallowed — a corrupt DB on one - * session must not lose the SDK-supplied summary/cwd. Failures in - * the SDK lookup propagate (the caller is doing a single targeted - * fetch and should learn that the SDK module is broken). + * The projected metadata is derived purely from the SDK entry, so no + * per-session overlay read is needed. Failures in the SDK lookup + * propagate (the caller is doing a single targeted fetch and should + * learn that the SDK module is broken). */ async getSessionMetadata(session: URI): Promise { // Don't trigger a cold SDK download just to hydrate session metadata @@ -1729,13 +1713,7 @@ export class ClaudeAgent extends Disposable implements IAgent { if (!sdkInfo) { return undefined; } - let overlay: IClaudeSessionOverlay = {}; - try { - overlay = await this._metadataStore.read(session); - } catch (err) { - this._logService.warn(`[Claude] Overlay read failed for session ${sessionId}`, err); - } - return this._metadataStore.project(sdkInfo, overlay); + return this._metadataStore.project(sdkInfo); } resolveSessionConfig(_params: IAgentResolveSessionConfigParams): Promise { diff --git a/src/vs/platform/agentHost/node/claude/claudeSessionMetadataStore.ts b/src/vs/platform/agentHost/node/claude/claudeSessionMetadataStore.ts index 7cb4093b2bbeb5..27a501e83cefeb 100644 --- a/src/vs/platform/agentHost/node/claude/claudeSessionMetadataStore.ts +++ b/src/vs/platform/agentHost/node/claude/claudeSessionMetadataStore.ts @@ -141,18 +141,19 @@ export class ClaudeSessionMetadataStore { } /** - * Combine an SDK-supplied {@link SDKSessionInfo} with this store's - * read overlay into the platform's {@link IAgentSessionMetadata} shape. - * Pure projection — does not touch the DB. + * Project an SDK-supplied {@link SDKSessionInfo} onto the platform's + * {@link IAgentSessionMetadata} shape. Pure projection — does not touch + * the DB. The per-session overlay no longer contributes any projected + * field, so it is not read here; the store is still consulted on the + * harness's internal restoration paths (see {@link read}). */ - project(entry: SDKSessionInfo, overlay: IClaudeSessionOverlay): IAgentSessionMetadata { + project(entry: SDKSessionInfo): IAgentSessionMetadata { return { session: AgentSession.uri(this._provider, entry.sessionId), startTime: entry.createdAt ?? entry.lastModified, modifiedTime: entry.lastModified, summary: entry.customTitle ?? entry.summary, workingDirectory: entry.cwd ? URI.file(entry.cwd) : undefined, - customizationDirectory: overlay.customizationDirectory, }; } } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 45ccaaa3caf73c..6ae330b004e647 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -1237,7 +1237,6 @@ export class CopilotAgent extends Disposable implements IAgent { project, summary: s.summary, workingDirectory, - customizationDirectory: metadata.customizationDirectory, }; return result; })); @@ -1274,7 +1273,6 @@ export class CopilotAgent extends Disposable implements IAgent { project, summary: sessionMetadata?.summary, workingDirectory, - customizationDirectory: storedMetadata?.customizationDirectory, }; } diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index a635ea94e56d4b..5dd3914cc7a0d4 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -3398,6 +3398,7 @@ export class CopilotAgentSession extends Disposable { // the terminal block (skip if any terminal block was already added // while the tool was running). const ptyTerminalUri = isShellTool(tracked.toolName) ? this._shellManager?.getTerminalUriForToolCall(e.data.toolCallId) : undefined; + let retireNonPtyShellTracking = !!ptyTerminalUri; if (ptyTerminalUri && !content.some(c => c.type === ToolResultContentType.Terminal)) { content.push({ type: ToolResultContentType.Terminal, @@ -3410,6 +3411,7 @@ export class CopilotAgentSession extends Disposable { if (isShellTool(tracked.toolName) && !ptyTerminalUri) { const completion = this._nonPtyShellTerminals.completeToolCall(e.data.toolCallId, toolOutput, shellExit); if (completion) { + retireNonPtyShellTracking = completion.shouldRetire; const terminalIndex = content.findIndex(c => c.type === ToolResultContentType.Terminal); if (terminalIndex === -1) { content.push({ @@ -3451,6 +3453,11 @@ export class CopilotAgentSession extends Disposable { }, _meta: tracked.meta ? toToolCallMeta(tracked.meta) : undefined, }, parentToolCallId); + if (retireNonPtyShellTracking) { + // Preserve the terminal result in chat state before removing its + // now-redundant live output resource from the host. + this._nonPtyShellTerminals.retire(e.data.toolCallId); + } })); this._register(wrapper.onIdle(e => { diff --git a/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts b/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts index 9673f5b017acda..76aa44634c70e2 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotNonPtyShellTerminals.ts @@ -47,6 +47,7 @@ function parseCompletedShell(text: string | undefined): TerminalCommandResult | export interface INonPtyShellToolCompletion { readonly uri: string; readonly result?: TerminalCommandResult; + readonly shouldRetire: boolean; } /** @@ -138,7 +139,11 @@ export class NonPtyShellTerminalStreams extends Disposable { const result = shellExit?.result ?? parseCompletedShell(toolOutput); if (!result) { - return stream.created ? { uri: stream.uri } : undefined; + if (!stream.created) { + this._streams.delete(toolCallId); + return undefined; + } + return { uri: stream.uri, shouldRetire: false }; } if (!stream.created) { this._createTerminal(toolCallId, stream); @@ -149,7 +154,26 @@ export class NonPtyShellTerminalStreams extends Disposable { if (result.exitCode !== undefined) { this._finalize(stream, result.exitCode); } - return { uri: stream.uri, result }; + return { + uri: stream.uri, + result, + shouldRetire: stream.finalized && result.preview !== undefined, + }; + } + + /** + * Releases the live output resource after its static completion has been + * published. Repeated calls are safe and do not dispose the resource twice. + */ + retire(toolCallId: string): void { + const stream = this._streams.get(toolCallId); + if (!stream) { + return; + } + this._streams.delete(toolCallId); + if (stream.created) { + this._terminalManager.disposeTerminal(stream.uri); + } } private _finalize(stream: INonPtyShellStream, exitCode: number): void { diff --git a/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts b/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts index 1387385dc24962..de4d653990c462 100644 --- a/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts +++ b/src/vs/platform/agentHost/test/electron-browser/remoteAgentHostProtocolClient.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { DeferredPromise, timeout } from '../../../../base/common/async.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -19,11 +20,11 @@ import { ConfigurationTarget, type IConfigurationValue } from '../../../configur import { ContentEncoding, ReconnectResultType } from '../../common/state/protocol/commands.js'; import { AhpErrorCodes } from '../../common/state/protocol/errors.js'; import { PROTOCOL_VERSION } from '../../common/state/protocol/version/registry.js'; -import { ActionType, type SessionActiveClientSetAction, type SessionActiveClientRemovedAction, type SessionTitleChangedAction } from '../../common/state/sessionActions.js'; +import { ActionType, type ChatTurnStartedAction, type SessionActiveClientSetAction, type SessionActiveClientRemovedAction, type SessionTitleChangedAction } from '../../common/state/sessionActions.js'; import { ProtocolError, type AhpServerNotification, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type ProtocolMessage } from '../../common/state/sessionProtocol.js'; import { hasKey } from '../../../../base/common/types.js'; import { mainWindow } from '../../../../base/browser/window.js'; -import { CustomizationType, ROOT_STATE_URI, StateComponents, customizationId } from '../../common/state/sessionState.js'; +import { CustomizationType, MessageAttachmentKind, MessageKind, PendingMessageKind, ROOT_STATE_URI, StateComponents, customizationId } from '../../common/state/sessionState.js'; import type { IClientTransport, IProtocolTransport } from '../../common/state/sessionTransport.js'; import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { TelemetryLevel } from '../../../telemetry/common/telemetry.js'; @@ -145,6 +146,7 @@ suite('RemoteAgentHostProtocolClient', () => { granted?: (address: string, uri: URI, mode: AgentHostPermissionMode) => boolean; onRequest?: (address: string, params: { uri: string; read?: boolean; write?: boolean }) => Promise; onGrantImplicitRead?: (address: string, uri: URI) => void; + readBytes?: VSBuffer; } /** @@ -169,7 +171,13 @@ suite('RemoteAgentHostProtocolClient', () => { _serviceBrand: undefined, check: async (addr, uri, mode) => grant(addr, uri, mode), async list(addr, uri) { await gateRead(addr, uri); return { entries: [] }; }, - async read(addr, uri) { await gateRead(addr, uri); throw new Error('Not implemented in stub'); }, + async read(addr, uri) { + await gateRead(addr, uri); + if (opts.readBytes) { + return { bytes: opts.readBytes }; + } + throw new Error('Not implemented in stub'); + }, async write(addr, params) { await gateWrite(addr, URI.parse(params.uri)); }, async del(addr, params) { await gateWrite(addr, URI.parse(params.uri)); }, async move(addr, params) { await gateWrite(addr, URI.parse(params.source)); await gateWrite(addr, URI.parse(params.destination)); }, @@ -204,6 +212,13 @@ suite('RemoteAgentHostProtocolClient', () => { await connectPromise; } + async function flushMicrotasks(): Promise { + // `await Promise.resolve()` only advances one microtask; loop to drain chained handlers. + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + } + function fireConfigurationChange(configurationService: TestConfigurationService, settingId: string): void { configurationService.onDidChangeConfigurationEmitter.fire({ source: ConfigurationTarget.USER, @@ -943,7 +958,7 @@ suite('RemoteAgentHostProtocolClient', () => { }); }); - suite('implicit grants for outgoing customization actions', () => { + suite('implicit grants for outgoing actions', () => { function createCapturingPermissionService(): { service: IAgentHostResourceService; calls: { address: string; uri: URI }[] } { const calls: { address: string; uri: URI }[] = []; @@ -979,6 +994,67 @@ suite('RemoteAgentHostProtocolClient', () => { ); }); + test('ChatTurnStarted grants attachment access before reverse resourceRead', async () => { + const granted = new Set(); + const attachmentUri = URI.file('/attachments/example.txt'); + const service = createResourceServiceStub({ + granted: (_address, uri, mode) => mode === AgentHostPermissionMode.Read && granted.has(uri.toString()), + onGrantImplicitRead: (_address, uri) => granted.add(uri.toString()), + readBytes: VSBuffer.fromString('attachment'), + }); + const { client, transport } = createClient(undefined, service); + const action: ChatTurnStartedAction = { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + startedAt: '2026-07-23T00:00:00.000Z', + message: { + text: 'Review this file', + origin: { kind: MessageKind.User }, + attachments: [{ + type: MessageAttachmentKind.Resource, + uri: attachmentUri.toString(), + label: 'example.txt', + }], + }, + }; + + client.dispatch('copilot-chat:/test', action); + transport.fireMessage({ + jsonrpc: '2.0', + id: 42, + method: 'resourceRead', + params: { channel: ROOT_STATE_URI, uri: attachmentUri.toString() }, + }); + await flushMicrotasks(); + + assert.deepStrictEqual(transport.sentMessages.at(-1), { + jsonrpc: '2.0', + id: 42, + result: { data: 'YXR0YWNobWVudA==', encoding: ContentEncoding.Base64 }, + }); + }); + + test('ChatPendingMessageSet grants resource attachments only', () => { + const { service, calls } = createCapturingPermissionService(); + const { client } = createClient(undefined, service); + + client.dispatch('copilot-chat:/test', { + type: ActionType.ChatPendingMessageSet, + kind: PendingMessageKind.Queued, + id: 'queued-1', + message: { + text: 'Review these attachments', + origin: { kind: MessageKind.User }, + attachments: [ + { type: MessageAttachmentKind.Resource, uri: 'file:///attachments/queued.txt', label: 'queued.txt' }, + { type: MessageAttachmentKind.EmbeddedResource, data: '', contentType: 'text/plain', label: 'inline.txt' }, + ], + }, + }); + + assert.deepStrictEqual(calls.map(call => call.uri.toString()), ['file:///attachments/queued.txt']); + }); + test('multiple customizations in the same directory dedupe to one grant', () => { const { service, calls } = createCapturingPermissionService(); const { client } = createClient(undefined, service); @@ -1088,14 +1164,6 @@ suite('RemoteAgentHostProtocolClient', () => { ); } - async function flushMicrotasks(): Promise { - // `await Promise.resolve()` only advances one microtask; loop a few times to - // drain chained .then handlers without resorting to fake timers. - for (let i = 0; i < 10; i++) { - await Promise.resolve(); - } - } - /** Wait until the client transitions into the {@link AgentHostClientState.Reconnecting} state. */ async function waitForReconnecting(client: RemoteAgentHostProtocolClient): Promise { if (client.connectionState === AgentHostClientState.Reconnecting) { @@ -1129,7 +1197,7 @@ suite('RemoteAgentHostProtocolClient', () => { * client plus a `transports` array recording each transport handed * out, so tests can drive handshake/reconnect interactions. */ - function createFactoryClient(): { client: RemoteAgentHostProtocolClient; transports: TestClientProtocolTransport[] } { + function createFactoryClient(permissionService = createPermissionService()): { client: RemoteAgentHostProtocolClient; transports: TestClientProtocolTransport[] } { const transports: TestClientProtocolTransport[] = []; const factory = () => { const t = disposables.add(new TestClientProtocolTransport()); @@ -1137,7 +1205,7 @@ suite('RemoteAgentHostProtocolClient', () => { return t; }; const client = disposables.add(new RemoteAgentHostProtocolClient( - 'test.example:1234', factory, undefined, new NullLogService(), createPermissionService(), new TestConfigurationService(), + 'test.example:1234', factory, undefined, new NullLogService(), permissionService, new TestConfigurationService(), )); return { client, transports }; } @@ -1234,6 +1302,74 @@ suite('RemoteAgentHostProtocolClient', () => { }); }); + test('attachment grant remains available when a pending turn is replayed after reconnect', async function () { + this.timeout(10_000); + return runWithFakedTimers({ useFakeTimers: true, maxTaskCount: 10_000 }, async () => { + const attachmentUri = URI.file('/attachments/replayed.txt'); + const granted = new Set(); + const permissionService = createResourceServiceStub({ + granted: (_address, uri, mode) => mode === AgentHostPermissionMode.Read && granted.has(uri.toString()), + onGrantImplicitRead: (_address, uri) => granted.add(uri.toString()), + readBytes: VSBuffer.fromString('replayed'), + }); + const { client, transports } = createFactoryClient(permissionService); + const connectPromise = client.connect(); + await completeHandshake(transports[0], connectPromise); + + const chatUri = URI.parse('copilot-chat:/test-chat'); + const subRef = client.getSubscription(StateComponents.Chat, chatUri, 'test'); + const subscribeReq = await waitForRequest(transports[0], 'subscribe'); + transports[0].fireMessage({ + jsonrpc: '2.0', id: subscribeReq.id, + result: { snapshot: { resource: chatUri.toString(), state: { turns: [] }, fromSeq: 5 } }, + }); + await Promise.resolve(); + + client.dispatch(chatUri.toString(), { + type: ActionType.ChatTurnStarted, + turnId: 'turn-1', + startedAt: '2026-07-23T00:00:00.000Z', + message: { + text: 'Review this file', + origin: { kind: MessageKind.User }, + attachments: [{ + type: MessageAttachmentKind.Resource, + uri: attachmentUri.toString(), + label: 'replayed.txt', + }], + }, + }); + + transports[0].fireClose(); + await waitForReconnecting(client); + const reconnectTransport = await waitForTransport(transports, 1); + reconnectTransport.connectDeferred.complete(); + const reconnect = await waitForRequest(reconnectTransport, 'reconnect'); + reconnectTransport.fireMessage({ + jsonrpc: '2.0', id: reconnect.id, + result: { type: ReconnectResultType.Replay, actions: [], missing: [] }, + }); + await flushMicrotasks(); + + assert.ok(findDispatchAction(reconnectTransport, ActionType.ChatTurnStarted)); + reconnectTransport.fireMessage({ + jsonrpc: '2.0', + id: 42, + method: 'resourceRead', + params: { channel: ROOT_STATE_URI, uri: attachmentUri.toString() }, + }); + await flushMicrotasks(); + assert.deepStrictEqual(reconnectTransport.sentMessages.at(-1), { + jsonrpc: '2.0', + id: 42, + result: { data: 'cmVwbGF5ZWQ=', encoding: ContentEncoding.Base64 }, + }); + + subRef.dispose(); + client.dispose(); + }); + }); + test('skips replay when server already echoed the action in the replay buffer', async function () { this.timeout(10_000); return runWithFakedTimers({ useFakeTimers: true, maxTaskCount: 10_000 }, async () => { diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index 95607599d9a846..8e44f96869e05a 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -3704,8 +3704,6 @@ suite('ClaudeAgent', () => { summaryB: b?.summary, modifiedA: a?.modifiedTime, modifiedB: b?.modifiedTime, - custDirA: a?.customizationDirectory?.toString(), - custDirB: b?.customizationDirectory, sdkCalls: sdk.listSessionsCallCount, }, { count: 2, @@ -3714,8 +3712,6 @@ suite('ClaudeAgent', () => { summaryB: 'Session B', modifiedA: 1000, modifiedB: 2000, - custDirA: URI.file('/foo').toString(), - custDirB: undefined, sdkCalls: 1, }); }); @@ -3772,17 +3768,11 @@ suite('ClaudeAgent', () => { assert.deepStrictEqual({ count: result.length, ids: result.map(r => AgentSession.id(r.session)).sort(), - okCustDir: find('ok')?.customizationDirectory?.toString(), - corruptCustDir: find('corrupt')?.customizationDirectory, corruptSummary: find('corrupt')?.summary, - externalCustDir: find('external')?.customizationDirectory, }, { count: 3, ids: ['corrupt', 'external', 'ok'], - okCustDir: URI.file('/ok').toString(), - corruptCustDir: undefined, corruptSummary: 'Corrupt', - externalCustDir: undefined, }); }); @@ -3875,7 +3865,6 @@ suite('ClaudeAgent', () => { startTime: sidecar?.startTime, modifiedTime: sidecar?.modifiedTime, workingDirectory: sidecar?.workingDirectory?.toString(), - customizationDirectory: sidecar?.customizationDirectory?.toString(), }, external: { session: external?.session.toString(), @@ -3883,7 +3872,6 @@ suite('ClaudeAgent', () => { startTime: external?.startTime, modifiedTime: external?.modifiedTime, workingDirectory: external?.workingDirectory?.toString(), - customizationDirectory: external?.customizationDirectory, }, unknown, sdkLookups: sdk.getSessionInfoCalls.slice().sort(), @@ -3894,7 +3882,6 @@ suite('ClaudeAgent', () => { startTime: 4900, modifiedTime: 5000, workingDirectory: URI.file('/work').toString(), - customizationDirectory: URI.file('/cust').toString(), }, external: { session: externalUri.toString(), @@ -3902,7 +3889,6 @@ suite('ClaudeAgent', () => { startTime: 5900, modifiedTime: 6000, workingDirectory: URI.file('/raw-cli').toString(), - customizationDirectory: undefined, }, unknown: undefined, sdkLookups: ['external', 'sidecar', 'unknown'], diff --git a/src/vs/platform/agentHost/test/node/claudeSessionMetadataStore.test.ts b/src/vs/platform/agentHost/test/node/claudeSessionMetadataStore.test.ts index aa061764671f95..5243d18c34cdc6 100644 --- a/src/vs/platform/agentHost/test/node/claudeSessionMetadataStore.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeSessionMetadataStore.test.ts @@ -119,13 +119,11 @@ suite('ClaudeSessionMetadataStore', () => { assert.deepStrictEqual(overlay.model, { id: 'm', config: { thinking: 'high' } }); }); - test('project combines SDK info with overlay onto IAgentSessionMetadata', async () => { + test('project maps SDK info onto IAgentSessionMetadata', async () => { const store = createStore(disposables); const sdkInfo = makeSdkInfo({ sessionId: 'abc', summary: 'sdk-summary', customTitle: 'custom', cwd: '/repo' }); - const projected = store.project(sdkInfo, { - customizationDirectory: URI.file('/custom'), - }); + const projected = store.project(sdkInfo); assert.deepStrictEqual({ session: projected.session.toString(), @@ -133,14 +131,12 @@ suite('ClaudeSessionMetadataStore', () => { modifiedTime: projected.modifiedTime, summary: projected.summary, workingDirectory: projected.workingDirectory?.toString(), - customizationDirectory: projected.customizationDirectory?.toString(), }, { session: 'claude:/abc', startTime: 1000, modifiedTime: 2000, summary: 'custom', workingDirectory: URI.file('/repo').toString(), - customizationDirectory: URI.file('/custom').toString(), }); }); @@ -148,16 +144,14 @@ suite('ClaudeSessionMetadataStore', () => { const store = createStore(disposables); const sdkInfo = makeSdkInfo({ summary: 'fallback', customTitle: undefined, cwd: undefined }); - const projected = store.project(sdkInfo, {}); + const projected = store.project(sdkInfo); assert.deepStrictEqual({ summary: projected.summary, workingDirectory: projected.workingDirectory, - customizationDirectory: projected.customizationDirectory, }, { summary: 'fallback', workingDirectory: undefined, - customizationDirectory: undefined, }); }); }); diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 46dad722ba4d1b..464157f4f08dce 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -2260,32 +2260,6 @@ suite('CopilotAgent', () => { } }); - test('getSessionMetadata preserves legacy customizationDirectory without inferring workingDirectory', async () => { - const sessionDataService = disposables.add(new TestSessionDataService()); - const session = AgentSession.uri('copilotcli', 'legacy-customization-directory'); - const db = sessionDataService.openDatabase(session); - await db.object.setMetadata('copilot.customizationDirectory', URI.file('/legacy-workspace').toString()); - db.dispose(); - - const client = new TestCopilotClient([sdkSession('legacy-customization-directory')]); - const agent = createTestAgent(disposables, { sessionDataService, copilotClient: client }); - try { - await agent.authenticate('https://api.github.com', 'token'); - - const metadata = await agent.getSessionMetadata(session); - assert.ok(metadata); - assert.deepStrictEqual(withoutUndefinedProperties(metadata), { - session, - startTime: 1000, - modifiedTime: 2000, - summary: 'SDK legacy-customization-directory', - customizationDirectory: URI.file('/legacy-workspace'), - }); - } finally { - await disposeAgent(agent); - } - }); - test('getSessionMetadata only returns sessions with a database', async () => { const sessionDataService = disposables.add(new TestSessionDataService()); const session = AgentSession.uri('copilotcli', 'external'); @@ -4336,7 +4310,8 @@ suite('CopilotAgent', () => { return { client, getCreateSessionCalls: () => createSessionCalls }; } - test('falls back to createSession after a Start Over truncate leaves the session empty', async () => { + // TODO: re-enable — flaky (2000ms timeout in CI). Tracked in https://github.com/microsoft/vscode/issues/327214 + test.skip('falls back to createSession after a Start Over truncate leaves the session empty', async () => { // Simulates the post-`truncateSession`/"Start Over" case: the on-disk // session has zero events, so the SDK's resumeSession refuses to // resume it. The exact wording varies across SDK versions, so we diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index dcd465ab9d0c84..ca27d36791478a 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -28,7 +28,7 @@ import { AgentFeedbackAttachmentDisplayKind } from '../../common/meta/agentFeedb import { IDiffComputeService } from '../../common/diffComputeService.js'; import { ISessionDataService, type ISessionDatabase } from '../../common/sessionDataService.js'; import { ActionType, type ChatDeltaAction, type ChatErrorAction, type ChatInputRequestedAction, type ChatResponsePartAction, type ChatToolCallCompleteAction, type ChatToolCallReadyAction, type ChatToolCallStartAction, type ChatTurnCompleteAction, type ChatUsageAction, type SessionAction } from '../../common/state/sessionActions.js'; -import { MessageAttachmentKind, MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildChatUri, buildDefaultChatUri, createSessionState, mergeSessionWithDefaultChat, readUsageInfoMeta, SessionStatus, type ToolDefinition, type ToolResultContent, type ToolResultFileEditContent, type UsageInfoMeta } from '../../common/state/sessionState.js'; +import { MessageAttachmentKind, MessageKind, ResponsePartKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ChatInputResponseKind, ToolCallConfirmationReason, ToolCallRiskAssessmentKind, ToolCallRiskAssessmentStatus, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildChatUri, buildDefaultChatUri, createSessionState, mergeSessionWithDefaultChat, readUsageInfoMeta, SessionStatus, type ToolDefinition, type ToolResultContent, type ToolResultFileEditContent, type ToolResultTerminalContent, type UsageInfoMeta } from '../../common/state/sessionState.js'; import { TerminalClaimKind } from '../../common/state/protocol/state.js'; import { CustomizationType, McpAuthRequiredReason, McpServerStatus, type Customization } from '../../common/state/protocol/channels-session/state.js'; import { CopilotAgentSession } from '../../node/copilot/copilotAgentSession.js'; @@ -2975,6 +2975,56 @@ suite('CopilotAgentSession', () => { ]); }); + test('completed non-pty shell calls retire their distinct live output resources', async () => { + const { session, mockSession, signals, terminalManager } = await createAgentSession(disposables); + const terminalUris = ['tc-retire-1', 'tc-retire-2', 'tc-retire-3'] + .map(toolCallId => buildNonPtyShellTerminalUri(session.sessionUri, toolCallId)); + + for (let i = 0; i < terminalUris.length; i++) { + const toolCallId = `tc-retire-${i + 1}`; + const output = `output ${i + 1}\n`; + mockSession.fire('tool.execution_start', { + toolCallId, + toolName: 'bash', + arguments: { command: `command-${i + 1}` }, + } as SessionEventPayload<'tool.execution_start'>['data']); + mockSession.fire('tool.execution_partial_result', { + toolCallId, + partialOutput: output, + } as SessionEventPayload<'tool.execution_partial_result'>['data']); + mockSession.fire('tool.execution_complete', { + toolCallId, + success: true, + result: { + content: output, + contents: [{ type: 'shell_exit', shellId: `${i + 1}`, exitCode: 0, outputPreview: output }], + }, + } as SessionEventPayload<'tool.execution_complete'>['data']); + } + + const completions = getActions(signals) + .filter((action): action is ChatToolCallCompleteAction => action.type === ActionType.ChatToolCallComplete); + assert.deepStrictEqual({ + terminalResults: completions.map(action => { + const terminal = action.result.content?.find(content => content.type === ToolResultContentType.Terminal) as ToolResultTerminalContent | undefined; + return { + resource: terminal?.resource, + preview: terminal?.result?.preview, + }; + }), + disposed: terminalManager.disposedTerminals, + }, { + terminalResults: terminalUris.map((resource, i) => ({ + resource, + preview: `output ${i + 1}\n`, + })), + disposed: terminalUris, + }); + + session.dispose(); + assert.deepStrictEqual(terminalManager.disposedTerminals, terminalUris); + }); + test('tool partial results stream into an output-only terminal channel', async () => { const { session, mockSession, signals, waitForSignal, terminalManager } = await createAgentSession(disposables); session.resetTurnState('turn-stream'); @@ -3028,6 +3078,7 @@ suite('CopilotAgentSession', () => { { uri: terminalUri, data: 'tick 2\n' }, ]); assert.deepStrictEqual(terminalManager.outputTerminalsFinalized, [{ uri: terminalUri, exitCode: 0 }]); + assert.deepStrictEqual(terminalManager.disposedTerminals, [terminalUri]); // shell_exit completion data lands on the streamed terminal block. const completed = getActions(signals).find(action => action.type === ActionType.ChatToolCallComplete) as ChatToolCallCompleteAction; @@ -3096,24 +3147,52 @@ suite('CopilotAgentSession', () => { }, } as SessionEventPayload<'tool.execution_complete'>['data']); - // The channel advertised by the completed tool call exists, carries - // the preview, and is terminated even though no partial result ever - // streamed into it. + // Completion creates, seeds, and finalizes the channel before the + // static result is published and the live resource is retired. assert.deepStrictEqual({ created: terminalManager.outputTerminalsCreated.map(t => t.uri), data: terminalManager.outputTerminalData, finalized: terminalManager.outputTerminalsFinalized, + disposed: terminalManager.disposedTerminals, }, { created: [terminalUri], data: [{ uri: terminalUri, data: 'ok\n' }], finalized: [{ uri: terminalUri, exitCode: 0 }], + disposed: [terminalUri], }); const completed = getActions(signals).find(action => action.type === ActionType.ChatToolCallComplete) as ChatToolCallCompleteAction; assert.ok(completed.result.content?.some(c => c.type === ToolResultContentType.Terminal && c.resource === terminalUri)); }); - test('tool success without shell_exit does not fabricate a process exit', async () => { + test('empty shell preview still retires the completed output channel', async () => { const { mockSession, terminalManager } = await createAgentSession(disposables); + const terminalUri = 'agenthost-terminal://shell/test-session-1/tc-empty-preview'; + + mockSession.fire('tool.execution_start', { + toolCallId: 'tc-empty-preview', + toolName: 'bash', + arguments: { command: 'true' }, + } as SessionEventPayload<'tool.execution_start'>['data']); + mockSession.fire('tool.execution_complete', { + toolCallId: 'tc-empty-preview', + success: true, + result: { + content: '', + contents: [{ type: 'shell_exit', shellId: '0', exitCode: 0, outputPreview: '' }], + }, + } as SessionEventPayload<'tool.execution_complete'>['data']); + + assert.deepStrictEqual({ + finalized: terminalManager.outputTerminalsFinalized, + disposed: terminalManager.disposedTerminals, + }, { + finalized: [{ uri: terminalUri, exitCode: 0 }], + disposed: [terminalUri], + }); + }); + + test('tool success without shell_exit does not fabricate a process exit', async () => { + const { session, mockSession, terminalManager } = await createAgentSession(disposables); mockSession.fire('tool.execution_start', { toolCallId: 'tc-err', @@ -3148,13 +3227,20 @@ suite('CopilotAgentSession', () => { assert.deepStrictEqual({ data: terminalManager.outputTerminalData, finalized: terminalManager.outputTerminalsFinalized, + disposed: terminalManager.disposedTerminals, }, { data: [ { uri: 'agenthost-terminal://shell/test-session-1/tc-err', data: 'boom\n' }, { uri: 'agenthost-terminal://shell/test-session-1/tc-ok', data: 'fine\n' }, ], finalized: [], + disposed: [], }); + session.dispose(); + assert.deepStrictEqual(terminalManager.disposedTerminals, [ + 'agenthost-terminal://shell/test-session-1/tc-err', + 'agenthost-terminal://shell/test-session-1/tc-ok', + ]); }); test('stable shell completion fallback finalizes when the SDK strips shell_exit', async () => { @@ -3179,7 +3265,13 @@ suite('CopilotAgentSession', () => { } as SessionEventPayload<'tool.execution_complete'>['data']); await waitForSignal(signal => isAction(signal, ActionType.ChatToolCallComplete)); - assert.deepStrictEqual(terminalManager.outputTerminalsFinalized, [{ uri: terminalUri, exitCode: 127 }]); + assert.deepStrictEqual({ + finalized: terminalManager.outputTerminalsFinalized, + disposed: terminalManager.disposedTerminals, + }, { + finalized: [{ uri: terminalUri, exitCode: 127 }], + disposed: [terminalUri], + }); const completed = getActions(signals).find(action => action.type === ActionType.ChatToolCallComplete) as ChatToolCallCompleteAction; assert.ok(completed.result.content?.some(content => content.type === ToolResultContentType.Terminal @@ -3224,6 +3316,34 @@ suite('CopilotAgentSession', () => { }, { created: [], data: [], finalized: [] }); }); + test('background shell output remains live until its session is disposed', async () => { + const { session, mockSession, terminalManager } = await createAgentSession(disposables); + const terminalUri = 'agenthost-terminal://shell/test-session-1/tc-background-stream'; + + mockSession.fire('tool.execution_start', { + toolCallId: 'tc-background-stream', + toolName: 'bash', + arguments: { command: 'long-running-command' }, + } as SessionEventPayload<'tool.execution_start'>['data']); + mockSession.fire('tool.execution_partial_result', { + toolCallId: 'tc-background-stream', + partialOutput: 'started\n', + } as SessionEventPayload<'tool.execution_partial_result'>['data']); + mockSession.fire('tool.execution_complete', { + toolCallId: 'tc-background-stream', + success: true, + result: { content: '' }, + } as SessionEventPayload<'tool.execution_complete'>['data']); + mockSession.fire('system.notification', { + content: 'Shell command completed', + kind: { type: 'shell_completed', shellId: 'shell-bg', exitCode: 0, description: 'long-running-command' }, + } as SessionEventPayload<'system.notification'>['data']); + + assert.deepStrictEqual(terminalManager.disposedTerminals, []); + session.dispose(); + assert.deepStrictEqual(terminalManager.disposedTerminals, [terminalUri]); + }); + test('completions without partials or shell_exit never create output channels', async () => { const { mockSession, terminalManager } = await createAgentSession(disposables); @@ -3339,7 +3459,7 @@ suite('CopilotAgentSession', () => { }); test('live tool_complete maps SDK shell_exit content to terminal completion', async () => { - const { mockSession, signals, terminalManager } = await createAgentSession(disposables); + const { session, mockSession, signals, terminalManager } = await createAgentSession(disposables); mockSession.fire('tool.execution_start', { toolCallId: 'tc-shell-exit', @@ -3378,11 +3498,15 @@ suite('CopilotAgentSession', () => { created: terminalManager.outputTerminalsCreated.map(t => t.uri), data: terminalManager.outputTerminalData, finalized: terminalManager.outputTerminalsFinalized, + disposed: terminalManager.disposedTerminals, }, { created: ['agenthost-terminal://shell/test-session-1/tc-shell-exit'], data: [], finalized: [{ uri: 'agenthost-terminal://shell/test-session-1/tc-shell-exit', exitCode: 127 }], + disposed: [], }); + session.dispose(); + assert.deepStrictEqual(terminalManager.disposedTerminals, ['agenthost-terminal://shell/test-session-1/tc-shell-exit']); }); test('live task_complete emits root markdown instead of a tool call', async () => { diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index ced990be17176b..8da80e79daab4b 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -173,6 +173,8 @@ The `NewChatView` input uses the control-tier corner radius for its send button, `ChatView` mounts session input banners directly above the chat input. The CI failures banner uses the orange accent for the card border/icon and for the primary Fix Checks button background/border. +The shared chat input can show a transparent VS Code pet overlay above the composer. `/vscode-pet` toggles the persisted preference in active chats and the new-session composer. The state hooks for idle, sleeping, processing, confirmation, completion, and activation remain wired, but currently every state shows the same idle buddy: blue in Stable and green in Insiders/development builds. Active chats anchor the pet to the actual input row so confirmation and question widgets above it do not add spacing, while the new-session composer anchors it to its input-area wrapper. Cursor-tracked pupils render over eye-less derivative sprites so movement cannot expose the original baked-in eyes; the source PNG and GIF assets remain unchanged. Enabling makes the pet hop into place; disabling makes it duck away before its image source is unloaded. Both transitions are interruptible and skipped when reduced motion is enabled. Hovering the pet invites the user to show it some love and teases future interactions. + When a `ChatView` loads its chat model (`acquireOrLoadSession`), it surfaces progress on **its own** progress bar, pinned to the top of that grid leaf. This mirrors how each editor group owns its `ProgressBar` (see `EditorGroupView`): the bar is created by the leaf host `AbstractChatView`, wrapped in a `ScopedProgressIndicator` (reused from `vs/workbench`) with an always-active scope, and driven via `AbstractChatView.showProgressWhile(promise, delay)`. Concurrent loads in other visible sessions each show their own progress instead of competing for a single part-wide bar, and overlapping loads on the same leaf are joined by the indicator so the bar only hides once all have settled. A short delay avoids flashing the bar for fast (cached) loads. ### 4.2 Visibility Model @@ -212,7 +214,7 @@ When the auxiliary bar is hidden the editor becomes the rightmost card and expan The single-pane editor group renders its title actions from the sessions-owned menus (`getGroupViewOptions` in `browser/parts/singlePaneEditorPart.ts` maps `editorActions` to `Menus.SessionsEditorTitle`), which shadows the core `MenuId.EditorTitle`. So `editor/title` items contributed by **extensions** would otherwise be dropped. `EditorTitleMenuBridgeContribution` in `contrib/editor/browser/editor.contribution.ts` (active only when `isSinglePaneLayoutEnabled`) bridges them: it listens to `MenuRegistry.onDidChangeMenu(MenuId.EditorTitle)` and mirrors **only** the extension-contributed items into `Menus.SessionsEditorTitle`. Extension items are identified two ways: command items by `item.command.source` (set by the `commands` extension point in `menusExtensionPoint.ts`), and submenu items by their `api:`-prefixed `submenu.id` (extension submenus are registered as `MenuId.for('api:')` by the `submenus` extension point). Core items have neither and are not bridged (they are already dual-contributed where needed). The mirror is kept in sync (a `DisposableStore` is cleared and rebuilt on every menu change) so it tracks extensions registering/unregistering. -The Toggle Details action (Toggle Secondary Side Bar in the non-single-pane layout) collapses or restores the secondary side bar while the editor stays open. In the single-pane layout it also has a default keybinding (**`⌥⌘L`**), and maximize/restore of the editor area has a default toggle keybinding (**`⌥⌘E`**, active only while the editor area is visible); both are scoped to the main sessions window with the single-pane setting enabled. The shared **Toggle Secondary Side Bar Visibility** command (`workbench.action.toggleAuxiliaryBar`) calls the layout service's `toggleSecondarySideBar()` operation. The classic layouts implement it by toggling and announcing the auxiliary bar, while the single-pane workbench delegates to its public `toggleEditorPane()` operation, which toggles and announces the editor pane; the command therefore has consistent command-palette and keybinding behavior without knowing a concrete layout. When a session's editor working set is restored on session switch, the editor part is revealed programmatically and the session's saved auxiliary bar visibility is honored (a side bar the user hid for a session stays hidden when returning to it). +The Toggle Details action (Toggle Secondary Side Bar in the non-single-pane layout) collapses or restores the secondary side bar while the editor stays open. In the single-pane layout it also has a default keybinding (**`⌥⌘L`**), and maximize/restore of the editor area has a default toggle keybinding (**`⌥⌘E`**, active only while the editor area is visible); both are scoped to the main sessions window with the single-pane setting enabled. The shared **Toggle Secondary Side Bar Visibility** command (`workbench.action.toggleAuxiliaryBar`) calls the layout service's `toggleSecondarySideBar()` operation. Its checked state uses the layout service's `isSecondarySideBarVisible()` context key, which is the auxiliary bar in classic layouts and the editor pane in single-pane. The classic layouts implement the operation by toggling and announcing the auxiliary bar, while the single-pane workbench delegates to its public `toggleEditorPane()` operation, which toggles and announces the editor pane and moves focus to the detail panel (or sessions list when details are unavailable) after hiding a focused editor pane; the command therefore has consistent command-palette, keybinding, and focus behavior without knowing a concrete layout. When a session's editor working set is restored on session switch, the editor part is revealed programmatically and the session's saved auxiliary bar visibility is honored (a side bar the user hid for a session stays hidden when returning to it). The main editor part can be explicitly revealed for workflows that target it directly. diff --git a/src/vs/sessions/SESSIONS.md b/src/vs/sessions/SESSIONS.md index 56d6de7d4ff0de..e35a3c74e73ba4 100644 --- a/src/vs/sessions/SESSIONS.md +++ b/src/vs/sessions/SESSIONS.md @@ -161,10 +161,18 @@ In the agent host, the real producer of read-only chats is **subagent (worker) c Subagent chats **persist** in the session catalog after the subagent completes (completion only marks the chat's turn complete; the chat is removed only when the whole session is disposed), so the read-only tab stays reviewable for the lifetime of the session. -**Opening a subagent chat from the transcript.** The inline subagent block (`ChatSubagentContentPart`) renders a small pill (`OpenSubagentChatActionViewItem`) that reveals the subagent's read-only tab. The pill is inserted at the **start** of the subagent header row (before the streaming title) so it keeps a fixed position instead of shifting as the title grows; its label reactively shows the **subagent chat's own title** (resolved from the forwarded chat resource via `findSubagentChat`), so in the Agents window the duplicate inline header title is hidden (single chip — the pill is only contributed there, so the CSS is scoped to `.agent-sessions-workbench`). No agent-name phrase is rendered before the pill. The pill itself is a standalone chip styled like the chat file/diff pill (`chat-codeblock-pill-widget`) — a colorless, bordered chip rather than a filled button (`OpenSubagentChatActionViewItem` extends `BaseActionViewItem` and renders its own DOM, avoiding the meta-button's inline-style foreground that CSS can't override) — with a leading conversation icon by default, swapped for a **spinner** while the subagent is still running. The running state is driven by the pill's own `chat-subagent-running` class, which `OpenSubagentChatActionViewItem` toggles reactively from the resolved subagent chat's own `SessionStatus.InProgress` status (the same `findSubagentChat` autorun that resolves the title); the enclosing `.chat-subagent-part`'s `chat-thinking-active` class is kept only as a CSS fallback because the spawning tool call completes as soon as the subagent is dispatched, so it stops early while the worker keeps running (see `media/openSubagentChat.css`). The subagent chat resource is carried to the widget on `IChatSubagentToolInvocationData.chatResource` (populated in `stateToProgressAdapter` from `ToolResultSubagentContent.resource`). Because the chat widget is provider-agnostic and lower-layer, the link invokes the plain-string command `CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID` (`workbench.action.chat.openAgentHostChat`) with the subagent chat URI; the sessions layer registers the handler (`openSubagentChat.ts`), which derives the chatId from the URI (handling the AHP, synthetic-fragment, and backend-path forms), finds the matching surfaced peer across the visible sessions, and calls `sessionsService.openChat` to activate the tab. The command no-ops when no handler is registered (e.g. the widget hosted outside the Agents window). +**Opening a subagent chat from the transcript.** The inline subagent block (`ChatSubagentContentPart`) renders a small pill (`OpenSubagentChatActionViewItem`) that reveals the subagent's read-only tab. The pill is a control-tier chip, not a fully rounded capsule, and is a sibling of the shared collapse button at the start of the header row. When `MenuId.ChatSubagentContent` has an action, `ChatSubagentContentPart` switches to `chat-subagent-open-chat-only` mode and directly hides the collapse button and animation container, leaving the pill as the only affordance. Do not rely on Agents-window CSS ancestry for this switch: the shared chat widget can be hosted through different DOM roots. The pill label reactively shows the subagent chat's own title, with no duplicate agent-name phrase beside it. Status uses the shared pixel spinner: the grid/dropper variant for `InProgress`, the ring variant for `NeedsInput`, and the conversation icon when complete. Normal progress remains neutral—the spinner is sufficient. A subagent with a queued confirmation gets the subtle sessions-list warning background pulse; the subagent whose confirmation is currently active above the input gets the stronger warning border/background. A numeric warning badge is shown only for two or more pending confirmations; one confirmation keeps the warning state without a redundant `1`. The carousel publishes its active subagent id for this presentation state only; confirmation ownership/routing is unchanged. The carousel's subagent reference remains scroll-to-context in regular/editor chat, but in the Agents window it invokes `workbench.action.chat.openAgentHostChat` to open the related read-only chat. A quiet italic duration sits outside the border: `Working for 10s` while active and `Worked for 10s` after completion. Timing starts from the child chat's actual first `activeTurn.startedAt`, updates once per second while active, and freezes from the completed turn duration. Those values are copied onto serialized subagent tool data so stopping or reloading cannot reset the display. `ChatSubagentContentPart` publishes timing, confirmation count, and active-confirmation state through the toolbar action context. The subagent chat resource is carried to the widget on `IChatSubagentToolInvocationData.chatResource` (populated in `stateToProgressAdapter` from `ToolResultSubagentContent.resource`). Because the chat widget is provider-agnostic and lower-layer, the link invokes `workbench.action.chat.openAgentHostChat` with the subagent chat URI; the sessions layer handler derives the chat id, finds the matching surfaced peer across visible sessions, and calls `sessionsService.openChat` to activate the existing tab. + +**Confirmations in read-only subagent chats.** Read-only hides the composer, but the tool-confirmation carousel remains visible and keeps the input part in layout. This lets multi-chat/side-chat subagent views resolve their own confirmations without making the chat message composer interactive. + +A terminal parent response is authoritative for active subagent timing. Stop can surface as either a canceled or completed response depending on the provider/turn path, so every terminal response force-finalizes active subagent parts with a local duration fallback; otherwise `Working for …` can continue indefinitely until reload. Non-terminal responses still follow the child chat's own active state. + +**Subagent terminal progress.** When the last meaningful response part is the parent subagent invocation, its pill is the active progress affordance and `ChatListItemRenderer` must not append a second generic shimmering working-progress phrase below it. Child tool/hook updates can arrive later in the raw response array while still rendering inside an earlier pill, so they must not suppress progress that visually follows normal markdown. Subagent-tagged or regular markdown is supporting output, not the pill itself; if markdown follows the pill, normal working-progress rules apply. **Restoring subagent chats.** Subagent chats are in-memory only; on restart the agent host restores them as separate sessions but no longer re-adds them to the parent catalog. `AgentService._registerRestoredSubagent` mirrors the live `_handleSubagentStarted` flow on restore — it re-adds the subagent to the parent session's catalog (same `ahp-chat://subagent/...` chat URI, `origin: Tool`, `interactivity: ReadOnly`, restored turns) so it reappears as a read-only tab. +History restoration must also repair parent tool calls whose persisted `_meta`/subagent result content was lost. `AgentHostSessionHandler._enrichHistoryWithSubagentCalls` treats the session's tool-origin chat catalog as the canonical spawn record: a serialized tool call whose id matches `origin.toolCallId` is upgraded to `toolSpecificData.kind === "subagent"` with the catalog title/resource, so reload renders the pill instead of a generic "Delegating task" row. + **Subagents in the Chats menu.** Subagents spawned by the **currently-active** chat are shown as a separate group (`2_subagents`) at the bottom of the **Chats** (Conversations) submenu, below the session's regular chats (`1_chats`); a separator divides the two groups. Per-chat association uses `IChatOrigin.parentChat` — the sessions-layer origin carries the spawning chat's resource (mapped from the protocol `ChatOrigin.chat` by the agent host provider's `_resolveParentChatResource`) — so the group changes as the active chat changes. Selecting a subagent entry toggles its read-only tab open/closed like any other chat entry. The entries are populated per session by `SessionConversationsMenuContribution` (only when the active chat has subagents). Subagents on their own do **not** show the chat tab strip: `IActiveSession.shouldShowChatTabs` is shown only when there is more than one visible tab (e.g. a subagent explicitly opened as a tab alongside the main chat) — a subagent that has not been opened as a tab is ignored. The **Chats** menu is always surfaced in the **session header meta row** (at the end of the pills), independent of the strip's visibility, kept available by `SessionActiveChatHasSubagentsContext` even when the parent is the only committed chat. **Background activities above the chat input.** `SessionChatInputToolbar` combines live integrated browsers and running subagents into one background-activities pill. Browsers come from `IBrowserViewWorkbenchService.getKnownBrowserViews()` and belong to the viewed chat when their `IBrowserViewOwner.sessionId` matches that chat or one of its direct tool-origin subagents; subagents come from the owning session's in-progress tool-origin chats whose `origin.parentChat` is the viewed chat. A single activity shows its kind icon and label (browser page title, falling back to "Browser"; subagent title truncated after 30 characters with `...`). Multiple activities of one kind show **N Active Browsers/Subagents**; mixed kinds show **N Background Activities** with the session-in-progress icon. Any multi-item pill opens `IActionWidgetService` with categorized **Browsers** and **Subagents** sections (browser section first), where every selectable row has its kind icon and label. Opening a browser activity prefers a contextual browser page already **Sharing with Agent** for the same destination (exact URL first, then the browser tools' same-host rule), so the user sees the page the agent is driving; when no shared match exists, it opens the activity's normal browser input. The boolean `chat.turnStatusPills` setting gates the entire status-pills surface; for compatibility, any `true` member in the former per-pill object form enables the whole surface. When enabled, completed-turn pills replace the older checkpoint file-changes summary. `ChatView` mounts the toolbar in `ChatInputPart.persistentContentContainerElement`, which remains in layout when `ChatWidget.setReadOnly(true)` hides the rest of the composer, so these pills also remain available on read-only chats. @@ -195,7 +203,7 @@ The session type picker persists the last selection as `{ providerId, sessionTyp On reload, providers register asynchronously and agent hosts connect lazily, so the preferred provider may not have surfaced its session types when the restored draft is created. Rather than blocking on a "ready" gate, `NewChatWidget` creates the draft immediately with the best available provider, then upgrades it in place once the preferred `(providerId, sessionTypeId)` pair becomes servable (driven by `onDidChangeSessionTypes`). The upgrade listener lives for the widget's lifetime — there is **no** timeout or `LifecyclePhase` give-up, since an agent host can connect arbitrarily late — and is cancelled if the user picks a different type or the draft is sent. -Scheduled automations follow the same lazy-registration rule. Before claiming a run row, `AutomationRunner` checks whether its exact target is currently advertised; an unavailable target is deferred without advancing `nextRunAt`, and `AutomationScheduler` retries due automations when `onDidChangeSessionTypes` fires. Once a draft exists, an explicitly selected model waits on `getModelsSnapshot` / `onDidChangeModels` until it is available or conclusively unavailable, re-checking folder-specific session types for workspace-backed drafts. No startup delay or readiness timeout is used. +Scheduled automations follow the same lazy-registration rule. Before claiming a run row, `AutomationRunner` checks whether its exact target is currently advertised; an unavailable target is deferred without advancing `nextRunAt`, and `AutomationScheduler` retries due automations when `onDidChangeSessionTypes` fires. The automation dialog binds its chat input to the selected `ISessionType.chatSessionType` model target rather than the logical session type id, keeping extension-backed and Agent Host model namespaces distinct; when editing a legacy automation, it resolves a matching logical-target model into the selected concrete target before restoring the pick. Once a draft exists, an explicitly selected model waits on `getModelsSnapshot` / `onDidChangeModels` until it is available or conclusively unavailable, then applies the provider-resolved model identifier; workspace-backed drafts also re-check folder-specific session types. No startup delay or readiness timeout is used. ### Quick Chats @@ -239,6 +247,10 @@ Review-capable changesets expose `setReviewState(resource, reviewed)`. Agent-hos → Iterates providers, picks the first one whose resolveWorkspace(folderUri) succeeds (filtered by options.sessionTypeId when given) → Calls provider.createNewSession(folderUri, sessionTypeId) + → If another workspace draft is pending, deletes that provider draft and + fires onDidReplaceNewDraftSession before publishing the replacement + → SessionsTerminalContribution transfers terminals when the drafts share a + cwd/backend; otherwise it rehomes the old terminals as standalone → Returns ISession (model draft, `newSession`); the view then activates it so it becomes the activeSession and the draft slot shows reactively, and openNewSession resolves `{ session, trustDeclined: false }` @@ -607,6 +619,15 @@ Backend state change (turn complete, status update, etc.) Providers may fire `onDidReplaceSession` when a temporary (untitled) session is atomically replaced by a committed one after the first turn. +Workspace draft replacement is management-owned: `createNewSession` creates the +replacement, deletes the previous provider draft, fires +`onDidReplaceNewDraftSession`, and only then publishes the replacement through +`newSession`. Terminal ownership relies on this ordering so compatible terminals +can transfer before activation eagerly ensures the replacement terminal. +Incompatible terminals are detached as standalone terminals and excluded from +future session matching and visibility management. A terminal that finishes +creating after its draft was replaced is disposed before activation. + Provider add notifications are authoritative upserts. A provisional `listSessions()` entry may already be cached when the backend publishes its materialized project and working directory, so providers update the existing session adapter in place and report it as changed rather than replacing its identity. ### First-Time Window-Open Telemetry diff --git a/src/vs/sessions/SESSIONS_LIST.md b/src/vs/sessions/SESSIONS_LIST.md index 803b73342410d3..2523e61b4b1685 100644 --- a/src/vs/sessions/SESSIONS_LIST.md +++ b/src/vs/sessions/SESSIONS_LIST.md @@ -62,7 +62,7 @@ Two grouping modes (user-switchable): - **By Workspace** (default) — user groups and one section per workspace label share a single, freely-reorderable user-managed order below Pinned. By default groups come first and workspaces are alphabetical ("Unknown" workspace last) until the user drags them. - **By Date** — user groups form a contiguous, user-ordered block directly below Pinned; the non-grouped sessions follow in the fixed date sections (Recent, Older), where Recent holds up to 10 sessions from the last 7 days and Older holds the rest. Groups never mix into the date sections. -User groups are **fully user-managed**: their order is owned by `ISessionSectionOrderService`, defaults to newest-first, and is shared across both grouping modes (it no longer derives from the recency of a group's member sessions). +User groups are **fully user-managed**: their order is owned by `ISessionSectionOrderService`, defaults to newest-first, and is shared across both grouping modes (it no longer derives from the recency of a group's member sessions). Groups remain visible and persisted until explicitly deleted. A group with no currently-visible member rows renders a muted **"No session" placeholder row** like the empty Chats section; this includes genuinely empty groups and groups whose members currently render in Pinned/Done or are hidden by a filter. Archived sessions always go to the "Done" section regardless of grouping mode. Archive wins over pin — an archived session is never shown in Pinned. @@ -118,7 +118,7 @@ Regular sessions can be reordered by dragging them up or down within the list. P - **Grouping by Date** — the regular list is one continuous sequence, so dragging can move a session across date buckets (e.g. to the top makes it "Recent"). - **Grouping by Workspace** — reordering is restricted to within the same workspace group; drops onto another workspace are rejected. - **Pinned** — dropping a non-archived session on the Pinned header pins it and lets it sort naturally. Dropping it on a pinned session shows an insertion line, pins it, and stores the sort key needed to place it at that location. -- **User groups** — dropping a non-archived session on a group header adds or moves it into the group and lets it sort naturally. Dropping it on a session inside the group shows an insertion line for the exact slot and highlights only the group header to indicate the receiving group. +- **User groups** — dropping a non-archived session on a group header or its "No session" placeholder adds or moves it into the group and lets it sort naturally. Dropping it on a session inside the group shows an insertion line for the exact slot and highlights only the group header to indicate the receiving group. - **Scope** — archived (Done) sessions do not reorder or move into groups. Drops onto the Done section, unsupported section headers, and "show more" rows are rejected. - **Multi-selection** — dragging multiple selected sessions moves them as a contiguous block, preserving their relative order. The drag label reads `"N sessions"`. Dragging sessions into the sessions grid opens all of them. @@ -136,6 +136,8 @@ The insertion line relies on the base list widget's `drop-target-before`/`drop-t Archived sessions do not show the session group context menu actions ("Create Group", "Add to Group", "Move to Group", or "Remove from Group"). +The **Create Group** context-menu action is also available from list blank space, section headers, group headers, "show more" rows, and placeholder rows. These non-session entry points create an empty group and immediately start inline renaming; the session-row action creates the group with the selected sessions as before. + ### Read / Unread - Read/unread state is **owned by the sessions provider** and surfaced via `ISession.isRead`. Marking happens through `ISessionsManagementService.markRead` / `markUnread` / `markAllRead`, which route to the provider's `setSessionReadState`. The agent-host provider persists it via the protocol `IsRead` status bit; the Copilot Chat provider via its agent session model (`setRead`); the local chat provider via its persisted session metadata. @@ -183,7 +185,7 @@ The sessions list defines menu IDs that contributions can target to add actions. | Menu | Constant | Where it appears | Use for | |------|----------|------------------|---------| -| `SessionGroupToolbar` | `SessionGroupToolbarMenuId` | Toolbar on user-created group headers | Group-scoped actions: "New Session" (opens the new-session composer and joins the started session to the group), "Rename", and "Delete Group". The "New Session" intent is recorded via `ISessionGroupsService.setPendingNewSessionGroup` and bound to the committed session when it is started; abandoning the new session clears it. | +| `SessionGroupToolbar` | `SessionGroupToolbarMenuId` | Toolbar on user-created group headers | Group-scoped actions: "Mark All as Done" when the group has visible sessions, "Delete Group" when it has no members, and "New Session" (opens the new-session composer and joins the started session to the group). A group whose members are all pinned, archived, or filtered out shows neither destructive action. The "New Session" intent is recorded via `ISessionGroupsService.setPendingNewSessionGroup` and bound to the committed session when it is started; abandoning the new session clears it. | ### View Title Menus @@ -238,6 +240,8 @@ Context keys available for `when` clauses when contributing to session list menu | Key | Type | Description | |-----|------|-------------| | `sessionSection.type` | string | `'pinned'`, `'quickchats'`, `'archived'`, `'workspace: