mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-19 14:22:19 +01:00
agentHost: preserve local side chat context
Keep the selected local turn as side-chat provenance while using a separate concrete provider anchor. Inject the bounded local context exactly once on the first side-chat prompt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -195,3 +195,4 @@ You **must** run these checks before declaring work complete:
|
||||
- **Docked side-pane width persistence must be symmetric about the detail (aux) width**: in single-pane the docked detail (auxiliary bar) lives *inside* the editor grid node, so the workbench persists the pure editor-content width (`_persistedEditorWidth` = node − detail) and the grid descriptor reconstructs node = editor-content + detail. These must use the **same condition** for including the detail: only when the detail is visible (`partVisibility.auxiliaryBar`). Subtracting the detail width *unconditionally* at save while adding it back only when the detail is visible shrank an **Editor-only** session's side pane by the detail width on *every* reload, compounding toward zero ("side pane always tiny on reload"). Fix `_persistedEditorWidth` to subtract only when the detail is visible.
|
||||
- **Side-pane (editor grid node) size is workbench-level, not per session**: the editor grid node width is owned by the workbench grid and persisted globally (`workbench.sessions.partSizes` via `_savePartSizes`/`createDesktopGridDescriptor`), so switching sessions keeps the same width and reload restores it in one paint. Do **not** add a per-session width map in the layout controller that re-applies a width on session switch/reveal — it makes switching sessions jump the side pane around, and a post-paint restore on reload flickers. The 60% first-open default (`SIDE_PANE_WIDTH_RATIO` in `parts/editorPartSizing.ts`, applied by the single-pane `_applyEditorSplitSize` override on the first reveal that has no size to restore) is the only width the layout intentionally sets; everything else is the user's persisted grid size.
|
||||
- **Per-session menu visibility must flow through scoped context keys, not widget DOM hiding or global active-session keys**: toolbars evaluate `when` against their scoped `IContextKeyService`, while their pickers act on scoped `ISessionContext`. Expose provider-specific availability as an observable on `ISession` and publish it through `setSessionContextKeys`, so every visible session surface evaluates the same scoped state declaratively.
|
||||
- **Keep validation proportional to the change**: once focused tests cover a small correction, do not chain repeated reviewers or broad suites unless a concrete failure warrants escalation.
|
||||
|
||||
@@ -993,6 +993,8 @@ export interface IAgentCreateChatSideChatSource {
|
||||
readonly source: URI;
|
||||
/** Turn ID in the source chat the side chat records as its provenance. */
|
||||
readonly turnId: string;
|
||||
/** Concrete provider turn ID to fork/resume from when `turnId` names a host-only local turn. */
|
||||
readonly providerAnchorTurnId?: string;
|
||||
/** Bounded source-chat context captured from host state when the provider transcript lags. */
|
||||
readonly sourceContext?: string;
|
||||
/** User-visible assistant text captured while the source turn was active. */
|
||||
|
||||
@@ -16,6 +16,7 @@ export const MAX_SIDE_CHAT_CONTEXT_CHARS = 20_000;
|
||||
export interface IPersistedSideChat {
|
||||
readonly source: string;
|
||||
readonly turnId: string;
|
||||
readonly providerAnchorTurnId?: string;
|
||||
readonly inheritedTurnCount: number;
|
||||
readonly partialResponse?: string;
|
||||
readonly context?: string;
|
||||
@@ -50,6 +51,14 @@ export function getSideChatPartialResponse(activeTurn: ActiveTurn | undefined):
|
||||
return responseMarkdown ? truncateMiddle(responseMarkdown, MAX_SIDE_CHAT_CONTEXT_CHARS) : undefined;
|
||||
}
|
||||
|
||||
export function buildBoundedSideChatSourceContext(turns: readonly Turn[], turnId: string, activeTurn?: ActiveTurn): string | undefined {
|
||||
if (activeTurn?.id === turnId) {
|
||||
return buildSideChatSourceContext(turns, activeTurn);
|
||||
}
|
||||
const turnIndex = turns.findIndex(turn => turn.id === turnId);
|
||||
return turnIndex === -1 ? undefined : buildSideChatSourceContext(turns.slice(0, turnIndex + 1));
|
||||
}
|
||||
|
||||
export function injectSideChatContext(prompt: string, partialResponse?: string, sourceContext?: string): string {
|
||||
const context = [SIDE_CHAT_GUIDANCE];
|
||||
if (sourceContext) {
|
||||
@@ -77,11 +86,11 @@ export function prepareSideChatPrompt(prompt: string, turns: readonly Turn[], si
|
||||
if (!sideChat || turns.length > sideChat.inheritedTurnCount) {
|
||||
return prompt;
|
||||
}
|
||||
const sourceTurn = turns.find(turn => turn.id === sideChat.turnId);
|
||||
const sourceContext = sourceTurn ? undefined : sideChat.context;
|
||||
const selectedSourceTurn = turns.find(turn => turn.id === sideChat.turnId);
|
||||
const sourceContext = selectedSourceTurn ? undefined : sideChat.context;
|
||||
let partialResponse = sideChat.partialResponse;
|
||||
if (partialResponse) {
|
||||
const inheritedResponse = sourceTurn ? renderResponseMarkdown(sourceTurn.responseParts) : '';
|
||||
const inheritedResponse = selectedSourceTurn ? renderResponseMarkdown(selectedSourceTurn.responseParts) : '';
|
||||
if (inheritedResponse.includes(partialResponse)) {
|
||||
partialResponse = undefined;
|
||||
}
|
||||
@@ -180,16 +189,18 @@ export function decodeProviderData(providerData: string): IPersistedChat | undef
|
||||
const validModel = model && typeof model === 'object' && typeof (model as { id?: unknown }).id === 'string'
|
||||
? model as ModelSelection
|
||||
: undefined;
|
||||
const sideChat = value.sideChat as { source?: unknown; turnId?: unknown; inheritedTurnCount?: unknown; partialResponse?: unknown; context?: unknown } | undefined;
|
||||
const sideChat = value.sideChat as { source?: unknown; turnId?: unknown; providerAnchorTurnId?: unknown; inheritedTurnCount?: unknown; partialResponse?: unknown; context?: unknown } | undefined;
|
||||
const validSideChat = sideChat
|
||||
&& typeof sideChat.source === 'string'
|
||||
&& typeof sideChat.turnId === 'string'
|
||||
&& (sideChat.providerAnchorTurnId === undefined || typeof sideChat.providerAnchorTurnId === 'string')
|
||||
&& typeof sideChat.inheritedTurnCount === 'number'
|
||||
&& (sideChat.partialResponse === undefined || typeof sideChat.partialResponse === 'string')
|
||||
&& (sideChat.context === undefined || typeof sideChat.context === 'string')
|
||||
? {
|
||||
source: sideChat.source,
|
||||
turnId: sideChat.turnId,
|
||||
...(sideChat.providerAnchorTurnId ? { providerAnchorTurnId: sideChat.providerAnchorTurnId } : {}),
|
||||
inheritedTurnCount: sideChat.inheritedTurnCount,
|
||||
...(sideChat.partialResponse ? { partialResponse: sideChat.partialResponse } : {}),
|
||||
...(sideChat.context ? { context: sideChat.context } : {}),
|
||||
|
||||
@@ -36,7 +36,7 @@ import type { ChatPendingMessageSetAction, ChatTurnStartedAction } from '../comm
|
||||
import { ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, readSessionSpawnDepth, withSessionSpawnDepth, SessionStatus, ToolCallStatus, ToolResultContentType, AH_META_WORKSPACELESS_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, hostBuildInfoFromProduct, isAhpChatChannel, isDefaultChatUri, isSubagentChatUri, isSubagentSession, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSubagentSessionUri, readSessionGitState, readSessionWorkspaceless, withSessionGitHubState, withSessionGitState, withSessionWorkspaceless, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
|
||||
import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { buildSideChatSourceContext, getSideChatPartialResponse } from './agentPeerChats.js';
|
||||
import { buildBoundedSideChatSourceContext, getSideChatPartialResponse } from './agentPeerChats.js';
|
||||
import { AgentConfigurationService, IAgentConfigurationService } from './agentConfigurationService.js';
|
||||
import { AgentHostTerminalManager, IAgentHostTerminalManager } from './agentHostTerminalManager.js';
|
||||
import { ISessionDbUriFields, parseSessionDbUri } from './shared/fileEditTracker.js';
|
||||
@@ -1193,14 +1193,11 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
sideChat: {
|
||||
...options.sideChat,
|
||||
source: URI.parse(resolvedSideChat.sourceChat),
|
||||
...(resolvedSideChat.providerAnchorTurnId ? { providerAnchorTurnId: resolvedSideChat.providerAnchorTurnId } : {}),
|
||||
...(resolvedSideChat.sourceContext ? { sourceContext: resolvedSideChat.sourceContext } : {}),
|
||||
...(resolvedSideChat.partialResponse ? { partialResponse: resolvedSideChat.partialResponse } : {}),
|
||||
},
|
||||
};
|
||||
const concreteTurnId = this._localTurns.resolveConcreteTurnId(resolvedSideChat.sourceChat, options.sideChat.turnId);
|
||||
if (concreteTurnId !== undefined) {
|
||||
createOptions = { ...createOptions, sideChat: { ...createOptions.sideChat!, turnId: concreteTurnId } };
|
||||
}
|
||||
}
|
||||
if (options?.fork) {
|
||||
const sourceKey = options.fork.source.toString();
|
||||
@@ -1284,7 +1281,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
* origin. Throws when the source chat is not part of `session` or when the
|
||||
* referenced completed or active turn is absent.
|
||||
*/
|
||||
private _resolveSideChatOrigin(session: URI, sideChat: IAgentCreateChatSideChatSource): { origin: ChatOrigin; sourceChat: string; sourceContext?: string; partialResponse?: string } {
|
||||
private _resolveSideChatOrigin(session: URI, sideChat: IAgentCreateChatSideChatSource): { origin: ChatOrigin; sourceChat: string; providerAnchorTurnId?: string; sourceContext?: string; partialResponse?: string } {
|
||||
const sessionKey = session.toString();
|
||||
const sourceKey = sideChat.source.toString();
|
||||
const { sourceChatKey, sourceSessionKey, sourceState } = this._resolveSessionSourceChat(session, sideChat.source);
|
||||
@@ -1296,11 +1293,16 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
}
|
||||
// The bounded turn must be a real completed or currently-active turn.
|
||||
const activeTurn = sourceState?.activeTurn?.id === sideChat.turnId ? sourceState.activeTurn : undefined;
|
||||
if (!sourceState?.turns.some(t => t.id === sideChat.turnId) && !activeTurn) {
|
||||
const hasCompletedTurn = sourceState?.turns.some(t => t.id === sideChat.turnId) ?? false;
|
||||
if (!hasCompletedTurn && !activeTurn) {
|
||||
throw new Error(`[AgentService] createChat: side chat source turn ${sideChat.turnId} not found in ${sourceKey}`);
|
||||
}
|
||||
const isLocalSourceTurn = !activeTurn && this._localTurns.isLocal(sourceChatKey, sideChat.turnId);
|
||||
const providerAnchorTurnId = isLocalSourceTurn ? this._localTurns.resolveConcreteTurnId(sourceChatKey, sideChat.turnId) : undefined;
|
||||
const partialResponse = getSideChatPartialResponse(activeTurn);
|
||||
const sourceContext = activeTurn ? buildSideChatSourceContext(sourceState?.turns ?? [], activeTurn) : undefined;
|
||||
const sourceContext = (activeTurn || isLocalSourceTurn)
|
||||
? buildBoundedSideChatSourceContext(sourceState?.turns ?? [], sideChat.turnId, activeTurn)
|
||||
: undefined;
|
||||
return {
|
||||
origin: {
|
||||
kind: ChatOriginKind.SideChat,
|
||||
@@ -1308,6 +1310,7 @@ export class AgentService extends Disposable implements IAgentService {
|
||||
turnId: sideChat.turnId,
|
||||
},
|
||||
sourceChat: sourceChatKey,
|
||||
...(providerAnchorTurnId ? { providerAnchorTurnId } : {}),
|
||||
...(sourceContext ? { sourceContext } : {}),
|
||||
...(partialResponse ? { partialResponse } : {}),
|
||||
};
|
||||
|
||||
@@ -1256,7 +1256,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
// chat rather than inheriting the whole source backend.
|
||||
sdkSessionId = (await this._forkChat(session, options.fork))?.sessionId;
|
||||
} else if (options?.sideChat) {
|
||||
const forked = await this._forkChat(session, options.sideChat);
|
||||
const forked = await this._forkChat(session, { source: options.sideChat.source, turnId: options.sideChat.providerAnchorTurnId ?? options.sideChat.turnId });
|
||||
sdkSessionId = forked?.sessionId;
|
||||
const fallbackContext = options.sideChat.sourceContext ?? (!forked ? this._buildSideChatContext(session, options.sideChat.source, options.sideChat.turnId) : undefined);
|
||||
if (!forked && !fallbackContext && !options.sideChat.partialResponse) {
|
||||
@@ -1265,6 +1265,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
sideChat = {
|
||||
source: options.sideChat.source.toString(),
|
||||
turnId: options.sideChat.turnId,
|
||||
...(options.sideChat.providerAnchorTurnId ? { providerAnchorTurnId: options.sideChat.providerAnchorTurnId } : {}),
|
||||
inheritedTurnCount: forked?.inheritedTurnCount ?? 0,
|
||||
...(fallbackContext ? { context: fallbackContext } : {}),
|
||||
...(options.sideChat.partialResponse ? { partialResponse: options.sideChat.partialResponse } : {}),
|
||||
|
||||
@@ -2287,11 +2287,12 @@ export class CopilotAgent extends Disposable implements IAgent {
|
||||
if (!sourceEntry) {
|
||||
throw new Error(`[Copilot] createChat side chat: source chat ${options.sideChat.source.toString()} not found`);
|
||||
}
|
||||
const forked = await this._forkSdkChat(client, sourceEntry, options.sideChat.turnId, this._sessionDataService.getSessionDataDir(chat));
|
||||
const forked = await this._forkSdkChat(client, sourceEntry, options.sideChat.providerAnchorTurnId ?? options.sideChat.turnId, this._sessionDataService.getSessionDataDir(chat));
|
||||
sdkSessionId = forked.sessionId;
|
||||
sideChat = {
|
||||
source: options.sideChat.source.toString(),
|
||||
turnId: options.sideChat.turnId,
|
||||
...(options.sideChat.providerAnchorTurnId ? { providerAnchorTurnId: options.sideChat.providerAnchorTurnId } : {}),
|
||||
inheritedTurnCount: forked.inheritedTurnCount,
|
||||
...(options.sideChat.sourceContext ? { context: options.sideChat.sourceContext } : {}),
|
||||
...(options.sideChat.partialResponse ? { partialResponse: options.sideChat.partialResponse } : {}),
|
||||
|
||||
@@ -121,6 +121,28 @@ suite('agentPeerChats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('injects completed local-turn context even when the inherited transcript already contains the concrete provider anchor', () => {
|
||||
const sourceContext = 'User request:\nsource question\n\nAgent response:\nsource answer\n\n---\n\nUser request:\n!command';
|
||||
const localSideChat: IPersistedSideChat = {
|
||||
source: 'ahp-chat://default/source',
|
||||
turnId: 'local-turn',
|
||||
providerAnchorTurnId: sourceTurn.id,
|
||||
inheritedTurnCount: 1,
|
||||
context: sourceContext,
|
||||
};
|
||||
const prepared = prepareSideChatPrompt('Explain the branch', [sourceTurn], localSideChat);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
prepared,
|
||||
localQuestionCount: countOccurrences(prepared, 'User request:\n!command'),
|
||||
sourceQuestionCount: countOccurrences(prepared, 'User request:\nsource question'),
|
||||
}, {
|
||||
prepared: injectSideChatContext('Explain the branch', undefined, sourceContext),
|
||||
localQuestionCount: 1,
|
||||
sourceQuestionCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('strips hidden context even when the source text contains the legacy delimiter', () => {
|
||||
const prepared = prepareSideChatPrompt('Visible prompt', [], {
|
||||
...sideChat,
|
||||
|
||||
@@ -3336,6 +3336,50 @@ suite('AgentService (node dispatcher)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('creates a side chat from a completed local turn without losing its stable source turn identity', async () => {
|
||||
const db = new TestSessionDatabase();
|
||||
const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService()));
|
||||
const agent = disposables.add(new SideChatAgent('copilot'));
|
||||
localService.registerProvider(agent);
|
||||
const { session } = await agent.createSession();
|
||||
const sessionResource = (await agent.listSessions())[0].session;
|
||||
const defaultChatUri = buildDefaultChatUri(sessionResource.toString());
|
||||
agent.sessionMessages = [
|
||||
{ type: 'message', session, role: 'user', messageId: 'real-1', content: 'first question', toolRequests: [] },
|
||||
{ type: 'message', session, role: 'assistant', messageId: 'real-1-a', content: 'first answer', toolRequests: [] },
|
||||
];
|
||||
const localTurn: Turn = {
|
||||
id: 'local-1',
|
||||
state: TurnState.Complete,
|
||||
message: { text: '!command', origin: { kind: MessageKind.User } },
|
||||
responseParts: [],
|
||||
usage: undefined,
|
||||
};
|
||||
await db.insertLocalTurn({ turnId: 'local-1', chatUri: defaultChatUri, anchorTurnId: 'real-1', seq: 1, payload: JSON.stringify(localTurn) });
|
||||
await localService.restoreSession(sessionResource);
|
||||
const chatUri = URI.parse(buildChatUri(sessionResource, 'side-local'));
|
||||
|
||||
await localService.createChat(sessionResource, chatUri, { sideChat: { source: URI.parse(defaultChatUri), turnId: 'local-1' } });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
origin: localService.stateManager.getChatState(chatUri.toString())?.origin,
|
||||
sideChatForwarded: agent.lastCreateOptions?.sideChat && {
|
||||
source: agent.lastCreateOptions.sideChat.source.toString(),
|
||||
turnId: agent.lastCreateOptions.sideChat.turnId,
|
||||
providerAnchorTurnId: agent.lastCreateOptions.sideChat.providerAnchorTurnId,
|
||||
sourceContext: agent.lastCreateOptions.sideChat.sourceContext,
|
||||
},
|
||||
}, {
|
||||
origin: { kind: ChatOriginKind.SideChat, chat: defaultChatUri, turnId: 'local-1' },
|
||||
sideChatForwarded: {
|
||||
source: defaultChatUri,
|
||||
turnId: 'local-1',
|
||||
providerAnchorTurnId: 'real-1',
|
||||
sourceContext: 'User request:\nfirst question\n\nAgent response:\nfirst answer\n\n---\n\nUser request:\n!command',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('creates a side chat from the current active turn', async () => {
|
||||
const agent = disposables.add(new SideChatAgent('copilot'));
|
||||
service.registerProvider(agent);
|
||||
|
||||
@@ -6846,6 +6846,59 @@ suite('ClaudeAgent — Phase 11 customizations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('createChat({ sideChat }) preserves a local source turn id while forking from the concrete provider anchor', async () => {
|
||||
const { agent, sdk } = createTestContext(disposables);
|
||||
await agent.authenticate(GITHUB_COPILOT_PROTECTED_RESOURCE.resource, 'tok');
|
||||
const created = await agent.createSession({ workingDirectory: URI.file('/work') });
|
||||
const parentId = AgentSession.id(created.session);
|
||||
sdk.sessionMessagesById.set(parentId, forkSourceMessages(parentId));
|
||||
sdk.forkSessionResult = { sessionId: 'side-local-1' };
|
||||
sdk.sessionList = [{ sessionId: 'side-local-1', summary: 'side local', lastModified: 1, cwd: URI.file('/work').fsPath }];
|
||||
const sourceContext = 'User request:\nsource question\n\nAgent response:\nsource answer\n\n---\n\nUser request:\n!command';
|
||||
const injectedPrompt = injectSideChatContext('side question', undefined, sourceContext);
|
||||
sdk.sessionMessagesById.set('side-local-1', forkSourceMessages('side-local-1').slice(0, 2));
|
||||
|
||||
const chatUri = URI.parse(buildChatUri(created.session.toString(), 'chat-side-local'));
|
||||
const result = await agent.chats.createChat(chatUri, {
|
||||
sideChat: {
|
||||
source: created.session,
|
||||
turnId: 'local-1',
|
||||
providerAnchorTurnId: 'u1',
|
||||
sourceContext,
|
||||
},
|
||||
});
|
||||
sdk.nextQueryMessages = [makeSystemInitMessage('side-local-1'), makeResultSuccess('side-local-1')];
|
||||
await agent.chats.sendMessage(chatUri, 'side question', undefined, undefined, 'turn-side-local');
|
||||
const sentContent = sdk.warmQueries.at(-1)?.produced?.drainedPrompts[0]?.message.content;
|
||||
const sentPrompt = typeof sentContent === 'string'
|
||||
? sentContent
|
||||
: sentContent?.filter(block => block.type === 'text').map(block => block.text).join('\n');
|
||||
sdk.sessionMessagesById.set('side-local-1', [
|
||||
...forkSourceMessages('side-local-1').slice(0, 2),
|
||||
{ type: 'user', uuid: 'turn-side-local', session_id: 'side-local-1', parent_tool_use_id: null, message: { role: 'user', content: [{ type: 'text', text: injectedPrompt }] } },
|
||||
{ type: 'assistant', uuid: 'a3', session_id: 'side-local-1', parent_tool_use_id: null, message: { id: 'msg_a3', role: 'assistant', content: [{ type: 'text', text: 'side answer' }] } },
|
||||
]);
|
||||
const turns = await agent.chats.getMessages(chatUri);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
forkCall: sdk.forkSessionCalls[0],
|
||||
sentPrompt,
|
||||
turns: turns.map(turn => turn.message.text),
|
||||
sideChat: result ? JSON.parse(result.providerData!).sideChat : undefined,
|
||||
}, {
|
||||
forkCall: { sessionId: parentId, options: { upToMessageId: 'a1' } },
|
||||
sentPrompt: injectedPrompt,
|
||||
turns: ['side question'],
|
||||
sideChat: {
|
||||
source: created.session.toString(),
|
||||
turnId: 'local-1',
|
||||
providerAnchorTurnId: 'u1',
|
||||
inheritedTurnCount: 1,
|
||||
context: sourceContext,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('createChat({ fork }) with an unknown turn falls back to a fresh chat', async () => {
|
||||
const { agent, sdk } = createTestContext(disposables);
|
||||
await agent.authenticate(GITHUB_COPILOT_PROTECTED_RESOURCE.resource, 'tok');
|
||||
|
||||
@@ -3555,6 +3555,85 @@ suite('CopilotAgent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('createChat side chat preserves a local source turn id while forking from the concrete provider anchor', async () => {
|
||||
const sessionDataService = disposables.add(new TestSessionDataService());
|
||||
const agent = createTestAgent(disposables, { sessionDataService, copilotClient: new TestCopilotClient([]) });
|
||||
try {
|
||||
await agent.authenticate('https://api.github.com', 'token');
|
||||
const session = AgentSession.uri('copilotcli', 'side-local-peer');
|
||||
await agent.createSession({ session, workingDirectory: URI.file('/workspace') });
|
||||
const sourceTurn: Turn = {
|
||||
id: 't1',
|
||||
state: TurnState.Complete,
|
||||
message: { text: 'source', origin: { kind: MessageKind.User } },
|
||||
responseParts: [],
|
||||
usage: undefined,
|
||||
};
|
||||
const sourceContext = 'User request:\nsource\n\nAgent response:\nsource answer\n\n---\n\nUser request:\n!command';
|
||||
const injectedPrompt = injectSideChatContext('side', undefined, sourceContext);
|
||||
const sideTurn: Turn = {
|
||||
id: 't2',
|
||||
state: TurnState.Complete,
|
||||
message: { text: injectedPrompt, origin: { kind: MessageKind.User } },
|
||||
responseParts: [],
|
||||
usage: undefined,
|
||||
};
|
||||
const source = makeFakeChatSession(session, 'source-sdk', async () => [sourceTurn]);
|
||||
setDefaultSessionStub(agent, AgentSession.id(session), source.fake);
|
||||
const internals = agent as unknown as ChatInternals;
|
||||
let forkTurnId: string | undefined;
|
||||
internals._forkSdkChat = async (_client, _sourceEntry, turnId) => {
|
||||
forkTurnId = turnId;
|
||||
return { sessionId: 'side-sdk-id', inheritedTurnCount: 1 };
|
||||
};
|
||||
let messageReadCount = 0;
|
||||
let sideRecorder: IFakeChatRecorder | undefined;
|
||||
internals._createAgentSession = launchPlan => {
|
||||
const side = makeFakeChatSession(session, launchPlan.sessionId, async () => {
|
||||
messageReadCount++;
|
||||
return messageReadCount <= 2 ? [sourceTurn] : [sourceTurn, sideTurn];
|
||||
}, launchPlan.shellManager);
|
||||
sideRecorder = side.rec;
|
||||
return side.fake;
|
||||
};
|
||||
|
||||
const chatUri = URI.parse(buildChatUri(session, 'peer-side-local'));
|
||||
const result = await agent.chats.createChat(chatUri, {
|
||||
sideChat: {
|
||||
source: URI.parse(buildDefaultChatUri(session)),
|
||||
turnId: 'local-1',
|
||||
providerAnchorTurnId: 't1',
|
||||
sourceContext,
|
||||
},
|
||||
});
|
||||
await agent.chats.sendMessage(chatUri, 'side', undefined, undefined, 't2');
|
||||
await agent.chats.sendMessage(chatUri, 'follow-up', undefined, undefined, 't3');
|
||||
const turns = await agent.chats.getMessages(chatUri);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
forkTurnId,
|
||||
sentPrompts: sideRecorder?.sends.map(send => send.prompt),
|
||||
turns: turns.map(turn => turn.id),
|
||||
visiblePrompt: turns[0]?.message.text,
|
||||
sideChat: result ? JSON.parse(result.providerData!).sideChat : undefined,
|
||||
}, {
|
||||
forkTurnId: 't1',
|
||||
sentPrompts: [injectedPrompt, 'follow-up'],
|
||||
turns: ['t2'],
|
||||
visiblePrompt: 'side',
|
||||
sideChat: {
|
||||
source: buildDefaultChatUri(session),
|
||||
turnId: 'local-1',
|
||||
providerAnchorTurnId: 't1',
|
||||
inheritedTurnCount: 1,
|
||||
context: sourceContext,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await disposeAgent(agent);
|
||||
}
|
||||
});
|
||||
|
||||
test('sendMessage routes a turn to the targeted peer chat only', async () => {
|
||||
const agent = createTestAgent(disposables);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user