diff --git a/src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts b/src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts index 437721c171f..4e97723f9a8 100644 --- a/src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts +++ b/src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts @@ -24,6 +24,7 @@ import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, type IJsonR import { ContentEncoding } from '../common/state/protocol/commands.js'; import type { ISessionSummary } from '../common/state/sessionState.js'; import { WebSocketClientTransport } from './webSocketClientTransport.js'; +import { encodeBase64 } from '../../../base/common/buffer.js'; /** * A protocol-level client for a single remote agent host connection. @@ -288,9 +289,8 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC if (!p.uri) { sendError('Missing uri'); return; } this._fileService.readFile(URI.parse(p.uri)).then(content => { sendResult({ - data: content.value.toString(), - encoding: ContentEncoding.Utf8, - contentType: 'text/plain', + data: encodeBase64(content.value), + encoding: ContentEncoding.Base64, }); }).catch(err => sendError(err instanceof Error ? err.message : String(err))); return; diff --git a/src/vs/platform/agentHost/node/agentPluginManager.ts b/src/vs/platform/agentHost/node/agentPluginManager.ts index b2aa73592e5..2ba70f32a06 100644 --- a/src/vs/platform/agentHost/node/agentPluginManager.ts +++ b/src/vs/platform/agentHost/node/agentPluginManager.ts @@ -5,7 +5,6 @@ import { VSBuffer } from '../../../base/common/buffer.js'; import { SequencerByKey } from '../../../base/common/async.js'; -import { ResourceMap } from '../../../base/common/map.js'; import { URI } from '../../../base/common/uri.js'; import { IFileService } from '../../files/common/files.js'; import { ILogService } from '../../log/common/log.js'; @@ -42,11 +41,11 @@ export class AgentPluginManager implements IAgentPluginManager { /** Serializes concurrent sync operations per plugin URI. */ private readonly _sequencer = new SequencerByKey(); - /** Nonces for plugins on disk, keyed by plugin URI. */ - private readonly _cachedNonces = new ResourceMap(); + /** Nonces for plugins on disk, keyed by original customization URI string. */ + private readonly _cachedNonces = new Map(); - /** LRU order: most recently used plugin URIs at the end. */ - private readonly _lruOrder: URI[] = []; + /** LRU order: most recently used original customization URI strings at the end. */ + private readonly _lruOrder: string[] = []; /** Whether the on-disk cache has been loaded. */ private _cacheLoaded = false; @@ -110,8 +109,8 @@ export class AgentPluginManager implements IAgentPluginManager { const destDir = URI.joinPath(this._basePath, key); // Nonce cache hit — skip copy - if (ref.nonce && this._cachedNonces.get(pluginUri) === ref.nonce) { - this._touchLru(pluginUri); + if (ref.nonce && this._cachedNonces.get(ref.uri) === ref.nonce) { + this._touchLru(ref.uri); this._logService.trace(`[AgentPluginManager] Nonce match for ${ref.uri}, skipping copy`); return destDir; } @@ -121,9 +120,9 @@ export class AgentPluginManager implements IAgentPluginManager { await this._fileService.copy(pluginUri, destDir, true); if (ref.nonce) { - this._cachedNonces.set(pluginUri, ref.nonce); + this._cachedNonces.set(ref.uri, ref.nonce); } - this._touchLru(pluginUri); + this._touchLru(ref.uri); await this._evictIfNeeded(); await this._persistCache(); @@ -134,8 +133,8 @@ export class AgentPluginManager implements IAgentPluginManager { return uri.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 128); } - private _touchLru(uri: URI): void { - const idx = this._lruOrder.findIndex(u => u.toString() === uri.toString()); + private _touchLru(uri: string): void { + const idx = this._lruOrder.indexOf(uri); if (idx !== -1) { this._lruOrder.splice(idx, 1); } @@ -149,13 +148,13 @@ export class AgentPluginManager implements IAgentPluginManager { break; } this._cachedNonces.delete(evictUri); - const evictKey = this._keyForUri(evictUri.toString()); + const evictKey = this._keyForUri(evictUri); const evictDir = URI.joinPath(this._basePath, evictKey); - this._logService.info(`[AgentPluginManager] Evicting plugin: ${evictUri.toString()}`); + this._logService.info(`[AgentPluginManager] Evicting plugin: ${evictUri}`); try { await this._fileService.del(evictDir, { recursive: true }); } catch (err) { - this._logService.warn(`[AgentPluginManager] Failed to evict plugin: ${evictUri.toString()}`, err); + this._logService.warn(`[AgentPluginManager] Failed to evict plugin: ${evictUri}`, err); } } } @@ -181,9 +180,8 @@ export class AgentPluginManager implements IAgentPluginManager { // Entries are stored in LRU order (oldest first) for (const entry of entries) { if (typeof entry.uri === 'string' && typeof entry.nonce === 'string') { - const uri = URI.parse(entry.uri); - this._cachedNonces.set(uri, entry.nonce); - this._lruOrder.push(uri); + this._cachedNonces.set(entry.uri, entry.nonce); + this._lruOrder.push(entry.uri); } } this._logService.trace(`[AgentPluginManager] Loaded ${entries.length} cache entries from disk`); @@ -199,7 +197,7 @@ export class AgentPluginManager implements IAgentPluginManager { for (const uri of this._lruOrder) { const nonce = this._cachedNonces.get(uri); if (nonce) { - entries.push({ uri: uri.toString(), nonce }); + entries.push({ uri, nonce }); } } await this._fileService.createFolder(this._basePath); diff --git a/src/vs/platform/agentHost/node/protocolServerHandler.ts b/src/vs/platform/agentHost/node/protocolServerHandler.ts index d62f9bd3c2d..a741299f937 100644 --- a/src/vs/platform/agentHost/node/protocolServerHandler.ts +++ b/src/vs/platform/agentHost/node/protocolServerHandler.ts @@ -201,6 +201,7 @@ export class ProtocolServerHandler extends Disposable { if (client && this._clients.get(client.clientId) === client) { this._logService.info(`[ProtocolServer] Client disconnected: ${client.clientId}`); this._clients.delete(client.clientId); + this._rejectPendingReverseRequests(client.clientId); this._onDidChangeConnectionCount.fire(this._clients.size); } disposables.dispose(); @@ -403,11 +404,12 @@ export class ProtocolServerHandler extends Disposable { // ---- Reverse RPC (server → client requests) ---------------------------- private _reverseRequestId = 0; - private readonly _pendingReverseRequests = new Map void; reject: (reason: unknown) => void }>(); + private readonly _pendingReverseRequests = new Map void; reject: (reason: unknown) => void }>(); /** * Sends a JSON-RPC request to a connected client and waits for the response. * Used for reverse-RPC operations like reading client-side files. + * Rejects if the client disconnects or the server is disposed. */ private _sendReverseRequest(clientId: string, method: string, params: unknown): Promise { const client = this._clients.get(clientId); @@ -416,12 +418,24 @@ export class ProtocolServerHandler extends Disposable { } const id = ++this._reverseRequestId; return new Promise((resolve, reject) => { - this._pendingReverseRequests.set(id, { resolve: resolve as (value: unknown) => void, reject }); + this._pendingReverseRequests.set(id, { clientId, resolve: resolve as (value: unknown) => void, reject }); const request: IJsonRpcRequest = { jsonrpc: '2.0', id, method, params }; client.transport.send(request); }); } + /** + * Rejects and clears all pending reverse-RPC requests for a given client. + */ + private _rejectPendingReverseRequests(clientId: string): void { + for (const [id, pending] of this._pendingReverseRequests) { + if (pending.clientId === clientId) { + this._pendingReverseRequests.delete(id); + pending.reject(new Error(`Client ${clientId} disconnected`)); + } + } + } + private _handleRequest(client: IConnectedClient, method: string, params: unknown, id: number): void { const handler = this._requestHandlers.hasOwnProperty(method) ? this._requestHandlers[method as RequestMethod] : undefined; if (handler) { @@ -512,6 +526,10 @@ export class ProtocolServerHandler extends Disposable { client.disposables.dispose(); } this._clients.clear(); + for (const [, pending] of this._pendingReverseRequests) { + pending.reject(new Error('ProtocolServerHandler disposed')); + } + this._pendingReverseRequests.clear(); this._replayBuffer.length = 0; super.dispose(); } diff --git a/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts b/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts index 5d339b9bbe6..497d945e684 100644 --- a/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts +++ b/src/vs/platform/agentHost/test/node/agentPluginManager.test.ts @@ -12,6 +12,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { FileService } from '../../../files/common/fileService.js'; import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; +import { AGENT_CLIENT_SCHEME, toAgentClientUri } from '../../common/agentClientUri.js'; import { CustomizationStatus, type ICustomizationRef, type ISessionCustomization } from '../../common/state/sessionState.js'; import { AgentPluginManager } from '../../node/agentPluginManager.js'; @@ -25,6 +26,7 @@ suite('AgentPluginManager', () => { setup(() => { fileService = disposables.add(new FileService(new NullLogService())); disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); + disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, disposables.add(new InMemoryFileSystemProvider()))); manager = new AgentPluginManager(basePath, fileService, new NullLogService()); }); @@ -40,10 +42,11 @@ suite('AgentPluginManager', () => { } async function seedPluginDir(name: string, files: Record): Promise { - const dir = URI.from({ scheme: Schemas.inMemory, path: `/plugins/${name}` }); - await fileService.createFolder(dir); + const originalUri = URI.from({ scheme: Schemas.inMemory, path: `/plugins/${name}` }); + const agentClientDir = toAgentClientUri(originalUri, 'test-client'); + await fileService.createFolder(agentClientDir); for (const [fileName, content] of Object.entries(files)) { - await fileService.writeFile(URI.joinPath(dir, fileName), VSBuffer.fromString(content)); + await fileService.writeFile(URI.joinPath(agentClientDir, fileName), VSBuffer.fromString(content)); } } diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts index f8078b5027b..9da1eaba4e5 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts @@ -79,8 +79,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements // When a session is active, prefer session-level data (includes status) if (this._sessionCustomizations) { return this._sessionCustomizations.map(sc => ({ - uri: URI.isUri(sc.customization.uri) ? sc.customization.uri : URI.parse(sc.customization.uri as unknown as string), - type: 'customization', + uri: URI.isUri(sc.customization.uri) ? sc.customization.uri : URI.parse(sc.customization.uri), + type: 'plugin', name: sc.customization.displayName, description: sc.customization.description, status: toStatusString(sc.status), @@ -92,7 +92,7 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements // Baseline: agent-level customizations (no status info) return this._agentCustomizations.map(ref => ({ uri: URI.isUri(ref.uri) ? ref.uri : URI.parse(ref.uri as unknown as string), - type: 'customization', + type: 'plugin', name: ref.displayName, description: ref.description, })); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationSyncProvider.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationSyncProvider.ts index 2c4aed96511..aad8bd94e5f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationSyncProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationSyncProvider.ts @@ -46,7 +46,13 @@ export class AgentCustomizationSyncProvider extends Disposable implements ICusto const stored = this._storageService.get(this._storageKey, StorageScope.PROFILE); this._entries = new Map(); if (stored) { - const parsed = JSON.parse(stored) as (string | ISyncEntry)[]; + let parsed: (string | ISyncEntry)[] | undefined; + try { + parsed = JSON.parse(stored) as (string | ISyncEntry)[]; + } catch { + // ignored + } + if (Array.isArray(parsed)) { for (const item of parsed) { if (typeof item === 'string') { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index f7a7bd726d0..3ff27132289 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts @@ -407,13 +407,14 @@ export function getEffectiveCommandSource(hook: IHookCommand, os: OperatingSyste * Returns the actual field name from the JSON (e.g., 'bash' instead of 'osx' if bash was used). * This is used for editor focus to highlight the correct field. */ -export function getEffectiveCommandFieldKey(hook: IHookCommand, os: OperatingSystem): string { +export function getEffectiveCommandFieldKey(hook: IHookCommand | IParsedHookCommand, os: OperatingSystem): string { + const h = hook as Partial; if (os === OperatingSystem.Windows && hook.windows) { - return hook.windowsSource ?? 'windows'; + return h.windowsSource ?? 'windows'; } else if (os === OperatingSystem.Macintosh && hook.osx) { - return hook.osxSource ?? 'osx'; + return h.osxSource ?? 'osx'; } else if (os === OperatingSystem.Linux && hook.linux) { - return hook.linuxSource ?? 'linux'; + return h.linuxSource ?? 'linux'; } return 'command'; } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index b943c3529c1..fd562fbd396 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -40,6 +40,9 @@ import { TestFileService } from '../../../../../test/common/workbenchTestService import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { MockLabelService } from '../../../../../services/label/test/common/mockLabelService.js'; import { IAgentHostFileSystemService } from '../../../../../services/agentHost/common/agentHostFileSystemService.js'; +import { ICustomizationHarnessService } from '../../../common/customizationHarnessService.js'; +import { IAgentPluginService } from '../../../common/plugins/agentPluginService.js'; +import { IStorageService, InMemoryStorageService } from '../../../../../../platform/storage/common/storage.js'; // ---- Mock agent host service ------------------------------------------------ @@ -86,6 +89,12 @@ class MockAgentHostService extends mock() { // Protocol methods public override readonly clientId = 'test-window-1'; public dispatchedActions: { action: ISessionAction; clientId: string; clientSeq: number }[] = []; + + /** Returns dispatched actions filtered to turn-related types only + * (excludes lifecycle actions like activeClientChanged). */ + get turnActions() { + return this.dispatchedActions.filter(d => d.action.type === 'session/turnStarted'); + } public sessionStates = new Map(); override async subscribe(resource: URI): Promise { const resourceStr = resource.toString(); @@ -192,6 +201,13 @@ function createTestServices(disposables: DisposableStore) { registerAuthority: () => toDisposable(() => { }), ensureSyncedCustomizationProvider: () => { }, }); + instantiationService.stub(IStorageService, disposables.add(new InMemoryStorageService())); + instantiationService.stub(ICustomizationHarnessService, { + registerExternalHarness: () => toDisposable(() => { }), + }); + instantiationService.stub(IAgentPluginService, { + plugins: observableValue('plugins', []), + }); return { instantiationService, agentHostService, chatAgentService }; } @@ -255,6 +271,10 @@ async function startTurn( const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); ds.add(toDisposable(() => chatSession.dispose())); + // Clear any lifecycle actions (e.g. activeClientChanged from customization setup) + // so tests only see turn-related dispatches. + agentHostService.dispatchedActions.length = 0; + const collected: IChatProgress[][] = []; const seq = { v: 1 }; @@ -272,7 +292,9 @@ async function startTurn( await timeout(10); - const lastDispatch = agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1]; + // Filter for turn-related dispatches only (skip activeClientChanged etc.) + const turnDispatches = agentHostService.dispatchedActions.filter(d => d.action.type === 'session/turnStarted'); + const lastDispatch = turnDispatches[turnDispatches.length - 1] ?? agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1]; const session = (lastDispatch?.action as ITurnStartedAction)?.session; const turnId = (lastDispatch?.action as ITurnStartedAction)?.turnId; @@ -365,9 +387,9 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - assert.strictEqual(agentHostService.dispatchedActions[0].action.type, 'session/turnStarted'); - assert.strictEqual((agentHostService.dispatchedActions[0].action as ITurnStartedAction).userMessage.text, 'Hello'); + assert.strictEqual(agentHostService.turnActions.length, 1); + assert.strictEqual(agentHostService.turnActions[0].action.type, 'session/turnStarted'); + assert.strictEqual((agentHostService.turnActions[0].action as ITurnStartedAction).userMessage.text, 'Hello'); assert.ok(AgentSession.id(URI.parse(session)).startsWith('sdk-session-')); })); @@ -378,13 +400,16 @@ suite('AgentHostChatContribution', () => { const chatSession = await sessionHandler.provideChatSessionContent(resource, CancellationToken.None); disposables.add(toDisposable(() => chatSession.dispose())); + // Clear lifecycle actions so only turn dispatches are counted + agentHostService.dispatchedActions.length = 0; + // First turn const turn1Promise = chatSession.requestHandler!( makeRequest({ message: 'First', sessionResource: resource }), () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch1 = agentHostService.dispatchedActions[0]; + const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; // Echo the turnStarted to clear pending write-ahead agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); @@ -397,16 +422,16 @@ suite('AgentHostChatContribution', () => { () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch2 = agentHostService.dispatchedActions[1]; + const dispatch2 = agentHostService.turnActions[1]; const action2 = dispatch2.action as ITurnStartedAction; agentHostService.fireAction({ action: dispatch2.action, serverSeq: 3, origin: { clientId: agentHostService.clientId, clientSeq: dispatch2.clientSeq } }); agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action2.session, turnId: action2.turnId } as ISessionAction, serverSeq: 4, origin: undefined }); await turn2Promise; - assert.strictEqual(agentHostService.dispatchedActions.length, 2); + assert.strictEqual(agentHostService.turnActions.length, 2); assert.strictEqual( - (agentHostService.dispatchedActions[0].action as ITurnStartedAction).session.toString(), - (agentHostService.dispatchedActions[1].action as ITurnStartedAction).session.toString(), + (agentHostService.turnActions[0].action as ITurnStartedAction).session.toString(), + (agentHostService.turnActions[1].action as ITurnStartedAction).session.toString(), ); })); @@ -1265,8 +1290,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; assert.deepStrictEqual(turnAction.userMessage.attachments, [ { type: 'file', path: URI.file('/workspace/test.ts').fsPath, displayName: 'test.ts' }, ]); @@ -1286,8 +1311,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; assert.deepStrictEqual(turnAction.userMessage.attachments, [ { type: 'directory', path: URI.file('/workspace/src').fsPath, displayName: 'src' }, ]); @@ -1307,8 +1332,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; assert.deepStrictEqual(turnAction.userMessage.attachments, [ { type: 'selection', path: URI.file('/workspace/foo.ts').fsPath, displayName: 'selection' }, ]); @@ -1328,8 +1353,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; // No attachments because it's not a file:// URI assert.strictEqual(turnAction.userMessage.attachments, undefined); })); @@ -1348,8 +1373,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; assert.strictEqual(turnAction.userMessage.attachments, undefined); })); @@ -1370,8 +1395,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; assert.deepStrictEqual(turnAction.userMessage.attachments, [ { type: 'file', path: URI.file('/workspace/a.ts').fsPath, displayName: 'a.ts' }, { type: 'directory', path: URI.file('/workspace/lib').fsPath, displayName: 'lib' }, @@ -1387,8 +1412,8 @@ suite('AgentHostChatContribution', () => { fire({ type: 'session/turnComplete', session, turnId } as ISessionAction); await turnPromise; - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction; + assert.strictEqual(agentHostService.turnActions.length, 1); + const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction; assert.strictEqual(turnAction.userMessage.attachments, undefined); })); }); @@ -1560,8 +1585,8 @@ suite('AgentHostChatContribution', () => { await turnPromise; // Turn dispatched via connection.dispatchAction - assert.strictEqual(agentHostService.dispatchedActions.length, 1); - assert.strictEqual((agentHostService.dispatchedActions[0].action as ITurnStartedAction).userMessage.text, 'Test message'); + assert.strictEqual(agentHostService.turnActions.length, 1); + assert.strictEqual((agentHostService.turnActions[0].action as ITurnStartedAction).userMessage.text, 'Test message'); })); }); @@ -1857,13 +1882,16 @@ suite('AgentHostChatContribution', () => { const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); disposables.add(toDisposable(() => chatSession.dispose())); + // Clear lifecycle actions so only turn dispatches are counted + agentHostService.dispatchedActions.length = 0; + // First, do a normal turn so the backend session is created const turn1Promise = chatSession.requestHandler!( makeRequest({ message: 'Hello', sessionResource }), () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch1 = agentHostService.dispatchedActions[0]; + const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; const session = action1.session; // Echo + complete the first turn @@ -1904,13 +1932,16 @@ suite('AgentHostChatContribution', () => { const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); disposables.add(toDisposable(() => chatSession.dispose())); + // Clear lifecycle actions so only turn dispatches are counted + agentHostService.dispatchedActions.length = 0; + // Normal turn to create backend session const turn1Promise = chatSession.requestHandler!( makeRequest({ message: 'Init', sessionResource }), () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch1 = agentHostService.dispatchedActions[0]; + const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; const session = action1.session; agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); @@ -1976,13 +2007,16 @@ suite('AgentHostChatContribution', () => { const serverRequestEvents: { prompt: string }[] = []; disposables.add(chatSession.onDidStartServerRequest!(e => serverRequestEvents.push(e))); + // Clear lifecycle actions so only turn dispatches are counted + agentHostService.dispatchedActions.length = 0; + // Normal client turn — should NOT fire onDidStartServerRequest const turnPromise = chatSession.requestHandler!( makeRequest({ message: 'Client turn', sessionResource }), () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch = agentHostService.dispatchedActions[0]; + const dispatch = agentHostService.turnActions[0]; const action = dispatch.action as ITurnStartedAction; agentHostService.fireAction({ action: dispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch.clientSeq } }); agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action.session, turnId: action.turnId } as ISessionAction, serverSeq: 2, origin: undefined }); @@ -1998,13 +2032,16 @@ suite('AgentHostChatContribution', () => { const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); disposables.add(toDisposable(() => chatSession.dispose())); + // Clear lifecycle actions so only turn dispatches are counted + agentHostService.dispatchedActions.length = 0; + // First, do a normal turn so the backend session is created const turn1Promise = chatSession.requestHandler!( makeRequest({ message: 'Init', sessionResource }), () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch1 = agentHostService.dispatchedActions[0]; + const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; const session = action1.session; agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } }); @@ -2061,13 +2098,16 @@ suite('AgentHostChatContribution', () => { const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); disposables.add(toDisposable(() => chatSession.dispose())); + // Clear lifecycle actions so only turn dispatches are counted + agentHostService.dispatchedActions.length = 0; + // First, do a normal turn so the backend session is created const turn1Promise = chatSession.requestHandler!( makeRequest({ message: 'Init', sessionResource }), () => { }, [], CancellationToken.None, ); await timeout(10); - const dispatch1 = agentHostService.dispatchedActions[0]; + const dispatch1 = agentHostService.turnActions[0]; const action1 = dispatch1.action as ITurnStartedAction; const session = action1.session; agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } });