diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 3de8693ee6d39..aa8664b1d315c 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -902,6 +902,19 @@ export interface IAgent { */ getChats?(session: URI): Promise; + /** + * Resolves the backing agent-session id for a chat — the id that names the + * chat's on-disk log / session-state directory. For the default chat this + * is the owning session's id; for a peer chat it is the harness's + * per-chat session id, which differs from the chat's `ahp-chat` URI. + * + * Lets the agent service publish the id under + * {@link SessionState._meta} so clients can locate a specific chat's logs. + * Optional: harnesses that name every chat's logs by the session id (so the + * client can derive it from the chat resource) omit it. + */ + getChatSessionId?(session: URI, chat: URI): Promise; + /** * Called when the session's pending (steering) message changes. * The agent harness decides how to react — e.g. inject steering diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index c69bb0b56608f..601fb66ea51fc 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -822,6 +822,69 @@ export function withSessionGitState(meta: SessionMeta | undefined, gitState: ISe return Object.keys(next).length > 0 ? next : undefined; } +/** + * Reserved key under {@link SessionMeta} for the map of each chat's backing + * agent-session id (the id that names the chat's on-disk log directory). + * + * A peer chat is addressed by an `ahp-chat` URI whose path is the *owning + * session's* id and whose fragment is the AHP chat id — neither of which is + * the id the harness uses to name the chat's own log/session-state directory. + * Hosts that back chats by a distinct per-chat session id (e.g. the Copilot + * CLI harness) publish that id here, keyed by AHP chat id, so clients can + * locate a specific chat's logs without reconstructing host-private paths. + * + * The default chat is omitted: its log directory is already named by the + * owning session id, which clients can read straight off the chat resource. + */ +export const SESSION_META_CHAT_SESSION_IDS_KEY = 'chatSessionIds'; + +/** + * Reads the {@link SESSION_META_CHAT_SESSION_IDS_KEY} map from + * {@link SessionMeta}: AHP chat id → backing agent-session id. Returns + * `undefined` when absent or malformed; entries with a non-string value are + * dropped so a partial map still propagates. + * + * Like {@link readSessionGitState}, this takes the raw {@link SessionMeta} + * value rather than its parent {@link SessionState}: the sessions provider + * stores and reads a detached meta snapshot without retaining the owning state. + */ +export function readChatSessionIds(meta: SessionMeta | undefined): Record | undefined { + const value = meta?.[SESSION_META_CHAT_SESSION_IDS_KEY]; + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return undefined; + } + const result: Record = {}; + for (const [chatId, sessionId] of Object.entries(value as Record)) { + if (typeof sessionId === 'string') { + result[chatId] = sessionId; + } + } + return Object.keys(result).length > 0 ? result : undefined; +} + +/** + * Returns a new {@link SessionMeta} with `chatId` mapped to `sessionId` under + * {@link SESSION_META_CHAT_SESSION_IDS_KEY} (or with the entry removed when + * `sessionId` is `undefined`). Existing entries for other chats are preserved. + * Returns `undefined` if the result would be empty. + */ +export function withChatSessionId(meta: SessionMeta | undefined, chatId: string, sessionId: string | undefined): SessionMeta | undefined { + const existing = readChatSessionIds(meta); + const map: Record = { ...existing }; + if (sessionId !== undefined) { + map[chatId] = sessionId; + } else { + delete map[chatId]; + } + const next: { [key: string]: unknown } = { ...meta }; + if (Object.keys(map).length > 0) { + next[SESSION_META_CHAT_SESSION_IDS_KEY] = map; + } else { + delete next[SESSION_META_CHAT_SESSION_IDS_KEY]; + } + return Object.keys(next).length > 0 ? next : undefined; +} + // ---- RootState _meta accessors --------------------------------------------- /** diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index cddc187f5ed41..076ad4382f18c 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -29,7 +29,7 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } f import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, ResourceChangeType, ResourceType, ResourceWriteMode, type CreateResourceWatchParams, type CreateResourceWatchResult, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWatchState, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js'; import { ChangesSummary, MessageAttachmentKind, type MessageAttachment, type MessageResourceAttachment } from '../common/state/protocol/state.js'; import type { ChatPendingMessageSetAction, ChatTurnStartedAction } from '../common/state/protocol/actions.js'; -import { ResponsePartKind, SessionStatus, ToolCallStatus, ToolResultContentType, buildResourceWatchChannelUri, buildSubagentSessionUriPrefix, hostBuildInfoFromProduct, isAhpChatChannel, isSubagentSession, parseDefaultChatUri, parseResourceWatchChannelUri, parseSubagentSessionUri, readSessionGitState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js'; +import { ResponsePartKind, SessionStatus, ToolCallStatus, ToolResultContentType, buildResourceWatchChannelUri, buildSubagentSessionUriPrefix, hostBuildInfoFromProduct, isAhpChatChannel, isSubagentSession, parseChatUri, parseDefaultChatUri, parseResourceWatchChannelUri, parseSubagentSessionUri, readChatSessionIds, readSessionGitState, withChatSessionId, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js'; import { IProductService } from '../../product/common/productService.js'; import { AgentConfigurationService, IAgentConfigurationService } from './agentConfigurationService.js'; import { AgentHostTerminalManager, type IAgentHostTerminalManager } from './agentHostTerminalManager.js'; @@ -681,12 +681,20 @@ export class AgentService extends Disposable implements IAgentService { // subscribers once the chat can actually receive messages. await provider.createChat(session, chat, options); this._stateManager.addChat(sessionKey, chat.toString(), options?.title !== undefined ? { title: options.title } : undefined); + await this._publishChatSessionId(provider, session, chat); } async disposeChat(session: URI, chat: URI): Promise { const sessionKey = session.toString(); const provider = this._findProviderForSession(session); this._stateManager.removeChat(sessionKey, chat.toString()); + const parsed = parseChatUri(chat); + if (parsed) { + const current = this._stateManager.getSessionState(sessionKey)?._meta; + if (readChatSessionIds(current)?.[parsed.chatId] !== undefined) { + this._stateManager.setSessionMeta(sessionKey, withChatSessionId(current, parsed.chatId, undefined)); + } + } await provider?.disposeChat?.(session, chat); } @@ -1612,7 +1620,40 @@ export class AgentService extends Disposable implements IAgentService { } const title = await this._readPersistedChatTitle(session, chatUri); this._stateManager.restoreChat(session.toString(), chatUri.toString(), { title, turns: [...turns] }); + await this._publishChatSessionId(agent, session, chatUri); + } + } + + /** + * Publishes a peer chat's backing agent-session id under the owning + * session's `_meta` (keyed by AHP chat id) so clients can locate the + * chat's on-disk logs. No-op for the default chat (its logs are named by + * the session id, derivable from the chat resource) and for providers that + * do not report a distinct per-chat id. The read-modify-write of `_meta` + * is synchronous after the `getChatSessionId` await so it cannot clobber a + * concurrent git-state write. + */ + private async _publishChatSessionId(agent: IAgent, session: URI, chat: URI): Promise { + if (!agent.getChatSessionId) { + return; + } + const parsed = parseChatUri(chat); + if (!parsed) { + return; } + let sessionId: string | undefined; + try { + sessionId = await agent.getChatSessionId(session, chat); + } catch (err) { + this._logService.warn(`[AgentService] Failed to resolve chat session id for ${chat.toString()}: ${toErrorMessage(err)}`); + return; + } + if (!sessionId) { + return; + } + const sessionStr = session.toString(); + const current = this._stateManager.getSessionState(sessionStr)?._meta; + this._stateManager.setSessionMeta(sessionStr, withChatSessionId(current, parsed.chatId, sessionId)); } /** Reads a peer chat's persisted custom title, if any. */ diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 3f37722b1f33d..fe4124274aa23 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -1709,6 +1709,30 @@ export class CopilotAgent extends Disposable implements IAgent { return result; } + /** + * Resolves the backing SDK session id for a chat — the id that names the + * chat's `~/.copilot/session-state/` directory. The default chat is + * backed by the owning session's id; a peer chat is backed by a distinct + * `chatSdkId` minted in {@link createChat}, looked up from the in-memory + * session if live, otherwise from the persisted catalog. Returns + * `undefined` when a peer chat has no backing conversation. + */ + async getChatSessionId(session: URI, chat: URI): Promise { + if (isDefaultChatUri(chat)) { + return AgentSession.id(session); + } + const inMemory = this._chatSessions.get(chat.toString())?.sessionId; + if (inMemory) { + return inMemory; + } + const parsed = parseChatUri(chat); + if (!parsed) { + return undefined; + } + const persisted = await this._readPersistedChats(session); + return persisted.get(parsed.chatId)?.sdkSessionId; + } + /** * Returns the SDK-backed {@link CopilotAgentSession} for an additional peer * chat, resuming its persisted SDK conversation if it is not already in diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts index 63cb6ec94c939..59b7cce160394 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -28,7 +28,7 @@ import type { IAgentSubscription } from '../../../../../platform/agentHost/commo import { ResolveSessionConfigResult } from '../../../../../platform/agentHost/common/state/protocol/commands.js'; import { AgentCustomization, AgentSelection, ChangesSummary, type ChangesetFile, Customization, CustomizationType, ModelSelection, SessionStatus as ProtocolSessionStatus, RootConfigState, RootState, SessionActiveClient, SessionState, SessionSummary, type Changeset } from '../../../../../platform/agentHost/common/state/protocol/state.js'; import { ActionType, isChatAction, isSessionAction, NotificationType } from '../../../../../platform/agentHost/common/state/sessionActions.js'; -import { AgentInfo, buildChatUri, buildDefaultChatUri, isDefaultChatUri, parseChatUri, readSessionGitState, ROOT_STATE_URI, SessionMeta, StateComponents, type ChatSummary, type ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js'; +import { AgentInfo, buildChatUri, buildDefaultChatUri, DEFAULT_CHAT_ID, isDefaultChatUri, parseChatUri, readChatSessionIds, readSessionGitState, ROOT_STATE_URI, SessionMeta, StateComponents, type ChatSummary, type ISessionGitState } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; @@ -37,6 +37,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../pla import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; import { IAgentHostActiveClientService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.js'; import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js'; +import { buildSdkSessionResource } from '../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js'; import { IChatSendRequestOptions, IChatService } from '../../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { IChatSessionFileChange, IChatSessionFileChange2, IChatSessionsService } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, isChatPermissionLevel, type IChatDefaultConfiguration } from '../../../../../workbench/contrib/chat/common/constants.js'; @@ -760,6 +761,17 @@ export class AgentHostSessionAdapter extends Disposable implements ISession { return workspaceChanged; } + /** + * Resolves the backing agent-session id the host reported for one of this + * session's chats (keyed by AHP chat id under `_meta`). Returns `undefined` + * for the default chat or any chat the host did not report — callers fall + * back to the chat resource's own id, which already names the default + * chat's logs. + */ + getChatSdkSessionId(chatId: string): string | undefined { + return readChatSessionIds(this._meta)?.[chatId]; + } + updateChangesets(changesetsMetadata: readonly Changeset[] | undefined) { if (!changesetsMetadata) { return; @@ -2593,6 +2605,31 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement } } + /** + * Resolves a session-shaped resource carrying the host-reported backing + * session id for `chatResource`, so a specific chat's `events.jsonl` can be + * resolved through the same pipeline as a top-level session. Returns + * `undefined` when the chat is the default chat or the host did not report a + * distinct id — callers then fall back to `chatResource`, whose own path id + * already names the default chat's logs. + */ + getChatSdkSessionResource(chatResource: URI): URI | undefined { + const rawId = chatResource.path.startsWith('/') ? chatResource.path.substring(1) : chatResource.path; + if (!rawId) { + return undefined; + } + const cached = this._sessionCache.get(rawId); + if (!cached) { + return undefined; + } + const chatId = chatResource.fragment || DEFAULT_CHAT_ID; + const sdkSessionId = cached.getChatSdkSessionId(chatId); + if (!sdkSessionId || sdkSessionId === rawId) { + return undefined; + } + return buildSdkSessionResource(chatResource, sdkSessionId); + } + // -- Lazy session-state subscription seeding ----------------------------- /** diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/exportDebugLogsAction.ts b/src/vs/sessions/contrib/providers/agentHost/browser/exportDebugLogsAction.ts index 60ddb06ca4312..1967394c2ddd8 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/exportDebugLogsAction.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/exportDebugLogsAction.ts @@ -44,11 +44,29 @@ export class ExportAgentHostDebugLogsAction extends Action2 { const activeAgentHostSession = isAgentHostSession(activeSession, sessionsProvidersService) ? activeSession : undefined; const sessionForEvents = activeAgentHostSession ?? getMostRecentAgentHostSession(sessionsManagementService.getSessions(), sessionsProvidersService); - const activeSessionContext: IActiveAgentHostSessionForExport | undefined = sessionForEvents + // Sessions can contain multiple chats and the events file is per-chat, + // so prefer the focused chat tab's resource for the active session. + const focusedChat = activeAgentHostSession?.activeChat.get(); + const chatResource = focusedChat?.resource ?? sessionForEvents?.resource; + + // A peer chat's events.jsonl (and its shared-log lines) live under a + // host-private backing session id that the chat resource does not + // encode. Ask the provider to map the focused chat to a session-shaped + // resource carrying that id so the export collects the right files; + // fall back to the chat resource for the default chat. + let eventsResource = chatResource; + if (activeAgentHostSession && chatResource) { + const provider = sessionsProvidersService.getProvider(activeAgentHostSession.providerId); + if (provider instanceof BaseAgentHostSessionsProvider) { + eventsResource = provider.getChatSdkSessionResource(chatResource) ?? chatResource; + } + } + + const activeSessionContext: IActiveAgentHostSessionForExport | undefined = sessionForEvents && eventsResource ? { - resource: sessionForEvents.resource, - title: activeAgentHostSession?.title.get(), - isLocal: sessionForEvents.resource.scheme.startsWith('agent-host-'), + resource: eventsResource, + title: focusedChat?.title.get() ?? activeAgentHostSession?.title.get(), + isLocal: eventsResource.scheme.startsWith('agent-host-'), } : undefined; @@ -56,7 +74,7 @@ export class ExportAgentHostDebugLogsAction extends Action2 { } } -function isAgentHostSession(session: ISession | undefined, sessionsProvidersService: ISessionsProvidersService): session is ISession { +function isAgentHostSession(session: T | undefined, sessionsProvidersService: ISessionsProvidersService): session is T { return !!session && sessionsProvidersService.getProvider(session.providerId) instanceof BaseAgentHostSessionsProvider; } diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/openSessionEventsFileActions.ts b/src/vs/sessions/contrib/providers/agentHost/browser/openSessionEventsFileActions.ts index 4b3c7c06ba017..66e1ab750d406 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/openSessionEventsFileActions.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/openSessionEventsFileActions.ts @@ -11,13 +11,23 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { openCopilotCliStateFile } from '../../../../../workbench/contrib/chat/browser/actions/openCopilotCliStateFileAction.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; +import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js'; +import { BaseAgentHostSessionsProvider } from './baseAgentHostSessionsProvider.js'; import { IsAgentHostSession } from './agentHostSkillButtons.js'; /** * Sessions-app variant of "Open Copilot CLI State File". Uses the Agents - * window's `ISessionsService.activeSession` to find the active - * Copilot CLI session, then defers to the shared workbench helper for - * the actual resolution and editor opening. + * window's `ISessionsService.activeSession` to find the focused chat tab + * of the active Copilot CLI session, then defers to the shared workbench + * helper for the actual resolution and editor opening. + * + * Sessions can contain multiple chats; the state file is per-chat, so we + * resolve the focused chat's resource rather than the session's main chat. + * A peer chat's `events.jsonl` lives under a host-private backing session id + * that the chat resource does not encode, so we first ask the provider to map + * the focused chat to a session-shaped resource carrying that id (falling back + * to the chat resource for the default chat, whose path id already names its + * logs). * * The vscode workbench registers a separate action class * (`OpenCopilotCliStateFileAction` in @@ -40,7 +50,18 @@ export class OpenSessionEventsFileAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const sessionsService = accessor.get(ISessionsService); - const sessionResource = sessionsService.activeSession.get()?.resource; + const sessionsProvidersService = accessor.get(ISessionsProvidersService); + const activeSession = sessionsService.activeSession.get(); + const focusedChatResource = activeSession?.activeChat.get()?.resource ?? activeSession?.resource; + + let sessionResource = focusedChatResource; + if (activeSession && focusedChatResource) { + const provider = sessionsProvidersService.getProvider(activeSession.providerId); + if (provider instanceof BaseAgentHostSessionsProvider) { + sessionResource = provider.getChatSdkSessionResource(focusedChatResource) ?? focusedChatResource; + } + } + await openCopilotCliStateFile(accessor, sessionResource); } } diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts index 8bb8f2447a525..0d9ee5fc13561 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/openSessionEventsFile.test.ts @@ -11,7 +11,8 @@ import { IRemoteAgentHostConnectionInfo, RemoteAgentHostConnectionStatus } from import { IsSessionsWindowContext } from '../../../../../../workbench/common/contextkeys.js'; import { OpenCopilotCliStateFileAction } from '../../../../../../workbench/contrib/chat/browser/actions/openCopilotCliStateFileAction.js'; import { ChatContextKeys } from '../../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { buildLocalCopilotLogsUri, buildRemoteCopilotLogsUri, getCopilotCliSessionRawId, resolveEventsUri } from '../../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js'; +import { buildLocalCopilotLogsUri, buildRemoteCopilotLogsUri, buildSdkSessionResource, getCopilotCliSessionRawId, resolveEventsUri } from '../../../../../../workbench/contrib/chat/browser/copilotCliEventsUri.js'; +import { readChatSessionIds, withChatSessionId } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { IsAgentHostSession } from '../../browser/agentHostSkillButtons.js'; import { OpenSessionEventsFileAction } from '../../browser/openSessionEventsFileActions.js'; @@ -146,4 +147,41 @@ suite('openSessionEventsFile resolveEventsUri', () => { const result = resolveEventsUri(undefined, userHome, () => undefined); assert.deepStrictEqual(result, { kind: 'no-session' }); }); + + test('peer chat resolves via its backing sdk session id, not the main session path id', () => { + // A peer chat resource carries the main session id in its path and the + // AHP chat id in its fragment; neither names the chat's events.jsonl. + const peerChat = URI.from({ scheme: 'agent-host-copilotcli', path: '/main-session', fragment: 'chat-2' }); + const sdkResource = buildSdkSessionResource(peerChat, 'sdk-789'); + const result = resolveEventsUri(sdkResource, userHome, () => undefined); + assert.deepStrictEqual( + { resource: result.kind === 'ok' ? result.resource.toString() : undefined }, + { resource: 'file:///home/me/.copilot/session-state/sdk-789/events.jsonl' }, + ); + }); + + test('peer chat on a remote keeps the chat scheme so resolution stays remote', () => { + const conn = makeRemoteConn('localhost:4321', '/home/remote'); + const peerChat = URI.from({ scheme: 'remote-localhost__4321-copilotcli', path: '/main', fragment: 'chat-2' }); + const sdkResource = buildSdkSessionResource(peerChat, 'sdk-789'); + const result = resolveEventsUri(sdkResource, userHome, authority => authority === 'localhost__4321' ? conn : undefined); + assert.deepStrictEqual( + { resource: result.kind === 'ok' ? result.resource.toString() : undefined }, + { resource: 'vscode-agent-host://localhost__4321/home/remote/.copilot/session-state/sdk-789/events.jsonl?_ah%3DeyJzY2hlbWUiOiJmaWxlIn0' }, + ); + }); + + test('chat session id _meta accessors round-trip and merge per chat', () => { + const meta1 = withChatSessionId(undefined, 'chat-2', 'sdk-789'); + const meta2 = withChatSessionId(meta1, 'chat-3', 'sdk-abc'); + assert.deepStrictEqual({ + afterFirst: readChatSessionIds(meta1), + afterSecond: readChatSessionIds(meta2), + missing: readChatSessionIds(undefined), + }, { + afterFirst: { 'chat-2': 'sdk-789' }, + afterSecond: { 'chat-2': 'sdk-789', 'chat-3': 'sdk-abc' }, + missing: undefined, + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts b/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts index 3ca2fbabfb01e..6b756f441ae97 100644 --- a/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts +++ b/src/vs/workbench/contrib/chat/browser/copilotCliEventsUri.ts @@ -101,6 +101,18 @@ export function getCopilotCliSessionRawId(sessionResource: URI | undefined): str return getRawSessionId(sessionResource); } +/** + * Builds a session-shaped resource that carries `rawSessionId` in its path, + * preserving the chat resource's scheme (local/remote authority) and dropping + * the chat fragment. Used to resolve a specific chat's `events.jsonl` through + * the same {@link resolveEventsUri} pipeline as a top-level session, by + * substituting the chat's host-reported backing session id for the chat + * resource's own path id. + */ +export function buildSdkSessionResource(chatResource: URI, rawSessionId: string): URI { + return URI.from({ scheme: chatResource.scheme, path: `/${rawSessionId}` }); +} + export type ResolveEventsUriResult = | { readonly kind: 'ok'; readonly resource: URI } | { readonly kind: 'no-session' }