Merge remote-tracking branch 'origin/main' into connor4312/customizations

This commit is contained in:
Connor Peet
2026-03-31 14:03:48 -07:00
75 changed files with 965 additions and 254 deletions
+5 -5
View File
@@ -111,19 +111,19 @@ agent-browser snapshot -i
- Code OSS uses the default user data directory. Unlike VS Code Insiders, you don't typically need `--user-data-dir` since there's usually only one Code OSS instance running.
- If you see "Sent env to running instance. Terminating..." it means Code OSS is already running and forwarded your args to the existing instance. Quit Code OSS and relaunch with the flag, or use `--user-data-dir=/tmp/code-oss-debug` to force a new instance.
## Launching the Sessions App (Agent Sessions Window)
## Launching the Agents App (Agents Window)
The Sessions app is a separate workbench mode launched with the `--sessions` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance.
The Agents app is a separate workbench mode launched with the `--agents` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance.
```bash
cd <repo-root> # the root of your VS Code checkout
./scripts/code.sh --sessions --remote-debugging-port=9224
./scripts/code.sh --agents --remote-debugging-port=9224
```
Wait for the window to fully initialize, then connect:
```bash
# Wait for Sessions app to start, retry until connected
# Wait for Agents app to start, retry until connected
for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done
# Verify you're connected to the right target (not about:blank)
@@ -132,7 +132,7 @@ agent-browser snapshot -i
```
**Tips:**
- The `--sessions` flag launches the Agent Sessions workbench instead of the standard VS Code workbench.
- The `--agents` flag launches the Agents workbench instead of the standard VS Code workbench.
- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built.
## Launching VS Code Extensions for Debugging
@@ -62,6 +62,9 @@ export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModu
doubleQuotedStringLiterals.delete(keyNode);
key = keyNode.value;
} else if (keyNode.type === AST_NODE_TYPES.TemplateLiteral && keyNode.expressions.length === 0 && keyNode.quasis.length === 1) {
key = keyNode.quasis[0].value.cooked ?? undefined;
} else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) {
for (const property of keyNode.properties) {
if (property.type === AST_NODE_TYPES.Property && !property.computed) {
@@ -70,6 +73,9 @@ export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModu
doubleQuotedStringLiterals.delete(property.value);
key = property.value.value;
break;
} else if (property.value.type === AST_NODE_TYPES.TemplateLiteral && property.value.expressions.length === 0 && property.value.quasis.length === 1) {
key = property.value.quasis[0].value.cooked ?? undefined;
break;
}
}
}
+4 -4
View File
@@ -1,15 +1,15 @@
---
name: Sessions Window Developer
description: Specialist in developing the Agent Sessions Window
name: Agents Window Developer
description: Specialist in developing the Agents Window
---
# Role and Objective
You are a developer working on the 'sessions window'. Your goal is to make changes to the sessions window (`src/vs/sessions`), minimally editing outside of that directory.
You are a developer working on the 'agents window'. Your goal is to make changes to the agents window (`src/vs/sessions`), minimally editing outside of that directory.
# Instructions
1. **Always read the `sessions` skill first.** This is your primary source of truth for the sessions architecture.
- Invoke `skill: "sessions"`.
2. Focus your work on `src/vs/sessions/`.
3. Avoid making changes to core VS Code files (`src/vs/workbench/`, `src/vs/platform/`, etc.) unless absolutely necessary for the sessions window functionality.
3. Avoid making changes to core VS Code files (`src/vs/workbench/`, `src/vs/platform/`, etc.) unless absolutely necessary for the agents window functionality.
@@ -1,11 +1,11 @@
---
description: Architecture documentation for the Agent Sessions window — a sessions-first app built as a new top-level layer alongside vs/workbench. Covers layout, parts, chat widget, contributions, entry points, and development guidelines. Use when working in `src/vs/sessions`
description: Architecture documentation for the Agents window — an agents-first app built as a new top-level layer alongside vs/workbench. Covers layout, parts, chat widget, contributions, entry points, and development guidelines. Use when working in `src/vs/sessions`
applyTo: src/vs/sessions/**
---
# Agent Sessions Window
# Agents Window
The Agent Sessions window is a **standalone application** built as a new top-level layer (`vs/sessions`) in the VS Code architecture. It provides a sessions-first experience optimized for agent workflows — a simplified, fixed-layout workbench where chat is the primary interaction surface and editors appear as modal overlays.
The Agents window is a **standalone application** built as a new top-level layer (`vs/sessions`) in the VS Code architecture. It provides an agents-first experience optimized for agent workflows — a simplified, fixed-layout workbench where chat is the primary interaction surface and editors appear as modal overlays.
When working on files under `src/vs/sessions/`, use these skills for detailed guidance:
@@ -1,13 +1,13 @@
---
name: agent-sessions-layout
description: Agent Sessions workbench layout — covers the fixed layout structure, grid configuration, part visibility, editor modal, titlebar, sidebar footer, and implementation requirements. Use when implementing features or fixing issues in the Agent Sessions workbench layout.
description: Agents workbench layout — covers the fixed layout structure, grid configuration, part visibility, editor modal, titlebar, sidebar footer, and implementation requirements. Use when implementing features or fixing issues in the Agents workbench layout.
---
When working on the Agent Sessions workbench layout, always follow these guidelines:
When working on the Agents workbench layout, always follow these guidelines:
## 1. Read the Specification First
The authoritative specification for the Agent Sessions layout lives at:
The authoritative specification for the Agents layout lives at:
**`src/vs/sessions/LAYOUT.md`**
@@ -55,7 +55,7 @@ When proposing or implementing changes, follow these rules from the spec:
|------|---------|
| `sessions/LAYOUT.md` | Authoritative layout specification |
| `sessions/browser/workbench.ts` | Main layout implementation (`Workbench` class) |
| `sessions/browser/menus.ts` | Agent sessions menu IDs (`Menus` export) |
| `sessions/browser/menus.ts` | Agents menu IDs (`Menus` export) |
| `sessions/browser/layoutActions.ts` | Layout actions (toggle sidebar, panel, secondary sidebar) |
| `sessions/browser/paneCompositePartService.ts` | `AgenticPaneCompositePartService` |
| `sessions/browser/media/style.css` | Layout-specific styles |
@@ -67,7 +67,7 @@ When proposing or implementing changes, follow these rules from the spec:
| `sessions/browser/parts/panelPart.ts` | Panel part |
| `sessions/browser/parts/projectBarPart.ts` | Project Bar part (folder entries, icon customization) |
| `sessions/contrib/configuration/browser/configuration.contribution.ts` | Sets `workbench.editor.useModal` to `'all'` for modal editor overlay |
| `sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts` | Title bar widget and session picker |
| `sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts` | Title bar widget and agent picker |
| `sessions/contrib/chat/browser/runScriptAction.ts` | Run script split button for titlebar |
| `sessions/contrib/accountMenu/browser/account.contribution.ts` | Account widget for sidebar footer |
| `sessions/electron-browser/parts/titlebarPart.ts` | Desktop (Electron) titlebar part |
+14 -14
View File
@@ -1,9 +1,9 @@
---
name: sessions
description: Agent Sessions window architecture — covers the sessions-first app, layering, folder structure, chat widget, menus, contributions, entry points, and development guidelines. Use when implementing features or fixing issues in the Agent Sessions window.
description: Agents window architecture — covers the agents-first app, layering, folder structure, chat widget, menus, contributions, entry points, and development guidelines. Use when implementing features or fixing issues in the Agents window.
---
When working on the Agent Sessions window (`src/vs/sessions/`), always follow these guidelines:
When working on the Agents window (`src/vs/sessions/`), always follow these guidelines:
## 1. Read the Specification Documents First
@@ -41,13 +41,13 @@ vs/sessions ← Agent Sessions window (this layer)
### 2.3 How It Differs from VS Code
| Aspect | VS Code Workbench | Agent Sessions Window |
| Aspect | VS Code Workbench | Agents Window |
|--------|-------------------|----------------------|
| Layout | Configurable part positions | Fixed layout, no settings customization |
| Chrome | Activity bar, status bar, banner | Simplified — none of these |
| Primary UX | Editor-centric | Chat-first (Chat Bar is a primary part) |
| Editors | In the grid layout | Modal overlay above the workbench |
| Titlebar | Menubar, editor actions, layout controls | Session picker, run script, toggle sidebar/panel |
| Titlebar | Menubar, editor actions, layout controls | Agent picker, run script, toggle sidebar/panel |
| Navigation | Activity bar with viewlets | Sidebar (views) + sidebar footer (account) |
| Entry point | `vs/workbench` workbench class | `vs/sessions/browser/workbench.ts` `Workbench` class |
@@ -154,7 +154,7 @@ The main editor part is hidden (`display:none`). All editors open via `MODAL_GRO
## 5. Chat Widget
The Agent Sessions chat experience is built around `AgentSessionsChatWidget` — a wrapper around the core `ChatWidget` that adds:
The Agents chat experience is built around `AgentSessionsChatWidget` — a wrapper around the core `ChatWidget` that adds:
- **Deferred session creation** — the UI is interactive before any session resource exists; sessions are created on first message send
- **Target configuration** — observable state tracking which agent provider (Local, Cloud) is selected
@@ -172,17 +172,17 @@ Read `browser/widget/AGENTS_CHAT_WIDGET.md` for the full architecture.
## 6. Menus
The agent sessions window uses **its own menu IDs** defined in `browser/menus.ts` via the `Menus` export. **Never use shared `MenuId.*` constants** from `vs/platform/actions` for agent sessions UI — use the `Menus.*` equivalents instead.
The agents window uses **its own menu IDs** defined in `browser/menus.ts` via the `Menus` export. **Never use shared `MenuId.*` constants** from `vs/platform/actions` for agents window UI — use the `Menus.*` equivalents instead.
| Menu ID | Purpose |
|---------|---------|
| `Menus.ChatBarTitle` | Chat bar title actions |
| `Menus.CommandCenter` | Center toolbar with session picker widget |
| `Menus.CommandCenter` | Center toolbar with agent picker widget |
| `Menus.CommandCenterCenter` | Center section of command center |
| `Menus.TitleBarContext` | Titlebar context menu |
| `Menus.TitleBarLeftLayout` | Left layout toolbar |
| `Menus.TitleBarSessionTitle` | Session title in titlebar |
| `Menus.TitleBarSessionMenu` | Session menu in titlebar |
| `Menus.TitleBarSessionTitle` | Agent title in titlebar |
| `Menus.TitleBarSessionMenu` | Agent menu in titlebar |
| `Menus.TitleBarRightLayout` | Right layout toolbar |
| `Menus.PanelTitle` | Panel title bar actions |
| `Menus.SidebarTitle` | Sidebar title bar actions |
@@ -201,7 +201,7 @@ Defined in `common/contextkeys.ts`:
| `activeChatBar` | `string` | ID of the active chat bar panel |
| `chatBarFocus` | `boolean` | Whether chat bar has keyboard focus |
| `chatBarVisible` | `boolean` | Whether chat bar is visible |
| `sessionsWelcomeVisible` | `boolean` | Whether the sessions welcome overlay is visible |
| `sessionsWelcomeVisible` | `boolean` | Whether the agents welcome overlay is visible |
## 8. Contributions
Feature contributions live under `contrib/<featureName>/browser/` and are registered via imports in `sessions.desktop.main.ts` (desktop) or `sessions.common.main.ts` (browser-compatible).
@@ -210,8 +210,8 @@ Feature contributions live under `contrib/<featureName>/browser/` and are regist
| Contribution | Location | Purpose |
|-------------|----------|---------|
| **Sessions View** | `contrib/sessions/browser/` | Sessions list in sidebar, session picker, active session service |
| **Title Bar Widget** | `contrib/sessions/browser/sessionsTitleBarWidget.ts` | Session picker in titlebar center |
| **Sessions View** | `contrib/sessions/browser/` | Agents list in sidebar, agent picker, active session service |
| **Title Bar Widget** | `contrib/sessions/browser/sessionsTitleBarWidget.ts` | Agent picker in titlebar center |
| **Account Widget** | `contrib/accountMenu/browser/` | Account button in sidebar footer |
| **Chat Actions** | `contrib/chat/browser/` | Chat actions (run script, branch, prompts, customizations debug log) |
| **Changes View** | `contrib/changesView/browser/` | File changes in auxiliary bar |
@@ -229,7 +229,7 @@ Feature contributions live under `contrib/<featureName>/browser/` and are regist
### 8.2 Service Overrides
The agent sessions window registers its own implementations for:
The agents window registers its own implementations for:
- `IPaneCompositePartService``AgenticPaneCompositePartService` (creates agent-specific parts)
- `IPromptsService``AgenticPromptsService` (scopes prompt discovery to active session worktree)
@@ -241,7 +241,7 @@ Service overrides also live under `services/`:
### 8.3 `WindowVisibility.Sessions`
Views and contributions that should only appear in the agent sessions window (not in regular VS Code) use `WindowVisibility.Sessions` in their registration.
Views and contributions that should only appear in the agents window (not in regular VS Code) use `WindowVisibility.Sessions` in their registration.
## 9. Entry Points
+1 -1
View File
@@ -316,7 +316,7 @@
// for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910
"--disable-features=CalculateNativeWinOcclusion",
"--disable-extension=vscode.vscode-api-tests",
"--sessions"
"--agents"
],
"userDataDir": "${userHome}/.vscode-oss-sessions-dev",
"webRoot": "${workspaceFolder}",
+4 -4
View File
@@ -228,14 +228,14 @@
"problemMatcher": []
},
{
"label": "Run Dev Sessions",
"label": "Run Dev Agents",
"type": "shell",
"command": "./scripts/code.sh",
"windows": {
"command": ".\\scripts\\code.bat"
},
"args": [
"--sessions",
"--agents",
"--user-data-dir=${userHome}/.vscode-oss-sessions-dev",
"--extensions-dir=${userHome}/.vscode-oss-sessions-dev/extensions"
],
@@ -248,8 +248,8 @@
"problemMatcher": []
},
{
"label": "Run and Compile Sessions - OSS",
"dependsOn": ["Transpile Client", "Run Dev Sessions"],
"label": "Run and Compile Agents - OSS",
"dependsOn": ["Transpile Client", "Run Dev Agents"],
"dependsOrder": "sequence",
"inSessions": true,
"problemMatcher": []
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.115.0",
"distro": "f7bd750d5f598365ecd892bd9fd2c2b61315db0c",
"distro": "084798d47e4cc4c713def402d4a1110fb619116d",
"author": {
"name": "Microsoft Corporation"
},
+1
View File
@@ -25,6 +25,7 @@ export interface ILegacyQuotaSnapshotData {
export interface IEntitlementsData extends ILegacyQuotaSnapshotData {
readonly access_type_sku: string;
readonly chat_enabled: boolean;
readonly assigned_date: string;
readonly can_signup_for_limited: boolean;
readonly copilot_plan: string;
+12 -12
View File
@@ -412,8 +412,8 @@ export class CodeApplication extends Disposable {
// Mac only event: open new window when we get activated
if (!hasVisibleWindows) {
if ((process as INodeProcess).isEmbeddedApp || (this.environmentMainService.args['sessions'] && this.productService.quality !== 'stable')) {
await this.windowsMainService?.openSessionsWindow({ context: OpenContext.DOCK });
if ((process as INodeProcess).isEmbeddedApp || (this.environmentMainService.args['agents'] && this.productService.quality !== 'stable')) {
await this.windowsMainService?.openAgentsWindow({ context: OpenContext.DOCK });
} else {
await this.windowsMainService?.openEmptyWindow({ context: OpenContext.DOCK });
}
@@ -753,9 +753,9 @@ export class CodeApplication extends Disposable {
const windowOpenable = this.getWindowOpenableFromProtocolUrl(protocolUrl.uri);
if (windowOpenable) {
// Sessions app: skip all window openables (file/folder/workspace)
// Agents app: skip all window openables (file/folder/workspace)
if ((process as INodeProcess).isEmbeddedApp) {
this.logService.trace('app#resolveInitialProtocolUrls() sessions app skipping window openable:', protocolUrl.uri.toString(true));
this.logService.trace('app#resolveInitialProtocolUrls() agents app skipping window openable:', protocolUrl.uri.toString(true));
continue;
}
@@ -904,19 +904,19 @@ export class CodeApplication extends Disposable {
private async handleProtocolUrl(windowsMainService: IWindowsMainService, dialogMainService: IDialogMainService, urlService: IURLService, uri: URI, options?: IOpenURLOptions): Promise<boolean> {
this.logService.trace('app#handleProtocolUrl():', uri.toString(true), options);
// Sessions app: ensure the sessions window is open, then let other handlers process the URL.
// Agents app: ensure the agents window is open, then let other handlers process the URL.
if ((process as INodeProcess).isEmbeddedApp) {
this.logService.trace('app#handleProtocolUrl() sessions app handling protocol URL:', uri.toString(true));
this.logService.trace('app#handleProtocolUrl() agents app handling protocol URL:', uri.toString(true));
// Skip window openables (file/folder/workspace) for security
const windowOpenable = this.getWindowOpenableFromProtocolUrl(uri);
if (windowOpenable) {
this.logService.trace('app#handleProtocolUrl() sessions app skipping window openable:', uri.toString(true));
this.logService.trace('app#handleProtocolUrl() agents app skipping window openable:', uri.toString(true));
return true;
}
// Ensure sessions window is open to receive the URL
const windows = await windowsMainService.openSessionsWindow({ context: OpenContext.LINK, contextWindowId: undefined });
// Ensure agents window is open to receive the URL
const windows = await windowsMainService.openAgentsWindow({ context: OpenContext.LINK, contextWindowId: undefined });
const window = windows.at(0);
window?.focus();
await window?.ready();
@@ -1358,9 +1358,9 @@ export class CodeApplication extends Disposable {
const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP;
const args = this.environmentMainService.args;
// Handle sessions window first based on context
if ((process as INodeProcess).isEmbeddedApp || (args['sessions'] && this.productService.quality !== 'stable')) {
return windowsMainService.openSessionsWindow({ context, contextWindowId: undefined });
// Handle agents window first based on context
if ((process as INodeProcess).isEmbeddedApp || (args['agents'] && this.productService.quality !== 'stable')) {
return windowsMainService.openAgentsWindow({ context, contextWindowId: undefined });
}
// Then check for windows from protocol links to open
+3 -3
View File
@@ -500,9 +500,9 @@ export async function main(argv: string[]): Promise<void> {
// focusing issues when the new instance only sends data to a previous instance and then closes.
const spawnArgs = ['-n', '-g'];
// Figure out the app to launch: with --sessions we try to launch the embedded app
// Figure out the app to launch: with --agents we try to launch the embedded app
let appToLaunch = process.execPath;
if (args.sessions) {
if (args.agents) {
// process.execPath is e.g. /Applications/Code.app/Contents/MacOS/Electron
// Embedded app is at /Applications/Code.app/Contents/Applications/<EmbeddedApp>.app
const contentsPath = dirname(dirname(process.execPath));
@@ -512,7 +512,7 @@ export async function main(argv: string[]): Promise<void> {
const embeddedApp = files.find(file => file.endsWith('.app'));
if (embeddedApp) {
appToLaunch = join(applicationsPath, embeddedApp);
argv = argv.filter(arg => arg !== '--sessions');
argv = argv.filter(arg => arg !== '--agents');
}
} catch (error) {
/* may not exist on disk */
@@ -114,7 +114,7 @@ export class CopilotAgent extends Disposable implements IAgent {
}
}
env['COPILOT_CLI_RUN_AS_NODE'] = '1';
env['USE_BUILTIN_RIPGREP'] = '0';
env['USE_BUILTIN_RIPGREP'] = 'false';
// Resolve the CLI entry point from node_modules. We can't use require.resolve()
// because @github/copilot's exports map blocks direct subpath access.
@@ -221,7 +221,7 @@ echo "=== Step 3: Launching Sessions app ===" >&2
cd "$ROOT"
# Unset ELECTRON_RUN_AS_NODE to ensure the app launches as Electron, not Node.
VSCODE_SKIP_PRELAUNCH=1 ELECTRON_RUN_AS_NODE= ./scripts/code.sh \
--sessions \
--agents \
--skip-sessions-welcome \
--remote-debugging-port="$CDP_PORT" \
--user-data-dir="$USERDATA_DIR" \
+1 -1
View File
@@ -54,7 +54,7 @@ export interface NativeParsedArgs {
goto?: boolean;
'new-window'?: boolean;
'reuse-window'?: boolean;
'sessions'?: boolean;
'agents'?: boolean;
locale?: string;
'user-data-dir'?: string;
'prof-startup'?: boolean;
+1 -1
View File
@@ -108,7 +108,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") },
'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") },
'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") },
'sessions': { type: 'boolean', cat: 'o', description: localize('sessions', "Opens the sessions window.") },
'agents': { type: 'boolean', cat: 'o', deprecates: ['sessions'], description: localize('agents', "Opens the agents window.") },
'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") },
'waitMarkerFilePath': { type: 'string' },
'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") },
@@ -145,9 +145,9 @@ export class LaunchMainService implements ILaunchMainService {
await this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, baseConfig);
}
// Sessions window
else if (args['sessions'] && this.productService.quality !== 'stable') {
usedWindows = await this.windowsMainService.openSessionsWindow({ context, contextWindowId: undefined });
// Agents window
else if (args['agents'] && this.productService.quality !== 'stable') {
usedWindows = await this.windowsMainService.openAgentsWindow({ context, contextWindowId: undefined });
}
// Start without file/folder arguments
+1 -1
View File
@@ -129,7 +129,7 @@ export interface ICommonNativeHostService {
openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;
openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;
openSessionsWindow(): Promise<void>;
openAgentsWindow(): Promise<void>;
isFullScreen(options?: INativeHostOptions): Promise<boolean>;
toggleFullScreen(options?: INativeHostOptions): Promise<void>;
@@ -304,8 +304,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}, options);
}
async openSessionsWindow(windowId: number | undefined): Promise<void> {
await this.windowsMainService.openSessionsWindow({
async openAgentsWindow(windowId: number | undefined): Promise<void> {
await this.windowsMainService.openAgentsWindow({
context: OpenContext.API,
contextWindowId: windowId,
});
@@ -41,7 +41,7 @@ export interface IWindowsMainService {
openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise<ICodeWindow[]>;
openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void;
openSessionsWindow(openConfig: IBaseOpenConfiguration): Promise<ICodeWindow[]>;
openAgentsWindow(openConfig: IBaseOpenConfiguration): Promise<ICodeWindow[]>;
sendToFocused(channel: string, ...args: unknown[]): void;
sendToOpeningWindow(channel: string, ...args: unknown[]): void;
@@ -292,12 +292,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
this.handleChatRequest(openConfig, [window]);
}
async openSessionsWindow(openConfig: IBaseOpenConfiguration): Promise<ICodeWindow[]> {
this.logService.trace('windowsManager#openSessionsWindow');
async openAgentsWindow(openConfig: IBaseOpenConfiguration): Promise<ICodeWindow[]> {
this.logService.trace('windowsManager#openAgentsWindow');
const agentSessionsWorkspaceUri = this.environmentMainService.agentSessionsWorkspace;
if (!agentSessionsWorkspaceUri) {
throw new Error('Sessions workspace is not configured');
throw new Error('Agents workspace is not configured');
}
// Ensure the workspace file exists
+2 -2
View File
@@ -32,7 +32,7 @@ src/vs/workbench/contrib/chat/common/
└── customizationHarnessService.ts # ICustomizationHarnessService + ISectionOverride + helpers
```
The tree view and overview live in `vs/sessions` (sessions window only):
The tree view and overview live in `vs/sessions` (agent sessions window only):
```
src/vs/sessions/contrib/aiCustomizationTreeView/browser/
@@ -61,7 +61,7 @@ src/vs/sessions/contrib/sessions/browser/
The `IAICustomizationWorkspaceService` interface controls per-window behavior:
| Property / Method | Core VS Code | Sessions Window |
| Property / Method | Core VS Code | Agent Sessions Window |
|----------|-------------|----------|
| `managementSections` | All sections except Models | All sections except Models |
| `getStorageSourceFilter(type)` | Delegates to `ICustomizationHarnessService` | Delegates to `ICustomizationHarnessService` |
+1 -1
View File
@@ -199,7 +199,7 @@ When the setting is `'all'`:
The setting `workbench.editor.useModal` is an enum with three values:
- `'off'`: Editors never open in a modal overlay
- `'some'`: Certain editors (e.g. Settings, Keyboard Shortcuts) may open in a modal overlay when requested via `MODAL_GROUP`
- `'all'`: All editors open in a modal overlay (used by sessions window)
- `'all'`: All editors open in a modal overlay (used by agent sessions window)
---
+1 -1
View File
@@ -114,7 +114,7 @@ See [LAYOUT.md](LAYOUT.md) for the detailed layout specification.
## Sessions Provider Architecture
The sessions window uses an extensible provider model to manage sessions. Instead of hardcoding session type logic (CLI, Cloud, Agent Host) throughout the codebase, all session behavior is encapsulated in **sessions providers** that register with a central registry.
The agent sessions window uses an extensible provider model to manage sessions. Instead of hardcoding session type logic (CLI, Cloud, Agent Host) throughout the codebase, all session behavior is encapsulated in **sessions providers** that register with a central registry.
### Overview Diagram
+1 -1
View File
@@ -2,7 +2,7 @@
## Overview
The Sessions Provider architecture introduces an **extensible provider model** for managing agent sessions in the Sessions window. Instead of hardcoding session types and backends, multiple providers register with a central registry (`ISessionsProvidersService`), which aggregates sessions from all providers and routes actions to the correct one.
The Sessions Provider architecture introduces an **extensible provider model** for managing agent sessions in the Agent Sessions window. Instead of hardcoding session types and backends, multiple providers register with a central registry (`ISessionsProvidersService`), which aggregates sessions from all providers and routes actions to the correct one.
This design allows new compute environments (remote agent hosts, cloud backends, third-party agents) to plug in without modifying core session management code.
+1 -1
View File
@@ -896,7 +896,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
!this.partVisibility.panel ? LayoutClasses.PANEL_HIDDEN : undefined,
!this.partVisibility.auxiliaryBar ? LayoutClasses.AUXILIARYBAR_HIDDEN : undefined,
!this.partVisibility.chatBar ? LayoutClasses.CHATBAR_HIDDEN : undefined,
LayoutClasses.STATUSBAR_HIDDEN, // sessions window never has a status bar
LayoutClasses.STATUSBAR_HIDDEN, // agents window never has a status bar
this.mainWindowFullscreen ? LayoutClasses.FULLSCREEN : undefined
]);
}
+1 -1
View File
@@ -6,5 +6,5 @@
import { localize2 } from '../../nls.js';
export const SessionsCategories = Object.freeze({
Sessions: localize2('sessions', "Sessions"),
Sessions: localize2('agents', "Agents"),
});
@@ -263,7 +263,7 @@ export class AccountWidget extends ActionViewItem {
if (state.type === StateType.AvailableForDownload && state.canInstall === false) {
const { confirmed } = await this.dialogService.confirm({
message: localize('updateFromVSCode.title', "Update from VS Code"),
detail: localize('updateFromVSCode.detail', "This will close the Sessions app and open VS Code so you can install the update.\n\nLaunch Sessions again after the update is complete."),
detail: localize('updateFromVSCode.detail', "This will close the Agents app and open VS Code so you can install the update.\n\nLaunch Agents again after the update is complete."),
primaryButton: localize('updateFromVSCode.open', "Close and Open VS Code"),
});
if (confirmed) {
@@ -311,7 +311,7 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu
constructor() {
super({
id: sessionsAccountWidgetAction,
title: localize2('sessionsAccountWidget', 'Sessions Account'),
title: localize2('agentsAccountWidget', 'Agents Account'),
menu: {
id: Menus.SidebarFooter,
group: 'navigation',
@@ -80,7 +80,7 @@ function renderAccountWidget(ctx: ComponentFixtureContext, state: State, account
additionalServices: registerWorkbenchServices,
});
const action = ctx.disposableStore.add(new Action('sessions.action.accountWidget', 'Sessions Account'));
const action = ctx.disposableStore.add(new Action('sessions.action.accountWidget', 'Agents Account'));
const contextMenuService = instantiationService.get(IContextMenuService);
const menuService = instantiationService.get(IMenuService);
const contextKeyService = instantiationService.get(IContextKeyService);
@@ -286,19 +286,17 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
private _renderSuggestion(comment: ISessionEditorComment): HTMLElement {
const suggestionNode = $('div.agent-feedback-widget-suggestion');
const title = $('div.agent-feedback-widget-suggestion-title');
title.textContent = nls.localize('suggestedChange', "Suggested Change");
suggestionNode.appendChild(title);
for (const edit of comment.suggestion?.edits ?? []) {
const editNode = $('div.agent-feedback-widget-suggestion-edit');
const rangeLabel = $('div.agent-feedback-widget-suggestion-range');
const header = $('div.agent-feedback-widget-suggestion-header');
if (edit.range.startLineNumber === edit.range.endLineNumber) {
rangeLabel.textContent = nls.localize('suggestionLineNumber', "Line {0}", edit.range.startLineNumber);
header.textContent = nls.localize('suggestedChangeLine', "Suggested Change \u2022 Line {0}", edit.range.startLineNumber);
} else {
rangeLabel.textContent = nls.localize('suggestionLineRange', "Lines {0}-{1}", edit.range.startLineNumber, edit.range.endLineNumber);
header.textContent = nls.localize('suggestedChangeLines', "Suggested Change \u2022 Lines {0}-{1}", edit.range.startLineNumber, edit.range.endLineNumber);
}
editNode.appendChild(rangeLabel);
editNode.appendChild(header);
const newText = $('pre.agent-feedback-widget-suggestion-text');
newText.textContent = edit.newText;
@@ -85,6 +85,7 @@
/* Title */
.agent-feedback-widget-title {
font-weight: 500;
line-height: 12px;
color: var(--vscode-foreground);
white-space: nowrap;
overflow: hidden;
@@ -104,6 +105,7 @@
justify-content: center;
width: 22px;
height: 22px;
min-width: 22px;
border-radius: var(--vscode-cornerRadius-medium);
cursor: pointer;
color: var(--vscode-foreground);
@@ -154,14 +156,6 @@
color: var(--vscode-list-activeSelectionForeground);
}
.agent-feedback-widget-item-codeReview {
box-shadow: inset 2px 0 0 var(--vscode-editorWarning-foreground);
}
.agent-feedback-widget-item-prReview {
box-shadow: inset 2px 0 0 var(--vscode-editorInfo-foreground);
}
.agent-feedback-widget-item-header {
display: flex;
align-items: center;
@@ -195,7 +189,7 @@
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: 999px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.2px;
@@ -204,12 +198,12 @@
}
.agent-feedback-widget-item-codeReview .agent-feedback-widget-item-type {
background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 22%, transparent);
background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 22%, var(--vscode-editorWidget-background));
color: var(--vscode-editorWarning-foreground);
}
.agent-feedback-widget-item-prReview .agent-feedback-widget-item-type {
background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 22%, transparent);
background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 22%, var(--vscode-editorWidget-background));
color: var(--vscode-editorInfo-foreground);
}
@@ -244,21 +238,19 @@
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
border-radius: 6px;
background: color-mix(in srgb, var(--vscode-editorWidget-border, var(--vscode-widget-border)) 12%, transparent);
margin-top: 8px;
padding: 0px 8px 4px 12px;
}
.agent-feedback-widget-item-codeReview .agent-feedback-widget-suggestion {
background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 10%, transparent);
border-left: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground) 50%, transparent);
}
.agent-feedback-widget-item-prReview .agent-feedback-widget-suggestion {
background: color-mix(in srgb, var(--vscode-editorInfo-foreground) 10%, transparent);
border-left: 1px solid color-mix(in srgb, var(--vscode-editorInfo-foreground) 50%, transparent);
}
.agent-feedback-widget-suggestion-title,
.agent-feedback-widget-suggestion-range {
.agent-feedback-widget-suggestion-header {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
@@ -278,10 +270,10 @@
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
font-family: monospace;
font-family: var(--monaco-monospace-font);
font-size: 11px;
line-height: 1.45;
background: color-mix(in srgb, var(--vscode-editor-background) 65%, transparent);
background: var(--vscode-editorWidget-background);
}
/* Gutter decoration for range indicator on hover */
@@ -148,7 +148,7 @@ class CICheckListRenderer implements IListRenderer<ICICheckListItem, ICICheckTem
*/
export class CIStatusWidget extends Disposable {
static readonly HEADER_HEIGHT = 38; // total header height in px
static readonly HEADER_HEIGHT = 34; // total header height in px
static readonly MIN_BODY_HEIGHT = 84; // at least 3 checks (3 * 28)
static readonly PREFERRED_BODY_HEIGHT = 112; // preferred 4 checks (4 * 28)
static readonly MAX_BODY_HEIGHT = 240; // at most ~8 checks
@@ -7,7 +7,7 @@
display: flex;
flex-direction: column;
height: 100%;
padding: 4px 8px 8px 8px;
padding: 4px 8px;
box-sizing: border-box;
}
@@ -18,10 +18,9 @@
position: relative;
display: flex;
align-items: center;
/* gap: 6px; */
padding: 6px 4px;
padding: 4px;
margin-top: 4px;
border-radius: 8px;
border-radius: 4px;
min-height: 22px;
font-weight: 500;
cursor: pointer;
@@ -25,7 +25,7 @@ import { IUserDataProfileService } from '../../../../workbench/services/userData
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
/** URI root for built-in skills bundled with the Sessions app. */
/** URI root for built-in skills bundled with the Agents app. */
export const BUILTIN_SKILLS_URI = FileAccess.asFileUri('vs/sessions/skills');
export class AgenticPromptsService extends PromptsService {
@@ -222,7 +222,7 @@ export class RunScriptCustomTaskWidget extends Disposable {
private _getSubmitLabel(): string {
if (this._isAddExistingTask) {
return localize('confirmAddToSessions', "Add to Sessions Window");
return localize('confirmAddToAgents', "Add to Agents Window");
}
if (!this._isExistingTask) {
return localize('confirmAddTask', "Add Task");
@@ -367,7 +367,7 @@ export class WorkspacePicker extends Disposable {
const remoteStatus = remoteProvider?.connectionStatus?.get();
const actionItems = actions.map(({ action, index }, ci) => toAction({
id: `workspacePicker.browse.${index}`,
label: localize(`workspacePicker.browse`, "{0}...", action.label),
label: localize(`workspacePicker.browseAction`, "{0}...", action.label),
tooltip: ci === 0 ? provider.label : '',
enabled: remoteStatus !== RemoteAgentHostConnectionStatus.Disconnected && remoteStatus !== RemoteAgentHostConnectionStatus.Connecting,
run: () => this._executeBrowseAction(index),
@@ -382,7 +382,7 @@ export class WorkspacePicker extends Disposable {
items.push({
kind: ActionListItemKind.Action,
label: localize('workspacePicker.browse', "Select..."),
label: localize('workspacePicker.browseSelect', "Select..."),
group: { title: '', icon: Codicon.folderOpened },
item: {},
submenuActions,
@@ -392,7 +392,7 @@ export class WorkspacePicker extends Disposable {
const action = allBrowseActions[i];
items.push({
kind: ActionListItemKind.Action,
label: localize(`workspacePicker.browse`, "Select {0}...", action.label),
label: localize(`workspacePicker.browseSelectAction`, "Select {0}...", action.label),
group: { title: '', icon: action.icon },
item: { browseActionIndex: i },
});
@@ -12,7 +12,7 @@ export type { AICustomizationPromptsStorage } from '../../../../workbench/contri
export { BUILTIN_STORAGE } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
/**
* Prompt path for built-in prompts bundled with the Sessions app.
* Prompt path for built-in prompts bundled with the Agents app.
*/
export interface IBuiltinPromptPath {
readonly uri: URI;
@@ -86,6 +86,10 @@
text-align: left;
}
.agent-sessions-new-button-container .monaco-button:focus {
outline-offset: -1px !important;
}
.agent-sessions-new-button-container .monaco-button .new-session-button-label {
display: inline-flex;
align-items: center;
@@ -48,7 +48,7 @@ class SessionsWelcomeOverlay extends Disposable {
this.overlay = append(container, $('.sessions-welcome-overlay'));
this.overlay.setAttribute('role', 'dialog');
this.overlay.setAttribute('aria-modal', 'true');
this.overlay.setAttribute('aria-label', localize('welcomeOverlay.aria', "Sign in to use Sessions"));
this.overlay.setAttribute('aria-label', localize('welcomeOverlay.aria', "Sign in to use Agents"));
this._register(toDisposable(() => this.overlay.remove()));
const card = append(this.overlay, $('.sessions-welcome-card'));
@@ -57,7 +57,7 @@ class SessionsWelcomeOverlay extends Disposable {
const header = append(card, $('.sessions-welcome-header'));
const iconEl = append(header, $('span.sessions-welcome-icon'));
iconEl.appendChild(renderIcon(Codicon.agent));
append(header, $('h2', undefined, localize('welcomeTitle', "Sign in to use Sessions")));
append(header, $('h2', undefined, localize('welcomeTitle', "Sign in to use Agents")));
append(header, $('p.sessions-welcome-subtitle', undefined, localize('welcomeSubtitle', "Agent-powered development")));
// Action area
@@ -93,8 +93,8 @@ class SessionsWelcomeOverlay extends Disposable {
const success = await this.commandService.executeCommand<boolean>(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID, {
dialogIcon: Codicon.agent,
dialogTitle: this.chatEntitlementService.anonymous ?
localize('sessions.startUsingSessions', "Start using Sessions") :
localize('sessions.signinRequired', "Sign in to use Sessions"),
localize('agents.startUsingAgents', "Start using Agents") :
localize('agents.signinRequired', "Sign in to use Agents"),
});
if (success) {
@@ -259,7 +259,7 @@ registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.resetSessionsWelcome',
title: localize2('resetSessionsWelcome', "Reset Sessions Welcome"),
title: localize2('resetSessionsWelcome', "Reset Agents Welcome"),
category: Categories.Developer,
f1: true,
});
@@ -4,7 +4,7 @@
>
> **Source:** `github/copilot-agent-runtime` codebase as of 2026-02-25.
> Some information has been removed by the human compiling this spec, scoping to what is deemed most relevant for the sessions window implementation. For the full details, see the source code (for maintainers likely checked out side-by-side).
> Some information has been removed by the human compiling this spec, scoping to what is deemed most relevant for the agent sessions window implementation. For the full details, see the source code (for maintainers likely checked out side-by-side).
---
+1 -1
View File
@@ -33,7 +33,7 @@ export const RemoteNameContext = new RawContextKey<string>('remoteName', '', loc
export const VirtualWorkspaceContext = new RawContextKey<string>('virtualWorkspace', '', localize('virtualWorkspace', "The scheme of the current workspace is from a virtual file system or an empty string."));
export const TemporaryWorkspaceContext = new RawContextKey<boolean>('temporaryWorkspace', false, localize('temporaryWorkspace', "The scheme of the current workspace is from a temporary file system."));
export const IsSessionsWindowContext = new RawContextKey<boolean>('isSessionsWindow', false, localize('isSessionsWindow', "Whether the current window is a sessions window."));
export const IsSessionsWindowContext = new RawContextKey<boolean>('isSessionsWindow', false, localize('isSessionsWindow', "Whether the current window is a agent sessions window."));
export const HasWebFileSystemAccess = new RawContextKey<boolean>('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access)
+2 -2
View File
@@ -69,11 +69,11 @@ export const enum WindowVisibility {
*/
Editor = 1,
/**
* Visible only in sessions window
* Visible only in agent sessions window
*/
Sessions = 2,
/**
* Visible in both editor and sessions windows
* Visible in both editor and agent sessions windows
*/
Both = 3,
}
@@ -4,12 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../../base/common/codicons.js';
import { fromNow } from '../../../../../base/common/date.js';
import { isUriComponents, URI } from '../../../../../base/common/uri.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { localize2 } from '../../../../../nls.js';
import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IChatService } from '../../common/chatService/chatService.js';
import { ILanguageModelsService } from '../../common/languageModels.js';
@@ -32,9 +35,64 @@ export function registerChatDeveloperActions() {
registerAction2(LogChatInputHistoryAction);
registerAction2(LogChatIndexAction);
registerAction2(InspectChatModelAction);
registerAction2(InspectChatModelReferencesAction);
registerAction2(ClearRecentlyUsedLanguageModelsAction);
}
function formatChatModelReferenceInspection(accessor: ServicesAccessor): string {
const chatService = accessor.get(IChatService);
const agentSessionsService = accessor.get(IAgentSessionsService);
const debugInfo = chatService.getChatModelReferenceDebugInfo();
const referencedModels = debugInfo.models.filter(model => model.referenceCount > 0);
const pendingEditModels = debugInfo.models.filter(model => model.hasPendingEdits);
const pendingDisposalModels = debugInfo.models.filter(model => model.pendingDisposal);
let output = '# Chat Model References\n\n';
output += `- Live models: ${debugInfo.totalModels}\n`;
output += `- Live references: ${debugInfo.totalReferences}\n`;
output += `- Models with active references: ${referencedModels.length}\n`;
output += `- Models with pending edits: ${pendingEditModels.length}\n`;
output += `- Models pending disposal: ${pendingDisposalModels.length}\n\n`;
output += 'Created by shows who loaded or created the model. Holders shows who currently keeps the model alive.\n\n';
if (!debugInfo.models.length) {
output += 'No live chat models.\n';
return output;
}
for (const model of debugInfo.models) {
const liveModel = chatService.getSession(model.sessionResource);
const agentSession = agentSessionsService.getSession(model.sessionResource);
const archived = agentSession ? agentSession.isArchived() : 'unknown';
const age = liveModel ? fromNow(liveModel.timing.created, true, true, true) : 'unknown';
output += `## ${model.title || '(untitled)'}\n\n`;
output += `- Session: ${model.sessionResource.toString()}\n`;
output += `- Created by: ${model.createdBy}\n`;
output += `- Archived: ${archived}\n`;
output += `- Age: ${age}\n`;
output += `- Initial location: ${model.initialLocation}\n`;
output += `- Imported: ${model.isImported}\n`;
output += `- Pending edits: ${model.hasPendingEdits}\n`;
output += `- Background keep-alive enabled: ${model.willKeepAlive}\n`;
output += `- Pending disposal: ${model.pendingDisposal}\n`;
output += `- Reference count: ${model.referenceCount}\n`;
if (model.holders.length) {
output += '- Holders:\n';
for (const holder of model.holders) {
output += ` - ${holder.holder}: ${holder.count}\n`;
}
} else {
output += '- Holders: none\n';
}
output += '\n';
}
return output;
}
class LogChatInputHistoryAction extends Action2 {
static readonly ID = 'workbench.action.chat.logInputHistory';
@@ -129,6 +187,35 @@ class InspectChatModelAction extends Action2 {
}
}
class InspectChatModelReferencesAction extends Action2 {
static readonly ID = 'workbench.action.chat.inspectChatModelReferences';
constructor() {
super({
id: InspectChatModelReferencesAction.ID,
title: localize2('workbench.action.chat.inspectChatModelReferences.label', "Inspect Chat Model References"),
icon: Codicon.inspect,
category: Categories.Developer,
f1: true,
precondition: ChatContextKeys.enabled
});
}
override async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const editorService = accessor.get(IEditorService);
await editorService.openEditor({
resource: undefined,
contents: instantiationService.invokeFunction(formatChatModelReferenceInspection),
languageId: 'markdown',
options: {
pinned: true
}
});
}
}
class ClearRecentlyUsedLanguageModelsAction extends Action2 {
static readonly ID = 'workbench.action.chat.clearRecentlyUsedLanguageModels';
@@ -95,7 +95,7 @@ export function registerChatForkActions() {
}
}
const modelRef = chatService.loadSessionFromData(cleanData);
const modelRef = chatService.loadSessionFromData(cleanData, 'ChatForkActions#forkCleanSession');
// Defer navigation until after the slash command flow completes.
const newSessionResource = modelRef.object.sessionResource;
@@ -216,16 +216,19 @@ export function registerChatForkActions() {
}
}
const modelRef = chatService.loadSessionFromData(forkedData);
const modelRef = chatService.loadSessionFromData(forkedData, 'ChatForkActions#forkSession');
if (!modelRef) {
return;
}
// Navigate to the new session in the chat view pane
const newSessionResource = modelRef.object.sessionResource;
await chatWidgetService.openSession(newSessionResource, ChatViewPaneTarget);
modelRef.dispose();
try {
const newSessionResource = modelRef.object.sessionResource;
await chatWidgetService.openSession(newSessionResource, ChatViewPaneTarget);
} finally {
modelRef.dispose();
}
}
private pendingFork = new Map<string, Promise<void>>();
@@ -123,17 +123,21 @@ export function registerChatExportActions() {
let options: IChatEditorOptions;
if (opts?.target === 'chatViewPane') {
const modelRef = chatService.loadSessionFromData(data);
sessionResource = modelRef.object.sessionResource;
resolvedTarget = ChatViewPaneTarget;
options = { pinned: true };
const modelRef = chatService.loadSessionFromData(data, 'ChatImportExport#importToChatView');
try {
sessionResource = modelRef.object.sessionResource;
resolvedTarget = ChatViewPaneTarget;
options = { pinned: true };
await widgetService.openSession(sessionResource, resolvedTarget, options);
} finally {
modelRef.dispose();
}
} else {
sessionResource = ChatEditorInput.getNewEditorUri();
resolvedTarget = ACTIVE_GROUP;
options = { target: { data }, pinned: true };
await widgetService.openSession(sessionResource, resolvedTarget, options);
}
await widgetService.openSession(sessionResource, resolvedTarget, options);
} catch (err) {
throw err;
}
@@ -79,7 +79,7 @@ export class AgentSessionHoverWidget extends Disposable {
}
private async loadModel() {
const modelRef = await this.chatService.acquireOrLoadSession(this.session.resource, ChatAgentLocation.Chat, this.cts.token);
const modelRef = await this.chatService.acquireOrLoadSession(this.session.resource, ChatAgentLocation.Chat, this.cts.token, 'AgentSessionHoverWidget#loadModel');
if (this._store.isDisposed) {
modelRef?.dispose();
return;
@@ -159,7 +159,7 @@
.ai-customization-management-editor .content-inner {
height: 100%;
padding: 8px 12px;
padding: 10px;
box-sizing: border-box;
}
@@ -571,7 +571,6 @@
.ai-customization-list-widget .section-footer .section-footer-description {
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
margin: 0 0 8px 0;
}
@@ -979,8 +978,7 @@
display: flex;
align-items: center;
gap: 12px;
padding: 8px 4px 12px 4px;
border-bottom: 1px solid var(--vscode-widget-border);
padding: 4px 0;
flex-shrink: 0;
}
@@ -495,6 +495,8 @@ async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAcce
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
}
await chatService.cancelCurrentRequestForSession(sessionResource, 'restoreCheckpoint');
// Restore the snapshot to what it was before the request(s) that we deleted
const snapshotRequestId = chatRequests[itemIndex].id;
await session.restoreSnapshot(snapshotRequestId, undefined);
@@ -556,7 +556,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
let attachedContext = chatOptions.attachedContext;
const resource = URI.revive(chatOptions.resource);
const ref = await chatService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None);
const ref = await chatService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatSessionsContribution#sendPrompt');
try {
const promptFile = await resolvePromptSlashCommand(chatOptions.prompt, promptsService, toolsService);
if (promptFile) {
@@ -413,13 +413,13 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [
excludeWhenToolsInvoked: ['listDebugEvents'],
},
{
id: 'tip.openSessionsWindow',
id: 'tip.openAgentsWindow',
tier: ChatTipTier.Qol,
buildMessage() {
return new MarkdownString(
localize(
'tip.openSessionsWindow',
"Try the [Sessions Window](command:workbench.action.openSessionsWindow \"Open Sessions Window\") to run multiple agents simultaneously and manage your coding sessions."
'tip.openAgentsWindow',
"Try the [Agents Application](command:workbench.action.openAgentsWindow \"Open Agents Application\") to run multiple agents simultaneously and manage your coding sessions."
)
);
},
@@ -428,7 +428,7 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [
IsSessionsWindowContext.negate(),
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
),
excludeWhenCommandsExecuted: ['workbench.action.openSessionsWindow'],
dismissWhenCommandsClicked: ['workbench.action.openSessionsWindow'],
excludeWhenCommandsExecuted: ['workbench.action.openAgentsWindow'],
dismissWhenCommandsClicked: ['workbench.action.openAgentsWindow'],
},
];
@@ -91,6 +91,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
// Model name used by this subagent for hover tooltip
private modelName: string | undefined;
private _isDefaultDescription: boolean;
private readonly _hoverDisposable = this._register(new MutableDisposable());
// Confirmation auto-expand tracking
@@ -114,18 +115,20 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
/**
* Extracts subagent info (description, agentName, prompt) from a tool invocation.
*/
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } {
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; isDefaultDescription: boolean; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } {
const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent');
// Only parent subagent tools contain the full subagent info
if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) {
return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined };
return { description: defaultDescription, isDefaultDescription: true, agentName: undefined, prompt: undefined, modelName: undefined };
}
// Check toolSpecificData first (works for both live and serialized)
if (toolInvocation.toolSpecificData?.kind === 'subagent') {
const hasDescription = !!toolInvocation.toolSpecificData.description;
return {
description: toolInvocation.toolSpecificData.description ?? defaultDescription,
isDefaultDescription: !hasDescription,
agentName: toolInvocation.toolSpecificData.agentName,
prompt: toolInvocation.toolSpecificData.prompt,
modelName: toolInvocation.toolSpecificData.modelName,
@@ -138,15 +141,17 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
const params = state.type !== IChatToolInvocation.StateKind.Streaming ?
state.parameters as IRunSubagentToolInputParams | undefined
: undefined;
const hasDescription = !!params?.description;
return {
description: params?.description ?? defaultDescription,
isDefaultDescription: !hasDescription,
agentName: params?.agentName,
prompt: params?.prompt,
modelName: undefined,
};
}
return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined };
return { description: defaultDescription, isDefaultDescription: true, agentName: undefined, prompt: undefined, modelName: undefined };
}
constructor(
@@ -164,7 +169,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
@IConfigurationService configurationService: IConfigurationService,
) {
// Extract description, agentName, and prompt from toolInvocation
const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
const { description, isDefaultDescription, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
// Build title: "AgentName: description" or "Subagent: description"
const prefix = agentName || localize('chat.subagent.prefix', 'Subagent');
@@ -172,6 +177,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
super(initialTitle, context, undefined, hoverService, configurationService);
this.description = description;
this._isDefaultDescription = isDefaultDescription;
this.agentName = agentName;
this.prompt = prompt;
this.modelName = modelName;
@@ -354,6 +360,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
if (this._collapseButton) {
this._collapseButton.icon = Codicon.check;
}
if (this._isDefaultDescription) {
this.description = localize('chat.subagent.completedDefaultDescription', 'Ran subagent');
}
this.finalizeTitle();
// Collapse when done
this.setExpanded(false);
@@ -516,10 +526,16 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
this.renderResultText(textParts.join('\n'));
}
// Update model name from toolSpecificData (set during invoke())
if (toolInvocation.toolSpecificData?.kind === 'subagent' && toolInvocation.toolSpecificData.modelName) {
this.modelName = toolInvocation.toolSpecificData.modelName;
this.updateHover();
// Update description and model name from toolSpecificData (set during invoke())
if (toolInvocation.toolSpecificData?.kind === 'subagent') {
if (toolInvocation.toolSpecificData.description) {
this.description = toolInvocation.toolSpecificData.description;
this._isDefaultDescription = false;
}
if (toolInvocation.toolSpecificData.modelName) {
this.modelName = toolInvocation.toolSpecificData.modelName;
this.updateHover();
}
}
// Mark as inactive when the tool completes
@@ -527,8 +543,9 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
} else if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) {
wasStreaming = false;
// Update things that change when tool is done streaming
const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
const { description, isDefaultDescription, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
this.description = description;
this._isDefaultDescription = isDefaultDescription;
this.agentName = agentName;
this.prompt = prompt;
if (modelName) {
@@ -122,9 +122,6 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt
}
protected override _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined {
if (this._isSessionsWindow && sessionTypeItem.type === AgentSessionProviders.Cloud && !this._hasGitRepository()) {
return localize('chat.cloudRequiresGit', "Requires a Git repository");
}
return undefined;
}
@@ -394,7 +394,7 @@ class QuickChat extends Disposable {
}
private updateModel(): void {
this.modelRef ??= this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true });
this.modelRef ??= this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true, debugOwner: 'ChatQuick#updateModel' });
const model = this.modelRef?.object;
if (!model) {
throw new Error('Could not start chat session');
@@ -213,16 +213,16 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
const inputType = chatSessionType ?? this.resource.authority;
if (this._sessionResource) {
this.modelRef.value = await this.chatService.acquireOrLoadSession(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None);
this.modelRef.value = await this.chatService.acquireOrLoadSession(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatEditorInput#resolve');
// For local session only, if we find no existing session, create a new one
if (!this.model && LocalChatSessionUri.parseLocalSessionId(this._sessionResource)) {
this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: true });
this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: true, debugOwner: 'ChatEditorInput#resolveNewLocalSession' });
}
} else if (!this.options.target) {
this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: !inputType });
this.modelRef.value = this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { canUseTools: !inputType, debugOwner: 'ChatEditorInput#resolveUntitled' });
} else if (this.options.target.data) {
this.modelRef.value = this.chatService.loadSessionFromData(this.options.target.data);
this.modelRef.value = this.chatService.loadSessionFromData(this.options.target.data, 'ChatEditorInput#resolveImportedData');
}
if (!this.model || this.isDisposed()) {
@@ -253,7 +253,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
if (!this._widget?.viewModel && !this.restoringSession) {
const sessionResource = this.getTransferredOrPersistedSessionInfo();
this.restoringSession =
(sessionResource ? this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None) : Promise.resolve(undefined)).then(async modelRef => {
(sessionResource ? this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatViewPane#onDidChangeAgents') : Promise.resolve(undefined)).then(async modelRef => {
if (!this._widget) {
return; // renderBody has not been called yet
}
@@ -699,7 +699,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
private async _applyModel(): Promise<void> {
const sessionResource = this.getTransferredOrPersistedSessionInfo();
const modelRef = sessionResource ? await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None) : undefined;
const modelRef = sessionResource ? await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatViewPane#applyModel') : undefined;
await this.showModel(CancellationToken.None, modelRef);
}
@@ -714,8 +714,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
let ref: IChatModelReference | undefined;
if (startNewSession) {
ref = modelRef ?? (this.chatService.transferredSessionResource
? await this.chatService.acquireOrLoadSession(this.chatService.transferredSessionResource, ChatAgentLocation.Chat, token)
: this.chatService.startNewLocalSession(ChatAgentLocation.Chat));
? await this.chatService.acquireOrLoadSession(this.chatService.transferredSessionResource, ChatAgentLocation.Chat, token, 'ChatViewPane#showModel')
: this.chatService.startNewLocalSession(ChatAgentLocation.Chat, { debugOwner: 'ChatViewPane#showModel' }));
if (!ref) {
throw new Error('Could not start chat session');
}
@@ -836,7 +836,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
const clearWidgetCancellationListener = token.onCancellationRequested(() => clearWidget.dispose());
try {
const newModelRef = await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, token);
const newModelRef = await this.chatService.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, token, 'ChatViewPane#loadSession');
clearWidget.dispose();
await queue;
@@ -14,7 +14,6 @@ import { ThemeIcon } from '../../../../../base/common/themables.js';
import { hasKey } from '../../../../../base/common/types.js';
import { URI, UriComponents } from '../../../../../base/common/uri.js';
import { IRange, Range } from '../../../../../editor/common/core/range.js';
import { HookTypeValue } from '../promptSyntax/hookTypes.js';
import { ISelection } from '../../../../../editor/common/core/selection.js';
import { Command, Location, TextEdit } from '../../../../../editor/common/languages.js';
import { FileType } from '../../../../../platform/files/common/files.js';
@@ -22,16 +21,18 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in
import { IAutostartResult } from '../../../mcp/common/mcpTypes.js';
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { IWorkspaceSymbol } from '../../../search/common/search.js';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from '../participants/chatAgents.js';
import { IChatEditingSession } from '../editing/chatEditingService.js';
import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from '../model/chatModel.js';
import { IParsedChatRequest } from '../requestParser/chatParserTypes.js';
import { IChatParserContext } from '../requestParser/chatRequestParser.js';
import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';
import { IChatRequestVariableValue } from '../attachments/chatVariables.js';
import { ChatAgentLocation } from '../constants.js';
import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from '../tools/languageModelToolsService.js';
import { ReadonlyChatSessionOptionsMap } from '../chatSessionsService.js';
import { ChatAgentLocation } from '../constants.js';
import { IChatEditingSession } from '../editing/chatEditingService.js';
import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from '../model/chatModel.js';
import type { IChatModelReferenceDebugSnapshot } from '../model/chatModelStore.js';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from '../participants/chatAgents.js';
import { HookTypeValue } from '../promptSyntax/hookTypes.js';
import { IParsedChatRequest } from '../requestParser/chatParserTypes.js';
import { IChatParserContext } from '../requestParser/chatRequestParser.js';
import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from '../tools/languageModelToolsService.js';
export interface IChatRequest {
message: string;
@@ -1440,7 +1441,7 @@ export interface IChatService {
*
* @returns A reference to the session's model or undefined if there is no active session for the given resource.
*/
acquireExistingSession(sessionResource: URI): IChatModelReference | undefined;
acquireExistingSession(sessionResource: URI, debugOwner?: string): IChatModelReference | undefined;
/**
* Tries to acquire an existing a chat session for the resource. If no session exists, tries to load one for the given
@@ -1448,12 +1449,14 @@ export interface IChatService {
*
* @returns A reference to the session's model, or undefined if the session could not be loaded
*/
acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModelReference | undefined>;
acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken, debugOwner?: string): Promise<IChatModelReference | undefined>;
/**
* Loads a session from exported chat data
*/
loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference;
loadSessionFromData(data: IExportableChatData | ISerializableChatData, debugOwner?: string): IChatModelReference;
getChatModelReferenceDebugInfo(): IChatModelReferenceDebugSnapshot;
getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined;
@@ -1543,6 +1546,7 @@ export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActiva
export interface IChatSessionStartOptions {
canUseTools?: boolean;
disableBackgroundKeepAlive?: boolean;
debugOwner?: string;
}
export const ChatStopCancellationNoopEventName = 'chat.stopCancellationNoop';
@@ -370,7 +370,7 @@ export class ChatService extends Disposable implements IChatService {
}
sessionResource ??= LocalChatSessionUri.forSession(session.sessionId);
const sessionRef = await this.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None);
const sessionRef = await this.acquireOrLoadSession(sessionResource, ChatAgentLocation.Chat, CancellationToken.None, 'ChatService#reviveSessionsWithEdits');
if (sessionRef?.object.editingSession) {
await chatEditingSessionIsReady(sessionRef.object.editingSession);
// the session will hold a self-reference as long as there are modified files
@@ -456,7 +456,7 @@ export class ChatService extends Disposable implements IChatService {
sessionResource,
canUseTools: options?.canUseTools ?? true,
disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive
});
}, options?.debugOwner ?? 'ChatService#startNewLocalSession');
}
private _startSession(props: IStartSessionProps): ChatModel {
@@ -508,13 +508,17 @@ export class ChatService extends Disposable implements IChatService {
return this._sessionModels.get(sessionResource);
}
acquireExistingSession(sessionResource: URI): IChatModelReference | undefined {
return this._sessionModels.acquireExisting(sessionResource);
acquireExistingSession(sessionResource: URI, debugOwner?: string): IChatModelReference | undefined {
return this._sessionModels.acquireExisting(sessionResource, debugOwner ?? 'ChatService#acquireExistingSession');
}
private async acquireOrRestoreLocalSession(sessionResource: URI): Promise<IChatModelReference | undefined> {
getChatModelReferenceDebugInfo() {
return this._sessionModels.getReferenceDebugSnapshot();
}
private async acquireOrRestoreLocalSession(sessionResource: URI, debugOwner?: string): Promise<IChatModelReference | undefined> {
this.trace('acquireOrRestoreSession', `${sessionResource}`);
const existingRef = this.acquireExistingSession(sessionResource);
const existingRef = this.acquireExistingSession(sessionResource, debugOwner);
if (existingRef) {
return existingRef;
}
@@ -539,7 +543,7 @@ export class ChatService extends Disposable implements IChatService {
location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat,
sessionResource,
canUseTools: true,
});
}, debugOwner ?? 'ChatService#acquireOrRestoreLocalSession');
return sessionRef;
}
@@ -556,7 +560,7 @@ export class ChatService extends Disposable implements IChatService {
this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title;
}
loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference {
loadSessionFromData(data: IExportableChatData | ISerializableChatData, debugOwner?: string): IChatModelReference {
const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid();
const sessionResource = LocalChatSessionUri.forSession(sessionId);
return this._sessionModels.acquireOrCreate({
@@ -564,22 +568,22 @@ export class ChatService extends Disposable implements IChatService {
location: data.initialLocation ?? ChatAgentLocation.Chat,
sessionResource,
canUseTools: true,
});
}, debugOwner ?? 'ChatService#loadSessionFromData');
}
async acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModelReference | undefined> {
async acquireOrLoadSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken, debugOwner?: string): Promise<IChatModelReference | undefined> {
if (sessionResource.scheme === Schemas.vscodeLocalChatSession) {
return this.acquireOrRestoreLocalSession(sessionResource);
return this.acquireOrRestoreLocalSession(sessionResource, debugOwner);
} else {
return this.loadRemoteSession(sessionResource, location, token);
return this.loadRemoteSession(sessionResource, location, token, debugOwner);
}
}
private async loadRemoteSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModelReference | undefined> {
private async loadRemoteSession(sessionResource: URI, location: ChatAgentLocation, token: CancellationToken, debugOwner?: string): Promise<IChatModelReference | undefined> {
// Check if session already exists before resolving the provider,
// so we can return a cached model even if the provider was unregistered.
{
const existingRef = this.acquireExistingSession(sessionResource);
const existingRef = this.acquireExistingSession(sessionResource, debugOwner);
if (existingRef) {
return existingRef;
}
@@ -593,7 +597,7 @@ export class ChatService extends Disposable implements IChatService {
// Make sure we haven't created this in the meantime
{
const existingRef = this.acquireExistingSession(sessionResource);
const existingRef = this.acquireExistingSession(sessionResource, debugOwner);
if (existingRef) {
providedSession.dispose();
return existingRef;
@@ -644,7 +648,7 @@ export class ChatService extends Disposable implements IChatService {
canUseTools: false,
transferEditingSession: providedSession.transferredState?.editingSession,
inputState: providedSession.transferredState?.inputState,
});
}, debugOwner ?? 'ChatService#loadRemoteSession');
// Restore permission level from metadata even when initialData was not constructed
if (storedPermissionLevel && !initialData) {
@@ -2300,7 +2300,7 @@ export class ChatModel extends Disposable implements IChatModel {
const needsInput = this.requestNeedsInput.read(r);
const shouldStayAlive = inProgress || !!needsInput;
if (shouldStayAlive && !selfRef.value) {
selfRef.value = chatService.acquireExistingSession(this._sessionResource);
selfRef.value = chatService.acquireExistingSession(this._sessionResource, 'ChatModel#requestInProgressKeepAlive');
} else if (!shouldStayAlive && selfRef.value) {
selfRef.clear();
}
@@ -2324,7 +2324,7 @@ export class ChatModel extends Disposable implements IChatModel {
this._register(autorun(r => {
const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified);
if (hasModified && !selfRef.value) {
selfRef.value = this.chatService.acquireExistingSession(this._sessionResource);
selfRef.value = this.chatService.acquireExistingSession(this._sessionResource, 'ChatModel#modifiedEditsKeepAlive');
} else if (!hasModified && selfRef.value) {
selfRef.clear();
}
@@ -9,7 +9,7 @@ import { ObservableMap } from '../../../../../base/common/observable.js';
import { URI } from '../../../../../base/common/uri.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { ChatAgentLocation } from '../constants.js';
import { IChatEditingSession } from '../editing/chatEditingService.js';
import { IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js';
import { ChatModel, ISerializableChatModelInputState, ISerializedChatDataReference } from './chatModel.js';
export interface IStartSessionProps {
@@ -27,12 +27,39 @@ export interface ChatModelStoreDelegate {
willDisposeModel: (model: ChatModel) => Promise<void>;
}
export interface IChatModelReferenceDebugHolder {
readonly holder: string;
readonly count: number;
}
export interface IChatModelReferenceDebugInfo {
readonly sessionResource: URI;
readonly title: string;
readonly createdBy: string;
readonly initialLocation: ChatAgentLocation;
readonly isImported: boolean;
readonly willKeepAlive: boolean;
readonly hasPendingEdits: boolean;
readonly pendingDisposal: boolean;
readonly referenceCount: number;
readonly holders: readonly IChatModelReferenceDebugHolder[];
}
export interface IChatModelReferenceDebugSnapshot {
readonly totalModels: number;
readonly totalReferences: number;
readonly models: readonly IChatModelReferenceDebugInfo[];
}
export class ChatModelStore extends Disposable {
private readonly _refCollection: ReferenceCollection<ChatModel>;
private readonly _models = new ObservableMap<string, ChatModel>();
private readonly _modelsToDispose = new Set<string>();
private readonly _pendingDisposals = new Set<Promise<void>>();
private readonly _modelCreateOwners = new Map<string, string>();
private readonly _referenceOwners = new Map<string, Map<number, string>>();
private _referenceOwnerIds = 0;
private readonly _onDidDisposeModel = this._register(new Emitter<ChatModel>());
public readonly onDidDisposeModel = this._onDidDisposeModel.event;
@@ -48,8 +75,8 @@ export class ChatModelStore extends Disposable {
const self = this;
this._refCollection = new class extends ReferenceCollection<ChatModel> {
protected createReferencedObject(key: string, props?: IStartSessionProps): ChatModel {
return self.createReferencedObject(key, props);
protected createReferencedObject(key: string, props?: IStartSessionProps, debugOwner?: string): ChatModel {
return self.createReferencedObject(key, props, debugOwner);
}
protected destroyReferencedObject(key: string, object: ChatModel): void {
return self.destroyReferencedObject(key, object);
@@ -76,19 +103,57 @@ export class ChatModelStore extends Disposable {
return this._models.has(this.toKey(uri));
}
public acquireExisting(uri: URI): IReference<ChatModel> | undefined {
public acquireExisting(uri: URI, debugOwner?: string): IReference<ChatModel> | undefined {
const key = this.toKey(uri);
if (!this._models.has(key)) {
return undefined;
}
return this._refCollection.acquire(key);
return this.wrapReference(key, this._refCollection.acquire(key, undefined, debugOwner), debugOwner);
}
public acquireOrCreate(props: IStartSessionProps): IReference<ChatModel> {
return this._refCollection.acquire(this.toKey(props.sessionResource), props);
public acquireOrCreate(props: IStartSessionProps, debugOwner?: string): IReference<ChatModel> {
const key = this.toKey(props.sessionResource);
return this.wrapReference(key, this._refCollection.acquire(key, props, debugOwner), debugOwner);
}
private createReferencedObject(key: string, props?: IStartSessionProps): ChatModel {
public getReferenceDebugSnapshot(): IChatModelReferenceDebugSnapshot {
const models = Array.from(this._models.values())
.map(model => {
const key = this.toKey(model.sessionResource);
const owners = this._referenceOwners.get(key) ?? new Map();
const countsByOwner = new Map<string, number>();
for (const owner of owners.values()) {
countsByOwner.set(owner, (countsByOwner.get(owner) ?? 0) + 1);
}
const holders = Array.from(countsByOwner.entries())
.map(([holder, count]) => ({ holder, count }))
.sort((a, b) => b.count - a.count || a.holder.localeCompare(b.holder));
return {
sessionResource: model.sessionResource,
title: model.title,
createdBy: this._modelCreateOwners.get(key) ?? 'unknown',
initialLocation: model.initialLocation,
isImported: !!model.isImported,
willKeepAlive: model.willKeepAlive,
hasPendingEdits: !!model.editingSession?.entries.get().some(entry => entry.state.get() === ModifiedFileEntryState.Modified),
pendingDisposal: this._modelsToDispose.has(key),
referenceCount: owners.size,
holders,
} satisfies IChatModelReferenceDebugInfo;
})
.sort((a, b) => b.referenceCount - a.referenceCount || Number(b.hasPendingEdits) - Number(a.hasPendingEdits) || a.sessionResource.toString().localeCompare(b.sessionResource.toString()));
return {
totalModels: models.length,
totalReferences: models.reduce((total, model) => total + model.referenceCount, 0),
models,
};
}
private createReferencedObject(key: string, props?: IStartSessionProps, debugOwner?: string): ChatModel {
this._modelsToDispose.delete(key);
const existingModel = this._models.get(key);
if (existingModel) {
@@ -101,6 +166,7 @@ export class ChatModelStore extends Disposable {
this.logService.trace(`Creating chat session ${key}`);
const model = this.delegate.createModel(props);
this._modelCreateOwners.set(key, debugOwner ?? 'unspecified');
if (model.sessionResource.toString() !== key) {
throw new Error(`Chat session key mismatch for ${key}`);
}
@@ -127,6 +193,8 @@ export class ChatModelStore extends Disposable {
if (this._modelsToDispose.has(key)) {
this.logService.trace(`Disposing chat session ${key}`);
this._models.delete(key);
this._modelCreateOwners.delete(key);
this._referenceOwners.delete(key);
this._onDidDisposeModel.fire(object);
object.dispose();
}
@@ -134,6 +202,34 @@ export class ChatModelStore extends Disposable {
}
}
private wrapReference(key: string, reference: IReference<ChatModel>, debugOwner?: string): IReference<ChatModel> {
const ownerId = ++this._referenceOwnerIds;
let ownerEntries = this._referenceOwners.get(key);
if (!ownerEntries) {
ownerEntries = new Map();
this._referenceOwners.set(key, ownerEntries);
}
ownerEntries.set(ownerId, debugOwner ?? 'unspecified');
let isDisposed = false;
return {
object: reference.object,
dispose: () => {
if (isDisposed) {
return;
}
isDisposed = true;
const owners = this._referenceOwners.get(key);
owners?.delete(ownerId);
if (owners?.size === 0) {
this._referenceOwners.delete(key);
}
reference.dispose();
}
};
}
/**
* For test use only
*/
@@ -18,11 +18,11 @@ import { isMacintosh, isWindows } from '../../../../../base/common/platform.js';
import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js';
import { Schemas } from '../../../../../base/common/network.js';
export class OpenSessionsWindowAction extends Action2 {
export class OpenAgentsWindowAction extends Action2 {
constructor() {
super({
id: 'workbench.action.openSessionsWindow',
title: localize2('openSessionsWindow', "Open Sessions Window"),
id: 'workbench.action.openAgentsWindow',
title: localize2('openAgentsWindow', "Open Agents Application"),
category: CHAT_CATEGORY,
precondition: ContextKeyExpr.and(ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate(), IsSessionsWindowContext.negate()),
f1: true,
@@ -50,7 +50,7 @@ export class OpenSessionsWindowAction extends Action2 {
await openerService.open(URI.from({ scheme, authority: Schemas.file }), { openExternal: true });
} else {
const nativeHostService = accessor.get(INativeHostService);
await nativeHostService.openSessionsWindow();
await nativeHostService.openAgentsWindow();
}
}
}
@@ -42,7 +42,7 @@ import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'
import { registerChatExportZipAction } from './actions/chatExportZip.js';
import { HoldToVoiceChatInChatViewAction, InlineVoiceChatAction, KeywordActivationContribution, QuickVoiceChatAction, ReadChatResponseAloud, StartVoiceChatAction, StopListeningAction, StopListeningAndSubmitAction, StopReadAloud, StopReadChatItemAloud, VoiceChatInChatViewAction } from './actions/voiceChatActions.js';
import { NativeBuiltinToolsContribution } from './builtInTools/tools.js';
import { OpenSessionsWindowAction } from './agentSessions/agentSessionsActions.js';
import { OpenAgentsWindowAction } from './agentSessions/agentSessionsActions.js';
class ChatCommandLineHandler extends Disposable {
@@ -217,7 +217,7 @@ class ChatLifecycleHandler extends Disposable {
}
}
registerAction2(OpenSessionsWindowAction);
registerAction2(OpenAgentsWindowAction);
registerAction2(StartVoiceChatAction);
registerAction2(VoiceChatInChatViewAction);
@@ -411,6 +411,55 @@ suite('ChatSubagentContentPart', () => {
assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed after markAsInactive');
});
test('markAsInactive should change default description to past tense', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
// no description — should use the default "Running subagent"
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
// Before marking inactive, title should show "Running subagent"
const button = getCollapseButton(part);
assert.ok(button, 'Should have collapse button');
const labelBefore = getCollapseButtonLabel(button);
const textBefore = labelBefore?.textContent ?? button.textContent ?? '';
assert.ok(textBefore.includes('Running subagent'), 'Title should show "Running subagent" before completion');
part.markAsInactive();
// After marking inactive, title should show "Ran subagent"
const labelAfter = getCollapseButtonLabel(button);
const textAfter = labelAfter?.textContent ?? button.textContent ?? '';
assert.ok(textAfter.includes('Ran subagent'), 'Title should show "Ran subagent" after completion');
assert.ok(!textAfter.includes('Running subagent'), 'Title should no longer show "Running subagent"');
});
test('markAsInactive should keep custom description unchanged', () => {
const toolInvocation = createMockToolInvocation({
toolSpecificData: {
kind: 'subagent',
description: 'Searching the codebase',
agentName: 'Explorer',
}
});
const context = createMockRenderContext(false);
const part = createPart(toolInvocation, context);
part.markAsInactive();
// After marking inactive, title should still show the custom description
const button = getCollapseButton(part);
assert.ok(button, 'Should have collapse button');
const label = getCollapseButtonLabel(button);
const text = label?.textContent ?? button.textContent ?? '';
assert.ok(text.includes('Searching the codebase'), 'Title should keep custom description after completion');
});
test('finalizeTitle should update button icon to check', () => {
// Enable the showCheckmarks setting so the check icon is visible
const configService = instantiationService.get(IConfigurationService) as TestConfigurationService;
@@ -9,7 +9,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js
import { Event } from '../../../../../../base/common/event.js';
import { MarkdownString } from '../../../../../../base/common/htmlContent.js';
import { DisposableStore } from '../../../../../../base/common/lifecycle.js';
import { constObservable, observableValue } from '../../../../../../base/common/observable.js';
import { constObservable, ISettableObservable, observableValue } from '../../../../../../base/common/observable.js';
import { URI } from '../../../../../../base/common/uri.js';
import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
@@ -45,7 +45,7 @@ import { ChatDebugServiceImpl } from '../../../common/chatDebugServiceImpl.js';
import { ChatRequestQueueKind, ChatSendResult, IChatFollowup, IChatModelReference, IChatService, ResponseModelState } from '../../../common/chatService/chatService.js';
import { ChatService } from '../../../common/chatService/chatServiceImpl.js';
import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js';
import { IChatEditingService, IChatEditingSession } from '../../../common/editing/chatEditingService.js';
import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js';
import { ChatModel, IChatModel, ISerializableChatData } from '../../../common/model/chatModel.js';
import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../../common/participants/chatAgents.js';
import { ChatSlashCommandService, IChatSlashCommandService } from '../../../common/participants/chatSlashCommands.js';
@@ -138,6 +138,7 @@ suite('ChatService', () => {
let instantiationService: TestInstantiationService;
let testFileService: InMemoryTestFileService;
let editingSessionEntries: ISettableObservable<readonly IModifiedFileEntry[]>;
let chatAgentService: IChatAgentService;
const testServices: ChatService[] = [];
@@ -188,12 +189,13 @@ suite('ChatService', () => {
instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None });
instantiationService.stub(IWorkspaceEditingService, { onDidEnterWorkspace: Event.None });
instantiationService.stub(IChatDebugService, testDisposables.add(new ChatDebugServiceImpl()));
editingSessionEntries = observableValue('editingSessionEntries', []);
instantiationService.stub(IChatEditingService, new class extends mock<IChatEditingService>() {
override startOrContinueGlobalEditingSession(): IChatEditingSession {
return {
state: constObservable('idle'),
state: constObservable(ChatEditingSessionState.Idle),
requestDisablement: observableValue('requestDisablement', []),
entries: constObservable([]),
entries: editingSessionEntries,
dispose: () => { }
} as unknown as IChatEditingSession;
}
@@ -267,6 +269,46 @@ suite('ChatService', () => {
assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2');
});
test('reports modified edit keep-alive holders', () => {
const testService = createChatService();
instantiationService.stub(IChatService, testService);
const rootRef = testService.startNewLocalSession(ChatAgentLocation.Chat, { debugOwner: 'ChatServiceTest#root' });
const modifiedEntry = new class extends mock<IModifiedFileEntry>() {
override state = constObservable(ModifiedFileEntryState.Modified);
}();
editingSessionEntries.set([modifiedEntry], undefined);
assert.deepStrictEqual(testService.getChatModelReferenceDebugInfo().models.map(model => ({
createdBy: model.createdBy,
holders: model.holders,
hasPendingEdits: model.hasPendingEdits,
referenceCount: model.referenceCount,
})), [{
createdBy: 'ChatServiceTest#root',
holders: [
{ holder: 'ChatModel#modifiedEditsKeepAlive', count: 1 },
{ holder: 'ChatServiceTest#root', count: 1 }
],
hasPendingEdits: true,
referenceCount: 2,
}]);
editingSessionEntries.set([], undefined);
assert.deepStrictEqual(testService.getChatModelReferenceDebugInfo().models.map(model => ({
holders: model.holders,
hasPendingEdits: model.hasPendingEdits,
referenceCount: model.referenceCount,
})), [{
holders: [{ holder: 'ChatServiceTest#root', count: 1 }],
hasPendingEdits: false,
referenceCount: 1,
}]);
rootRef.dispose();
});
test('addCompleteRequest', async () => {
const testService = createChatService();
@@ -11,6 +11,7 @@ import { URI } from '../../../../../../base/common/uri.js';
import { ChatRequestQueueKind, ChatSendResult, IChatDetail, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js';
import { ChatAgentLocation } from '../../../common/constants.js';
import { IChatModel, IChatRequestModel, IExportableChatData, ISerializableChatData } from '../../../common/model/chatModel.js';
import type { IChatModelReferenceDebugSnapshot } from '../../../common/model/chatModelStore.js';
export class MockChatService implements IChatService {
private readonly _chatModels: ISettableObservable<Iterable<IChatModel>> = observableValue('chatModels', []);
@@ -96,15 +97,19 @@ export class MockChatService implements IChatService {
return undefined;
}
loadSessionFromData(data: IExportableChatData | ISerializableChatData): IChatModelReference {
loadSessionFromData(data: IExportableChatData | ISerializableChatData, _debugOwner?: string): IChatModelReference {
throw new Error('Method not implemented.');
}
acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise<IChatModelReference | undefined> {
getChatModelReferenceDebugInfo(): IChatModelReferenceDebugSnapshot {
return { totalModels: 0, totalReferences: 0, models: [] };
}
acquireOrLoadSession(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken, _debugOwner?: string): Promise<IChatModelReference | undefined> {
throw new Error('Method not implemented.');
}
acquireExistingSession(_sessionResource: URI): IChatModelReference | undefined {
acquireExistingSession(_sessionResource: URI, _debugOwner?: string): IChatModelReference | undefined {
return undefined;
}
@@ -172,4 +172,115 @@ suite('ChatModelStore', () => {
assert.strictEqual(model.isDisposed, true);
});
test('tracks reference owners and creation owner', async () => {
const uri = URI.parse('test://session');
const props: IStartSessionProps = {
sessionResource: uri,
location: ChatAgentLocation.Chat,
canUseTools: true
};
const ref1 = testObject.acquireOrCreate(props, 'ChatModelStoreTest#create');
const ref2 = testObject.acquireExisting(uri, 'ChatModelStoreTest#existing');
const ref3 = testObject.acquireExisting(uri, 'ChatModelStoreTest#existing');
assert.deepStrictEqual(testObject.getReferenceDebugSnapshot(), {
totalModels: 1,
totalReferences: 3,
models: [{
sessionResource: uri,
title: '',
createdBy: 'ChatModelStoreTest#create',
initialLocation: ChatAgentLocation.Chat,
isImported: false,
willKeepAlive: true,
hasPendingEdits: false,
pendingDisposal: false,
referenceCount: 3,
holders: [
{ holder: 'ChatModelStoreTest#existing', count: 2 },
{ holder: 'ChatModelStoreTest#create', count: 1 }
]
}]
});
ref1.dispose();
ref2?.dispose();
ref3?.dispose();
willDisposePromises[0].complete();
await testObject.waitForModelDisposals();
});
test('reports pending disposal models without holders', async () => {
const uri = URI.parse('test://session');
const props: IStartSessionProps = {
sessionResource: uri,
location: ChatAgentLocation.Chat,
canUseTools: true
};
const ref = testObject.acquireOrCreate(props, 'ChatModelStoreTest#create');
ref.dispose();
assert.deepStrictEqual(testObject.getReferenceDebugSnapshot(), {
totalModels: 1,
totalReferences: 0,
models: [{
sessionResource: uri,
title: '',
createdBy: 'ChatModelStoreTest#create',
initialLocation: ChatAgentLocation.Chat,
isImported: false,
willKeepAlive: true,
hasPendingEdits: false,
pendingDisposal: true,
referenceCount: 0,
holders: []
}]
});
willDisposePromises[0].complete();
await testObject.waitForModelDisposals();
});
test('resurrection preserves debug tracking', async () => {
const uri = URI.parse('test://session');
const props: IStartSessionProps = {
sessionResource: uri,
location: ChatAgentLocation.Chat,
canUseTools: true
};
const ref1 = testObject.acquireOrCreate(props, 'OriginalCreator');
ref1.dispose();
// Model is pending disposal — re-acquire before disposal completes
const ref2 = testObject.acquireOrCreate(props, 'Rescuer');
// Complete the old disposal — should NOT wipe the model or tracking
willDisposePromises[0].complete();
await testObject.waitForModelDisposals();
assert.deepStrictEqual(testObject.getReferenceDebugSnapshot(), {
totalModels: 1,
totalReferences: 1,
models: [{
sessionResource: uri,
title: '',
createdBy: 'OriginalCreator',
initialLocation: ChatAgentLocation.Chat,
isImported: false,
willKeepAlive: true,
hasPendingEdits: false,
pendingDisposal: false,
referenceCount: 1,
holders: [{ holder: 'Rescuer', count: 1 }]
}]
});
ref2.dispose();
willDisposePromises[1].complete();
await testObject.waitForModelDisposals();
});
});
@@ -273,8 +273,10 @@ export class NotebookMultiCursorController extends Disposable implements INotebo
const textModelRef = await this.textModelService.createModelReference(cell.cellViewModel.uri);
const textModel = textModelRef.object.textEditorModel;
if (!textModel) {
textModelRef.dispose();
return undefined;
}
this.cursorsDisposables.add(textModelRef);
const cursorSimpleModel = this.constructCursorSimpleModel(cell.cellViewModel);
const converter = this.constructCoordinatesConverter();
@@ -85,6 +85,23 @@ export function normalizeTerminalCommandForDisplay(commandLine: string): string
return commandLine.replace(/\\(["'\/])/g, '$1');
}
/**
* Builds a single-line display string for a terminal command, suitable for UI messages.
* Normalizes escape artifacts, collapses newlines to spaces, and truncates to 80 characters.
*/
export function buildCommandDisplayText(command: string): string {
const normalized = normalizeTerminalCommandForDisplay(command).replace(/\r\n|\r|\n/g, ' ');
return normalized.length > 80 ? normalized.substring(0, 77) + '...' : normalized;
}
/**
* Normalizes a terminal command for execution by collapsing newlines to spaces.
* This prevents multi-line input from being sent as multiple commands via sendText.
*/
export function normalizeCommandForExecution(command: string): string {
return command.replace(/\r\n|\r|\n/g, ' ').trim();
}
export function generateAutoApproveActions(commandLine: string, subCommands: string[], autoApproveResult: { subCommandResults: ICommandApprovalResultWithReason[]; commandLineResult: ICommandApprovalResultWithReason }): ToolConfirmationAction[] {
const actions: ToolConfirmationAction[] = [];
@@ -24,6 +24,7 @@ import { AwaitTerminalTool, AwaitTerminalToolData } from './tools/awaitTerminalT
import { GetTerminalLastCommandTool, GetTerminalLastCommandToolData } from './tools/getTerminalLastCommandTool.js';
import { KillTerminalTool, KillTerminalToolData } from './tools/killTerminalTool.js';
import { GetTerminalOutputTool, GetTerminalOutputToolData } from './tools/getTerminalOutputTool.js';
import { SendToTerminalTool, SendToTerminalToolData } from './tools/sendToTerminalTool.js';
import { GetTerminalSelectionTool, GetTerminalSelectionToolData } from './tools/getTerminalSelectionTool.js';
import { ConfirmTerminalCommandTool, ConfirmTerminalCommandToolData } from './tools/runInTerminalConfirmationTool.js';
import { RunInTerminalTool, createRunInTerminalToolData } from './tools/runInTerminalTool.js';
@@ -105,6 +106,10 @@ export class ChatAgentToolsContribution extends Disposable implements IWorkbench
this._register(_toolsService.registerTool(KillTerminalToolData, killTerminalTool));
this._register(_toolsService.executeToolSet.addTool(KillTerminalToolData));
const sendToTerminalTool = _instantiationService.createInstance(SendToTerminalTool);
this._register(_toolsService.registerTool(SendToTerminalToolData, sendToTerminalTool));
this._register(_toolsService.executeToolSet.addTool(SendToTerminalToolData));
this._registerRunInTerminalTool();
const getTerminalSelectionTool = _instantiationService.createInstance(GetTerminalSelectionTool);
@@ -38,7 +38,7 @@ import type { ITerminalExecuteStrategy, ITerminalExecuteStrategyResult } from '.
import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js';
import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js';
import { getOutput } from '../outputHelpers.js';
import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh, normalizeTerminalCommandForDisplay } from '../runInTerminalHelpers.js';
import { buildCommandDisplayText, extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh, normalizeTerminalCommandForDisplay } from '../runInTerminalHelpers.js';
import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js';
import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js';
import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js';
@@ -108,6 +108,7 @@ function createPowerShellModelDescription(shell: string, isSandboxEnabled: boole
'Background Processes:',
'- For long-running tasks (e.g., servers), set isBackground=true',
'- Returns a terminal ID for checking status and runtime later',
`- Use ${TerminalToolId.SendToTerminal} to send commands to a background terminal`,
'- Use Start-Job for background PowerShell jobs',
];
@@ -184,7 +185,8 @@ Program Execution:
Background Processes:
- For long-running tasks (e.g., servers), set isBackground=true
- Returns a terminal ID for checking status and runtime later`];
- Returns a terminal ID for checking status and runtime later
- Use ${TerminalToolId.SendToTerminal} to send commands to a background terminal`];
if (isSandboxEnabled) {
parts.push(createSandboxLines(networkDomains).join('\n'));
@@ -528,10 +530,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
async handleToolStream(context: IToolInvocationStreamContext, _token: CancellationToken): Promise<IStreamedToolInvocation | undefined> {
const partialInput = context.rawInput as Partial<IRunInTerminalInputParams> | undefined;
if (partialInput && typeof partialInput === 'object' && partialInput.command) {
const normalizedCommand = normalizeTerminalCommandForDisplay(partialInput.command).replace(/\r\n|\r|\n/g, ' ');
const truncatedCommand = normalizedCommand.length > 80
? normalizedCommand.substring(0, 77) + '...'
: normalizedCommand;
const truncatedCommand = buildCommandDisplayText(partialInput.command);
const invocationMessage = partialInput.isBackground
? new MarkdownString(localize('runInTerminal.streaming.background', "Running `{0}` in background", truncatedCommand))
: new MarkdownString(localize('runInTerminal.streaming', "Running `{0}`", truncatedCommand));
@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { MarkdownString } from '../../../../../../base/common/htmlContent.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { localize } from '../../../../../../nls.js';
import { ToolDataSource, type CountTokensCallback, type IPreparedToolInvocation, type IToolData, type IToolImpl, type IToolInvocation, type IToolInvocationPreparationContext, type IToolResult, type ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js';
import { buildCommandDisplayText, normalizeCommandForExecution } from '../runInTerminalHelpers.js';
import { RunInTerminalTool } from './runInTerminalTool.js';
import { TerminalToolId } from './toolIds.js';
export const SendToTerminalToolData: IToolData = {
id: TerminalToolId.SendToTerminal,
toolReferenceName: 'sendToTerminal',
displayName: localize('sendToTerminalTool.displayName', 'Send to Terminal'),
modelDescription: `Send a command to an existing background terminal that was started with ${TerminalToolId.RunInTerminal}. Use this to send commands to long-running terminal sessions. The ID must be the exact opaque value returned by ${TerminalToolId.RunInTerminal}. After sending, use ${TerminalToolId.GetTerminalOutput} to check for updated output.`,
icon: Codicon.terminal,
source: ToolDataSource.Internal,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: `The ID of the background terminal to send a command to (returned by ${TerminalToolId.RunInTerminal}).`,
pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
},
command: {
type: 'string',
description: 'The command to send to the terminal. The text will be sent followed by Enter to execute it.'
},
},
required: [
'id',
'command',
]
}
};
export interface ISendToTerminalInputParams {
id: string;
command: string;
}
export class SendToTerminalTool extends Disposable implements IToolImpl {
async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
const args = context.parameters as ISendToTerminalInputParams;
const displayCommand = buildCommandDisplayText(args.command);
const invocationMessage = new MarkdownString();
invocationMessage.appendText(localize('send.progressive', "Sending {0} to terminal", displayCommand));
const pastTenseMessage = new MarkdownString();
pastTenseMessage.appendText(localize('send.past', "Sent {0} to terminal", displayCommand));
const confirmationMessage = new MarkdownString();
confirmationMessage.appendText(localize('send.confirm.message', "Run {0} in background terminal {1}", displayCommand, args.id));
return {
invocationMessage,
pastTenseMessage,
confirmationMessages: {
title: localize('send.confirm.title', "Send to Terminal"),
message: confirmationMessage,
},
};
}
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise<IToolResult> {
const args = invocation.parameters as ISendToTerminalInputParams;
const execution = RunInTerminalTool.getExecution(args.id);
if (!execution) {
return {
content: [{
kind: 'text',
value: `Error: No active terminal execution found with ID ${args.id}. The terminal may have already been killed or the ID is invalid. The ID must be the exact value returned by ${TerminalToolId.RunInTerminal}.`
}]
};
}
await execution.instance.sendText(normalizeCommandForExecution(args.command), true);
return {
content: [{
kind: 'text',
value: `Successfully sent command to terminal ${args.id}. Use ${TerminalToolId.GetTerminalOutput} to check for updated output.`
}]
};
}
}
@@ -5,6 +5,7 @@
export const enum TerminalToolId {
RunInTerminal = 'run_in_terminal',
SendToTerminal = 'send_to_terminal',
AwaitTerminal = 'await_terminal',
GetTerminalOutput = 'get_terminal_output',
KillTerminal = 'kill_terminal',
@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { SendToTerminalTool, SendToTerminalToolData } from '../../browser/tools/sendToTerminalTool.js';
import { RunInTerminalTool, type IActiveTerminalExecution } from '../../browser/tools/runInTerminalTool.js';
import type { IToolInvocation, IToolInvocationPreparationContext } from '../../../../chat/common/tools/languageModelToolsService.js';
import type { ITerminalExecuteStrategyResult } from '../../browser/executeStrategy/executeStrategy.js';
import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
suite('SendToTerminalTool', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
const UNKNOWN_TERMINAL_ID = '123e4567-e89b-12d3-a456-426614174000';
const KNOWN_TERMINAL_ID = '123e4567-e89b-12d3-a456-426614174001';
let tool: SendToTerminalTool;
let originalGetExecution: typeof RunInTerminalTool.getExecution;
setup(() => {
tool = store.add(new SendToTerminalTool());
originalGetExecution = RunInTerminalTool.getExecution;
});
teardown(() => {
RunInTerminalTool.getExecution = originalGetExecution;
});
function createInvocation(id: string, command: string): IToolInvocation {
return {
parameters: { id, command },
callId: 'test-call',
context: { sessionId: 'test-session' },
toolId: 'send_to_terminal',
tokenBudget: 1000,
isComplete: () => false,
isCancellationRequested: false,
} as unknown as IToolInvocation;
}
function createMockExecution(output: string): IActiveTerminalExecution & { sentTexts: { text: string; shouldExecute: boolean }[] } {
const sentTexts: { text: string; shouldExecute: boolean }[] = [];
return {
completionPromise: Promise.resolve({ output } as ITerminalExecuteStrategyResult),
instance: {
sendText: async (text: string, shouldExecute: boolean) => {
sentTexts.push({ text, shouldExecute });
},
} as unknown as ITerminalInstance,
getOutput: () => output,
sentTexts,
};
}
test('tool description documents terminal IDs and use cases', () => {
const idProperty = SendToTerminalToolData.inputSchema?.properties?.id as { description?: string; pattern?: string } | undefined;
assert.ok(SendToTerminalToolData.modelDescription.includes('existing background terminal'));
assert.ok(idProperty?.pattern?.includes('[0-9a-fA-F]{8}'));
});
test('returns error for unknown terminal id', async () => {
RunInTerminalTool.getExecution = () => undefined;
const result = await tool.invoke(
createInvocation(UNKNOWN_TERMINAL_ID, 'ls'),
async () => 0,
{ report: () => { } },
CancellationToken.None,
);
assert.strictEqual(result.content.length, 1);
assert.strictEqual(result.content[0].kind, 'text');
const value = (result.content[0] as { value: string }).value;
assert.ok(value.includes('No active terminal execution found'));
assert.ok(value.includes(UNKNOWN_TERMINAL_ID));
});
test('sends command to terminal and returns acknowledgment', async () => {
const mockExecution = createMockExecution('$ ls\nfile1.txt\nfile2.txt');
RunInTerminalTool.getExecution = () => mockExecution;
const result = await tool.invoke(
createInvocation(KNOWN_TERMINAL_ID, 'ls'),
async () => 0,
{ report: () => { } },
CancellationToken.None,
);
assert.strictEqual(result.content.length, 1);
assert.strictEqual(result.content[0].kind, 'text');
const value = (result.content[0] as { value: string }).value;
assert.ok(value.includes('Successfully sent command'));
assert.ok(value.includes(KNOWN_TERMINAL_ID));
assert.ok(value.includes('get_terminal_output'), 'should direct agent to use get_terminal_output');
// Verify sendText was called with shouldExecute=true
assert.strictEqual(mockExecution.sentTexts.length, 1);
assert.strictEqual(mockExecution.sentTexts[0].text, 'ls');
assert.strictEqual(mockExecution.sentTexts[0].shouldExecute, true);
});
test('sends multi-word command correctly', async () => {
const mockExecution = createMockExecution('output');
RunInTerminalTool.getExecution = () => mockExecution;
await tool.invoke(
createInvocation(KNOWN_TERMINAL_ID, 'echo hello world'),
async () => 0,
{ report: () => { } },
CancellationToken.None,
);
assert.strictEqual(mockExecution.sentTexts.length, 1);
assert.strictEqual(mockExecution.sentTexts[0].text, 'echo hello world');
assert.strictEqual(mockExecution.sentTexts[0].shouldExecute, true);
});
function createPreparationContext(id: string, command: string): IToolInvocationPreparationContext {
return {
parameters: { id, command },
toolCallId: 'test-call',
} as unknown as IToolInvocationPreparationContext;
}
test('prepareToolInvocation shows command in messages', async () => {
const prepared = await tool.prepareToolInvocation(
createPreparationContext(KNOWN_TERMINAL_ID, 'ls -la'),
CancellationToken.None,
);
assert.ok(prepared);
assert.ok(prepared.invocationMessage);
assert.ok(prepared.pastTenseMessage);
assert.ok(prepared.confirmationMessages);
assert.ok(prepared.confirmationMessages.title);
assert.ok(prepared.confirmationMessages.message);
});
test('prepareToolInvocation truncates long commands', async () => {
const longCommand = 'a'.repeat(100);
const prepared = await tool.prepareToolInvocation(
createPreparationContext(KNOWN_TERMINAL_ID, longCommand),
CancellationToken.None,
);
assert.ok(prepared);
const message = prepared.invocationMessage as IMarkdownString;
assert.ok(message.value.includes('...'));
});
test('prepareToolInvocation normalizes newlines in command', async () => {
const prepared = await tool.prepareToolInvocation(
createPreparationContext(KNOWN_TERMINAL_ID, 'echo hello\necho world'),
CancellationToken.None,
);
assert.ok(prepared);
const message = prepared.invocationMessage as IMarkdownString;
assert.ok(!message.value.includes('\n'), 'newlines should be collapsed to spaces');
});
});
@@ -202,6 +202,7 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
interface IAccountPolicyData {
readonly accountId: string;
readonly policyData: IPolicyData;
readonly entitlementsFetchedAt?: number;
readonly tokenEntitlementsFetchedAt?: number;
readonly mcpRegistryDataFetchedAt?: number;
}
@@ -379,7 +380,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
this._register(this.hostService.onDidChangeFocus(focused => {
if (focused) {
this.refetchDefaultAccount(true);
this.refetchDefaultAccount();
}
}));
}
@@ -396,7 +397,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return this.defaultAccount;
}
private async refetchDefaultAccount(useExistingEntitlements?: boolean): Promise<void> {
private async refetchDefaultAccount(): Promise<void> {
if (this.accountDataPollScheduler.isScheduled()) {
this.accountDataPollScheduler.cancel();
}
@@ -406,16 +407,16 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return;
}
this.logService.debug('[DefaultAccount] Refetching default account');
await this.updateDefaultAccount(useExistingEntitlements);
await this.updateDefaultAccount();
}
private async updateDefaultAccount(useExistingEntitlements?: boolean): Promise<void> {
await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount(useExistingEntitlements));
private async updateDefaultAccount(): Promise<void> {
await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount());
}
private async doUpdateDefaultAccount(useExistingEntitlements: boolean = false): Promise<void> {
private async doUpdateDefaultAccount(): Promise<void> {
try {
const defaultAccount = await this.fetchDefaultAccount(useExistingEntitlements);
const defaultAccount = await this.fetchDefaultAccount();
this.setDefaultAccount(defaultAccount);
this.scheduleAccountDataPoll();
} catch (error) {
@@ -423,7 +424,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
}
}
private async fetchDefaultAccount(useExistingEntitlements: boolean): Promise<IDefaultAccountData | null> {
private async fetchDefaultAccount(): Promise<IDefaultAccountData | null> {
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id);
@@ -433,7 +434,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return null;
}
return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider, useExistingEntitlements);
return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider);
}
private setDefaultAccount(account: IDefaultAccountData | null): void {
@@ -510,7 +511,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return result;
}
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider, useExistingEntitlements: boolean): Promise<IDefaultAccountData | null> {
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise<IDefaultAccountData | null> {
try {
this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id);
const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes);
@@ -520,29 +521,27 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return null;
}
return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions, useExistingEntitlements);
return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions);
} catch (error) {
this.logService.error('[DefaultAccount] Failed to get default account for provider:', authenticationProvider.id, getErrorMessage(error));
return null;
}
}
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[], useExistingEntitlements: boolean): Promise<IDefaultAccountData | null> {
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise<IDefaultAccountData | null> {
try {
const accountId = sessions[0].account.id;
const existingEntitlementsData = this._defaultAccount?.accountId === accountId ? this._defaultAccount?.defaultAccount.entitlementsData : undefined;
const accountPolicyData = this._policyData?.accountId === accountId ? this._policyData : undefined;
const [entitlementsData, tokenEntitlementsResult] = await Promise.all([
useExistingEntitlements && existingEntitlementsData ? existingEntitlementsData : this.getEntitlements(sessions),
this.getTokenEntitlements(sessions, accountPolicyData),
]);
const entitlementsResult = await this.getEntitlements(sessions, accountPolicyData);
const entitlementsData = entitlementsResult?.data;
const entitlementsFetchedAt = entitlementsResult?.fetchedAt;
const tokenEntitlementsResult = entitlementsData?.chat_enabled ? await this.getTokenEntitlements(sessions, accountPolicyData) : undefined;
let tokenEntitlementsFetchedAt: number | undefined;
const tokenEntitlementsFetchedAt: number | undefined = tokenEntitlementsResult?.fetchedAt;
let mcpRegistryDataFetchedAt: number | undefined;
let policyData: Mutable<IPolicyData> | undefined = accountPolicyData?.policyData ? { ...accountPolicyData.policyData } : undefined;
if (tokenEntitlementsResult) {
tokenEntitlementsFetchedAt = tokenEntitlementsResult.fetchedAt;
if (tokenEntitlementsResult?.data) {
const tokenEntitlementsData = tokenEntitlementsResult.data;
policyData = policyData ?? {};
policyData.chat_agent_enabled = tokenEntitlementsData.policyData.chat_agent_enabled;
@@ -567,11 +566,14 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
entitlementsData,
};
this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id);
const accountPolicyResult: IAccountPolicyData | null = policyData || entitlementsFetchedAt
? { accountId, policyData: policyData ?? {}, entitlementsFetchedAt, tokenEntitlementsFetchedAt, mcpRegistryDataFetchedAt }
: null;
return {
defaultAccount,
accountId,
policyData: policyData ? { accountId, policyData, tokenEntitlementsFetchedAt, mcpRegistryDataFetchedAt } : null,
copilotTokenInfo: tokenEntitlementsResult?.data.copilotTokenInfo ?? null,
policyData: accountPolicyResult,
copilotTokenInfo: tokenEntitlementsResult?.data?.copilotTokenInfo ?? null,
};
} catch (error) {
this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error));
@@ -627,13 +629,13 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return expectedScopes.every(scope => scopes.includes(scope));
}
private async getTokenEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: { policyData: Partial<IPolicyData>; copilotTokenInfo: ICopilotTokenInfo }; fetchedAt: number } | undefined> {
private async getTokenEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: { policyData: Partial<IPolicyData>; copilotTokenInfo: ICopilotTokenInfo } | undefined; fetchedAt: number }> {
if (accountPolicyData?.tokenEntitlementsFetchedAt && !this.isDataStale(accountPolicyData.tokenEntitlementsFetchedAt)) {
this.logService.debug('[DefaultAccount] Using last fetched token entitlements data');
return { data: { policyData: accountPolicyData.policyData, copilotTokenInfo: this._copilotTokenInfo ?? {} }, fetchedAt: accountPolicyData.tokenEntitlementsFetchedAt };
}
const data = await this.requestTokenEntitlements(sessions);
return data ? { data, fetchedAt: Date.now() } : undefined;
return { data, fetchedAt: Date.now() };
}
private async requestTokenEntitlements(sessions: AuthenticationSession[]): Promise<{ policyData: Partial<IPolicyData>; copilotTokenInfo: ICopilotTokenInfo } | undefined> {
@@ -680,37 +682,45 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return undefined;
}
private async getEntitlements(sessions: AuthenticationSession[]): Promise<IEntitlementsData | undefined | null> {
private async getEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: IEntitlementsData | undefined | null; fetchedAt: number | undefined }> {
const accountId = sessions[0].account.id;
const existingData = this._defaultAccount?.accountId === accountId ? this._defaultAccount?.defaultAccount.entitlementsData : undefined;
if (existingData && accountPolicyData?.entitlementsFetchedAt && !this.isDataStale(accountPolicyData.entitlementsFetchedAt)) {
this.logService.debug('[DefaultAccount] Using last fetched entitlements data');
return { data: existingData, fetchedAt: accountPolicyData.entitlementsFetchedAt };
}
const entitlementUrl = this.getEntitlementUrl();
if (!entitlementUrl) {
this.logService.debug('[DefaultAccount] No chat entitlements URL found');
return undefined;
return { data: undefined, fetchedAt: undefined };
}
this.logService.debug('[DefaultAccount] Fetching entitlements from:', entitlementUrl);
const response = await this.request(entitlementUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.entitlements');
if (!response) {
return undefined;
return { data: undefined, fetchedAt: Date.now() };
}
if (response.res.statusCode && response.res.statusCode !== 200) {
this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching entitlements`);
return (
const data = (
response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked)
response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist
) ? null : undefined;
return { data, fetchedAt: Date.now() };
}
try {
const data = await asJson<IEntitlementsData>(response);
if (data) {
return data;
return { data, fetchedAt: Date.now() };
}
this.logService.error('[DefaultAccount] Failed to fetch entitlements', 'No data returned');
} catch (error) {
this.logService.error('[DefaultAccount] Failed to fetch entitlements', getErrorMessage(error));
}
return undefined;
return { data: undefined, fetchedAt: Date.now() };
}
private async getMcpRegistryProvider(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<{ data: IMcpRegistryProvider | null; fetchedAt: number } | undefined> {
@@ -103,7 +103,7 @@ export class TestNativeHostService implements INativeHostService {
throw new Error('Method not implemented.');
}
async openSessionsWindow(): Promise<void> { }
async openAgentsWindow(): Promise<void> { }
async toggleFullScreen(): Promise<void> { }
async isMaximized(): Promise<boolean> { return true; }