Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/extension/chatSessions/vscode-node/chatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib
));

const sessionItemProvider = this._register(claudeAgentInstaService.createInstance(ClaudeChatSessionItemProvider));
this._register(vscode.chat.registerChatSessionItemProvider(ClaudeChatSessionItemProvider.claudeSessionType, sessionItemProvider));

const claudeAgentManager = this._register(claudeAgentInstaService.createInstance(ClaudeAgentManager));
const chatSessionContentProvider = this._register(claudeAgentInstaService.createInstance(ClaudeChatSessionContentProvider));
chatSessionContentProvider.setSessionIdResolver(resource => sessionItemProvider.getSessionId(resource));
const slashCommandService = this._register(claudeAgentInstaService.createInstance(ClaudeSlashCommandService));
const claudeChatSessionParticipant = new ClaudeChatSessionParticipant(ClaudeChatSessionItemProvider.claudeSessionType, claudeAgentManager, sessionItemProvider, chatSessionContentProvider, slashCommandService);
const chatParticipant = vscode.chat.createChatParticipant(ClaudeChatSessionItemProvider.claudeSessionType, claudeChatSessionParticipant.createHandler());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
// Track the last known option values for each session to detect actual changes
private readonly _lastKnownOptions = new Map<string, { modelId?: string; permissionMode?: PermissionMode }>();

private _sessionIdResolver: ((resource: vscode.Uri) => string | undefined) | undefined;

constructor(
@IClaudeCodeSessionService private readonly sessionService: IClaudeCodeSessionService,
@IClaudeCodeModels private readonly claudeCodeModels: IClaudeCodeModels,
Expand Down Expand Up @@ -82,6 +84,30 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
super.dispose();
}

/**
* Sets the resolver function for mapping resources to Claude session IDs.
* This is needed to handle overridden resources (e.g., untitled sessions).
*/
public setSessionIdResolver(resolver: (resource: vscode.Uri) => string | undefined): void {
this._sessionIdResolver = resolver;
}

/**
* Resolves a resource to a Claude session ID.
* Uses the custom resolver if set, otherwise falls back to ClaudeSessionUri.getId.
*/
private _resolveSessionId(resource: vscode.Uri): string | undefined {
if (this._sessionIdResolver) {
return this._sessionIdResolver(resource);
}
// Fallback to direct URI parsing
try {
return ClaudeSessionUri.getId(resource);
} catch {
return undefined;
}
}

/**
* Gets the model ID for a session, delegating to state service
*/
Expand Down Expand Up @@ -134,7 +160,10 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
}

async provideHandleOptionsChange(resource: vscode.Uri, updates: ReadonlyArray<vscode.ChatSessionOptionUpdate>, _token: vscode.CancellationToken): Promise<void> {
const sessionId = ClaudeSessionUri.getId(resource);
const sessionId = this._resolveSessionId(resource);
if (!sessionId) {
return;
}
for (const update of updates) {
if (update.optionId === MODELS_OPTION_ID) {
// Update last known first so the event listener won't fire back to UI
Expand All @@ -150,16 +179,16 @@ export class ClaudeChatSessionContentProvider extends Disposable implements vsco
}

async provideChatSessionContent(sessionResource: vscode.Uri, token: vscode.CancellationToken): Promise<vscode.ChatSession> {
const sessionId = ClaudeSessionUri.getId(sessionResource);
const sessionId = this._resolveSessionId(sessionResource);
const existingSession = await this.sessionService.getSession(sessionResource, token);
const toolContext = this._createToolContext();
const history = existingSession ?
this._buildChatHistory(existingSession, toolContext) :
[];

// Get model and permission mode from state service (queries session if active)
const model = await this.sessionStateService.getModelIdForSession(sessionId);
const permissionMode = this.sessionStateService.getPermissionModeForSession(sessionId);
const model = sessionId ? await this.sessionStateService.getModelIdForSession(sessionId) : undefined;
const permissionMode = sessionId ? this.sessionStateService.getPermissionModeForSession(sessionId) : 'default';

const options: Record<string, string> = {};
if (model) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,102 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Emitter, Event } from '../../../util/vs/base/common/event';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { IClaudeCodeSessionService } from '../../agents/claude/node/claudeCodeSessionService';

/**
* Chat session item provider for Claude Code.
* Chat session item controller for Claude Code.
* Reads sessions from ~/.claude/projects/<folder-slug>/, where each file name is a session id (GUID).
*/
export class ClaudeChatSessionItemProvider extends Disposable implements vscode.ChatSessionItemProvider {
export class ClaudeChatSessionItemProvider extends Disposable {

public static claudeSessionType = 'claude-code';

private readonly _onDidChangeChatSessionItems = this._register(new Emitter<void>());
public readonly onDidChangeChatSessionItems: Event<void> = this._onDidChangeChatSessionItems.event;
private readonly controller: vscode.ChatSessionItemController;

private readonly _onDidCommitChatSessionItem = this._register(new Emitter<{ original: vscode.ChatSessionItem; modified: vscode.ChatSessionItem }>());
public readonly onDidCommitChatSessionItem: Event<{ original: vscode.ChatSessionItem; modified: vscode.ChatSessionItem }> = this._onDidCommitChatSessionItem.event;
/**
* Maps Claude session IDs to the original chat session resource.
* Used to preserve the link between an untitled session and its Claude-backed session.
*/
private readonly sessionResourceOverrides = new Map<string, vscode.Uri>();

constructor(
@IClaudeCodeSessionService private readonly claudeCodeSessionService: IClaudeCodeSessionService
) {
super();

this.controller = vscode.chat.createChatSessionItemController(
ClaudeChatSessionItemProvider.claudeSessionType,
// refreshHandler returns immediately - it never triggers work that would cause a loop.
// Items are already in the collection when VS Code reads them.
() => this.refresh()
);
this._register(this.controller);
}

public refresh(): void {
this._onDidChangeChatSessionItems.fire();
/**
* Adds a single session item to the collection.
* Use this instead of refresh() when you know the specific session that was created.
*/
public addSession(sessionId: string, label: string, timestamp: Date, originalResource?: vscode.Uri): void {
if (originalResource) {
this.sessionResourceOverrides.set(sessionId, originalResource);
}

const resource = originalResource ?? ClaudeSessionUri.forSessionId(sessionId);
const item = this.controller.createChatSessionItem(resource, label);
item.tooltip = `Claude Code session: ${label}`;
item.timing = {
created: timestamp.getTime(),
startTime: timestamp.getTime(),
};
item.iconPath = new vscode.ThemeIcon('star-add');

this.controller.items.add(item);
}

public swap(original: vscode.ChatSessionItem, modified: vscode.ChatSessionItem): void {
this._onDidCommitChatSessionItem.fire({ original, modified });
/**
* Gets the Claude session ID for a given resource.
* Handles both direct Claude session URIs and overridden resources.
*/
public getSessionId(resource: vscode.Uri): string | undefined {
// First check if this is a direct Claude session URI
if (resource.scheme === ClaudeChatSessionItemProvider.claudeSessionType) {
return resource.path.slice(1);
}

// Otherwise, look up in the overrides map (reverse lookup)
for (const [sessionId, overrideResource] of this.sessionResourceOverrides) {
if (overrideResource.toString() === resource.toString()) {
return sessionId;
}
}

return undefined;
}

public async provideChatSessionItems(token: vscode.CancellationToken): Promise<vscode.ChatSessionItem[]> {
const sessions = await this.claudeCodeSessionService.getAllSessions(token);
const diskSessions = sessions.map(session => ({
resource: ClaudeSessionUri.forSessionId(session.id),
label: session.label,
tooltip: `Claude Code session: ${session.label}`,
timing: {
public async refresh(): Promise<void> {
const sessions = await this.claudeCodeSessionService.getAllSessions(CancellationToken.None);
const items = sessions.map(session => {
// Use the overridden resource if available, otherwise use the Claude session URI
const resource = this.sessionResourceOverrides.get(session.id)
?? ClaudeSessionUri.forSessionId(session.id);

const item = this.controller.createChatSessionItem(
resource,
session.label
);
item.tooltip = `Claude Code session: ${session.label}`;
item.timing = {
created: session.timestamp.getTime(),
startTime: session.timestamp.getTime(),
},
iconPath: new vscode.ThemeIcon('star-add')
} satisfies vscode.ChatSessionItem));
};
item.iconPath = new vscode.ThemeIcon('star-add');
return item;
});

return diskSessions;
this.controller.items.replace(items);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ChatExtendedRequestHandler } from 'vscode';
import { ClaudeAgentManager } from '../../agents/claude/node/claudeCodeAgent';
import { IClaudeSlashCommandService } from '../../agents/claude/vscode-node/claudeSlashCommandService';
import { ClaudeChatSessionContentProvider } from './claudeChatSessionContentProvider';
import { ClaudeChatSessionItemProvider, ClaudeSessionUri } from './claudeChatSessionItemProvider';
import { ClaudeChatSessionItemProvider } from './claudeChatSessionItemProvider';

// Import the tool permission handlers
import { PermissionMode } from '@anthropic-ai/claude-agent-sdk';
Expand Down Expand Up @@ -44,19 +44,21 @@ export class ClaudeChatSessionParticipant {
};
const { chatSessionContext } = context;
if (chatSessionContext) {
const sessionId = ClaudeSessionUri.getId(chatSessionContext.chatSessionItem.resource);
const modelId = await this.contentProvider.getModelIdForSession(sessionId);
const permissionMode = this.contentProvider.getPermissionModeForSession(sessionId);
const sessionId = this.sessionItemProvider.getSessionId(chatSessionContext.chatSessionItem.resource);
const modelId = sessionId ? await this.contentProvider.getModelIdForSession(sessionId) : undefined;
const permissionMode = sessionId ? this.contentProvider.getPermissionModeForSession(sessionId) : undefined;

if (chatSessionContext.isUntitled) {
if (chatSessionContext.isUntitled || !sessionId) {
/* New, empty session */
const claudeSessionId = await create(modelId, permissionMode);
if (claudeSessionId) {
// Tell UI to replace with claude-backed session
this.sessionItemProvider.swap(chatSessionContext.chatSessionItem, {
resource: ClaudeSessionUri.forSessionId(claudeSessionId),
label: request.prompt ?? 'Claude Code'
});
// Add the new session directly to the controller
this.sessionItemProvider.addSession(
claudeSessionId,
request.prompt,
new Date(),
chatSessionContext.chatSessionItem.resource
);
}
return {};
}
Expand Down