Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/sessionParser/claudeCodeSessionService.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45/**6* Claude Code Session Service7*8* This service provides access to Claude Code chat sessions using the9* `@anthropic-ai/claude-agent-sdk` session APIs. It handles:10* - Listing sessions via `listSessions()`11* - Loading full session content via `getSessionInfo()` + `getSessionMessages()`12* - Subagent loading via `listSubagents()` + `getSubagentMessages()`13*/1415import type { CancellationToken } from 'vscode';16import { ILogService } from '../../../../../platform/log/common/logService';17import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';18import { createServiceIdentifier } from '../../../../../util/common/services';19import { basename } from '../../../../../util/vs/base/common/resources';20import { URI } from '../../../../../util/vs/base/common/uri';21import { IAgentSessionsWorkspace } from '../../../../chatSessions/common/agentSessionsWorkspace';22import { IFolderRepositoryManager } from '../../../../chatSessions/common/folderRepositoryManager';23import { ClaudeSessionUri } from '../../common/claudeSessionUri';24import { IClaudeCodeSdkService } from '../claudeCodeSdkService';25import { getProjectFolders } from '../claudeProjectFolders';26import {27IClaudeCodeSession,28IClaudeCodeSessionInfo,29ISubagentSession,30} from './claudeSessionSchema';31import { buildClaudeCodeSession, sdkSessionInfoToSessionInfo, sdkSubagentMessagesToSubagentSession } from './sdkSessionAdapter';32import { toErrorMessage } from '../../../../../util/common/errorMessage';3334// #region Service Interface3536export const IClaudeCodeSessionService = createServiceIdentifier<IClaudeCodeSessionService>('IClaudeCodeSessionService');3738/**39* Service to load and manage Claude Code chat sessions.40*/41export interface IClaudeCodeSessionService {42readonly _serviceBrand: undefined;4344/**45* Get lightweight metadata for all sessions in the current workspace.46* This is optimized for listing sessions without loading full content.47*/48getAllSessions(token: CancellationToken): Promise<readonly IClaudeCodeSessionInfo[]>;4950/**51* Get a specific session with full content by its resource URI.52* This loads the complete message history and subagents.53*/54getSession(resource: URI, token: CancellationToken): Promise<IClaudeCodeSession | undefined>;55}5657// #endregion5859// #region Service Implementation6061export class ClaudeCodeSessionService implements IClaudeCodeSessionService {62declare _serviceBrand: undefined;6364constructor(65@IClaudeCodeSdkService private readonly _sdkService: IClaudeCodeSdkService,66@ILogService private readonly _logService: ILogService,67@IWorkspaceService private readonly _workspace: IWorkspaceService,68@IFolderRepositoryManager private readonly _folderRepositoryManager: IFolderRepositoryManager,69@IAgentSessionsWorkspace private readonly _agentSessionsWorkspace: IAgentSessionsWorkspace,70) { }7172/**73* Get lightweight metadata for all sessions in the current workspace.74* Delegates to the SDK's `listSessions()` and converts results.75*/76async getAllSessions(token: CancellationToken): Promise<readonly IClaudeCodeSessionInfo[]> {77if (this._agentSessionsWorkspace.isAgentSessionsWorkspace) {78try {79const sdkSessions = await this._sdkService.listSessions();80return sdkSessions.map(sdkInfo => sdkSessionInfoToSessionInfo(sdkInfo));81} catch (e) {82this._logService.debug(`[ClaudeCodeSessionService] Failed to list all sessions: ${e}`);83return [];84}85}8687const items: IClaudeCodeSessionInfo[] = [];88const projectFolders = await this._getProjectFolders();8990for (const { slug, folderUri } of projectFolders) {91if (token.isCancellationRequested) {92return items;93}9495const folderName = basename(folderUri);9697try {98const sdkSessions = await this._sdkService.listSessions(folderUri.fsPath);99for (const sdkInfo of sdkSessions) {100items.push(sdkSessionInfoToSessionInfo(sdkInfo, folderName));101}102} catch (e) {103this._logService.debug(`[ClaudeCodeSessionService] Failed to list sessions for slug ${slug}: ${e}`);104}105}106107return items;108}109110/**111* Get a specific session with full content by its resource URI.112* Uses SDK APIs for metadata, messages, and subagent transcripts.113*/114async getSession(resource: URI, token: CancellationToken): Promise<IClaudeCodeSession | undefined> {115const sessionId = ClaudeSessionUri.getSessionId(resource);116117if (this._agentSessionsWorkspace.isAgentSessionsWorkspace) {118try {119const info = await this._sdkService.getSessionInfo(sessionId);120if (!info) {121return undefined;122}123124const messages = await this._sdkService.getSessionMessages(sessionId, info.cwd);125if (token.isCancellationRequested) {126return undefined;127}128129const subagents = await this._loadSubagents(sessionId, info.cwd, token);130return buildClaudeCodeSession(info, messages, subagents);131} catch (e) {132this._logService.debug(`[ClaudeCodeSessionService] Failed to load session ${sessionId}: ${e}`);133return undefined;134}135}136137const projectFolders = await this._getProjectFolders();138139for (const { slug, folderUri } of projectFolders) {140if (token.isCancellationRequested) {141return undefined;142}143144const dir = folderUri.fsPath;145146try {147const info = await this._sdkService.getSessionInfo(sessionId, dir);148if (!info) {149continue;150}151152const sessionDir = info.cwd ?? dir;153const messages = await this._sdkService.getSessionMessages(sessionId, sessionDir);154if (token.isCancellationRequested) {155return undefined;156}157158const subagents = await this._loadSubagents(sessionId, sessionDir, token);159160const folderName = basename(folderUri);161return buildClaudeCodeSession(info, messages, subagents, folderName);162} catch (e) {163this._logService.debug(`[ClaudeCodeSessionService] Failed to load session ${sessionId} from slug ${slug}: ${e}`);164}165}166167return undefined;168}169170// #region Directory Discovery171172/**173* Get the project directory slugs to scan for sessions, along with their174* original folder URIs (needed for badge display).175*/176private _getProjectFolders() {177return getProjectFolders(this._workspace, this._folderRepositoryManager);178}179180// #endregion181182// #region Subagent Loading183184private async _loadSubagents(185sessionId: string,186cwd: string | undefined,187token: CancellationToken,188): Promise<readonly ISubagentSession[]> {189let agentIds: string[];190try {191agentIds = await this._sdkService.listSubagents(sessionId, cwd ? { dir: cwd } : undefined);192} catch (error) {193this._logService.warn(`[ClaudeCodeSessionService] listSubagents failed: ${toErrorMessage(error)}`);194return [];195}196197if (agentIds.length === 0 || token.isCancellationRequested) {198return [];199}200201const results = await Promise.allSettled(202agentIds.map(agentId => this._loadSubagentFromSdk(sessionId, agentId, cwd))203);204205if (token.isCancellationRequested) {206return [];207}208209const subagents: ISubagentSession[] = [];210for (const r of results) {211if (r.status === 'fulfilled' && r.value !== null) {212subagents.push(r.value);213}214}215216subagents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());217218return subagents;219}220221private async _loadSubagentFromSdk(222sessionId: string,223agentId: string,224cwd: string | undefined,225): Promise<ISubagentSession | null> {226try {227const messages = await this._sdkService.getSubagentMessages(sessionId, agentId, cwd ? { dir: cwd } : undefined);228return sdkSubagentMessagesToSubagentSession(agentId, messages);229} catch (error) {230this._logService.warn(`[ClaudeCodeSessionService] Failed to load subagent ${agentId} for session ${sessionId}: ${toErrorMessage(error)}`);231return null;232}233}234235// #endregion236}237238// #endregion239240241