Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ In multi-root and empty workspaces, a folder picker option appears in the chat s

### Session Metadata Enrichment

Each Claude session item carries metadata that drives the Sessions view UI (button visibility, status indicators). The `ClaudeChatSessionItemController._buildSessionMetadata()` method enriches session items with git repository state:
Each Claude session item carries metadata that drives the Sessions view UI (button visibility, status indicators). The `ClaudeChatSessionItemController._buildSessionMetadata()` method enriches session items with git repository state.

**Workspace Trust:** Session metadata and git change detection are gated on workspace trust via `IWorkspaceService.isResourceTrusted()`. For untrusted working directories, `_buildSessionMetadata()` returns only the `workingDirectoryPath` (no git data), and `getWorkspaceChanges()` is skipped entirely. The trust check is resolved once in `_createClaudeChatSessionItem` and passed into `_buildSessionMetadata` to avoid redundant calls. When trusted, the metadata fetch and workspace changes fetch run concurrently via `Promise.all`.

| Field | Type | Description |
|-------|------|-------------|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ Each session in the list displays:

Sessions are sorted by recency — the most recent session appears at the top. In the dedicated sidebar, they're also grouped by time period.

> **Note:** Git metadata (branch name, change stats, action buttons) and workspace change detection require the session's working directory to be in a **trusted workspace**. If the folder is untrusted, sessions still appear in the list but without git-related information or actions.

#### Git Action Buttons

When a session has a git repository, action buttons appear in the Changes view based on the repository state:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface IClaudeCodeSdkService {
* @param dir Workspace/project directory path (the SDK resolves this to the session storage location internally)
* @returns Array of session info objects
*/
listSessions(dir: string): Promise<SDKSessionInfo[]>;
listSessions(dir?: string): Promise<SDKSessionInfo[]>;

/**
* Gets detailed information for a specific session
Expand Down Expand Up @@ -98,7 +98,7 @@ export class ClaudeCodeSdkService implements IClaudeCodeSdkService {
return query(options);
}

public async listSessions(dir: string): Promise<SDKSessionInfo[]> {
public async listSessions(dir?: string): Promise<SDKSessionInfo[]> {
const { listSessions } = await this._loadSdk();
return listSessions({ dir });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { IWorkspaceService } from '../../../../../platform/workspace/common/work
import { createServiceIdentifier } from '../../../../../util/common/services';
import { basename } from '../../../../../util/vs/base/common/resources';
import { URI } from '../../../../../util/vs/base/common/uri';
import { IAgentSessionsWorkspace } from '../../../../chatSessions/common/agentSessionsWorkspace';
import { IFolderRepositoryManager } from '../../../../chatSessions/common/folderRepositoryManager';
import { ClaudeSessionUri } from '../../common/claudeSessionUri';
import { IClaudeCodeSdkService } from '../claudeCodeSdkService';
Expand Down Expand Up @@ -76,13 +77,24 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
@ILogService private readonly _logService: ILogService,
@IWorkspaceService private readonly _workspace: IWorkspaceService,
@IFolderRepositoryManager private readonly _folderRepositoryManager: IFolderRepositoryManager,
@IAgentSessionsWorkspace private readonly _agentSessionsWorkspace: IAgentSessionsWorkspace,
) { }

/**
* Get lightweight metadata for all sessions in the current workspace.
* Delegates to the SDK's `listSessions()` and converts results.
*/
async getAllSessions(token: CancellationToken): Promise<readonly IClaudeCodeSessionInfo[]> {
if (this._agentSessionsWorkspace.isAgentSessionsWorkspace) {
try {
const sdkSessions = await this._sdkService.listSessions();
return sdkSessions.map(sdkInfo => sdkSessionInfoToSessionInfo(sdkInfo));
} catch (e) {
this._logService.debug(`[ClaudeCodeSessionService] Failed to list all sessions: ${e}`);
return [];
}
}

const items: IClaudeCodeSessionInfo[] = [];
const projectFolders = await this._getProjectFolders();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../util/
import { CancellationToken, CancellationTokenSource } from '../../../../../../util/vs/base/common/cancellation';
import { URI } from '../../../../../../util/vs/base/common/uri';
import { IInstantiationService } from '../../../../../../util/vs/platform/instantiation/common/instantiation';
import { FolderRepositoryMRUEntry, IFolderRepositoryManager } from '../../../../../chatSessions/common/folderRepositoryManager';
import { IFolderRepositoryManager, FolderRepositoryMRUEntry } from '../../../../../chatSessions/common/folderRepositoryManager';
import { IAgentSessionsWorkspace } from '../../../../../chatSessions/common/agentSessionsWorkspace';
import { createExtensionUnitTestingServices } from '../../../../../test/node/services';
import { IClaudeCodeSdkService } from '../../claudeCodeSdkService';
import { computeFolderSlug } from '../../claudeProjectFolders';
Expand Down Expand Up @@ -103,6 +104,7 @@ describe('ClaudeCodeSessionService', () => {
const workspaceService = store.add(new TestWorkspaceService([folderUri]));
testingServiceCollection.set(IWorkspaceService, workspaceService);
testingServiceCollection.define(IFolderRepositoryManager, new MockFolderRepositoryManager());
testingServiceCollection.set(IAgentSessionsWorkspace, { _serviceBrand: undefined, isAgentSessionsWorkspace: false });

const accessor = testingServiceCollection.createTestingAccessor();
mockFs = accessor.get(IFileSystemService) as MockFileSystemService;
Expand Down Expand Up @@ -273,6 +275,54 @@ describe('ClaudeCodeSessionService', () => {

expect(sessions).toHaveLength(0);
});

describe('when in agent sessions workspace', () => {
let agentSessionsService: ClaudeCodeSessionService;
let agentSessionsSdkService: MockClaudeCodeSdkService;

beforeEach(() => {
agentSessionsSdkService = new MockClaudeCodeSdkService();
const sc = store.add(createExtensionUnitTestingServices(store));
sc.set(IFileSystemService, new MockFileSystemService());
sc.set(IClaudeCodeSdkService, agentSessionsSdkService);
sc.set(IWorkspaceService, store.add(new TestWorkspaceService([])));
sc.define(IFolderRepositoryManager, new MockFolderRepositoryManager());
sc.set(IAgentSessionsWorkspace, { _serviceBrand: undefined, isAgentSessionsWorkspace: true });

agentSessionsService = sc.createTestingAccessor().get(IInstantiationService).createInstance(ClaudeCodeSessionService);
});

it('lists all sessions without a dir argument', async () => {
agentSessionsSdkService.mockSessions = [
createSdkSessionInfo({ sessionId: 'global-1', summary: 'Global session' }),
createSdkSessionInfo({ sessionId: 'global-2', summary: 'Another session' }),
];

const sessions = await agentSessionsService.getAllSessions(CancellationToken.None);

expect(sessions).toHaveLength(2);
expect(sessions[0].id).toBe('global-1');
expect(sessions[1].id).toBe('global-2');
});

it('returns empty array when SDK throws', async () => {
agentSessionsSdkService.listSessions = async () => { throw new Error('SDK failure'); };

const sessions = await agentSessionsService.getAllSessions(CancellationToken.None);

expect(sessions).toHaveLength(0);
});

it('does not set folderName on sessions', async () => {
agentSessionsSdkService.mockSessions = [
createSdkSessionInfo({ sessionId: 'no-folder' }),
];

const sessions = await agentSessionsService.getAllSessions(CancellationToken.None);

expect(sessions[0].folderName).toBeUndefined();
});
});
});

// #endregion
Expand Down Expand Up @@ -471,6 +521,7 @@ describe('ClaudeCodeSessionService', () => {
const emptyWorkspaceService = store.add(new TestWorkspaceService([]));
noWorkspaceTestingServiceCollection.set(IWorkspaceService, emptyWorkspaceService);
noWorkspaceTestingServiceCollection.define(IFolderRepositoryManager, noWorkspaceFolderManager);
noWorkspaceTestingServiceCollection.set(IAgentSessionsWorkspace, { _serviceBrand: undefined, isAgentSessionsWorkspace: false });

noWorkspaceFolderManager.setMRUEntries([
{ folder: mruFolder, repository: undefined, lastAccessed: Date.now() },
Expand Down Expand Up @@ -500,6 +551,7 @@ describe('ClaudeCodeSessionService', () => {
noMruServiceCollection.set(IClaudeCodeSdkService, new MockClaudeCodeSdkService());
noMruServiceCollection.set(IWorkspaceService, store.add(new TestWorkspaceService([])));
noMruServiceCollection.define(IFolderRepositoryManager, noWorkspaceFolderManager);
noMruServiceCollection.set(IAgentSessionsWorkspace, { _serviceBrand: undefined, isAgentSessionsWorkspace: false });

const accessor = noMruServiceCollection.createTestingAccessor();
const noMruService = accessor.get(IInstantiationService).createInstance(ClaudeCodeSessionService);
Expand All @@ -526,6 +578,7 @@ describe('ClaudeCodeSessionService', () => {
multiMruServiceCollection.set(IClaudeCodeSdkService, multiSdkService);
multiMruServiceCollection.set(IWorkspaceService, store.add(new TestWorkspaceService([])));
multiMruServiceCollection.define(IFolderRepositoryManager, noWorkspaceFolderManager);
multiMruServiceCollection.set(IAgentSessionsWorkspace, { _serviceBrand: undefined, isAgentSessionsWorkspace: false });

const accessor = multiMruServiceCollection.createTestingAccessor();
const multiMruService = accessor.get(IInstantiationService).createInstance(ClaudeCodeSessionService);
Expand Down Expand Up @@ -553,6 +606,7 @@ describe('ClaudeCodeSessionService', () => {
const multiRootWorkspaceService = store.add(new TestWorkspaceService([folder1, folder2]));
multiRootTestingServiceCollection.set(IWorkspaceService, multiRootWorkspaceService);
multiRootTestingServiceCollection.define(IFolderRepositoryManager, new MockFolderRepositoryManager());
multiRootTestingServiceCollection.set(IAgentSessionsWorkspace, { _serviceBrand: undefined, isAgentSessionsWorkspace: false });

const accessor = multiRootTestingServiceCollection.createTestingAccessor();
const instaService = accessor.get(IInstantiationService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class MockClaudeCodeSdkService implements IClaudeCodeSdkService {
return this.createMockQuery(options.prompt);
}

public async listSessions(dir: string): Promise<SDKSessionInfo[]> {
public async listSessions(dir?: string): Promise<SDKSessionInfo[]> {
return this.mockSessions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ export class ClaudeChatSessionItemController extends Disposable {
item.timing = { ...item.timing, lastRequestEnded: Date.now() };
}
const session = await this._claudeCodeSessionService.getSession(resource, CancellationToken.None);
if (session?.cwd) {
if (session?.cwd && await this._workspaceService.isResourceTrusted(URI.file(session.cwd))) {
Comment thread
TylerLeonhardt marked this conversation as resolved.
item.changes = await this._claudeWorkspaceFolderService.getWorkspaceChanges(
session.cwd,
session.gitBranch,
Expand Down Expand Up @@ -734,12 +734,21 @@ export class ClaudeChatSessionItemController extends Disposable {
};
item.iconPath = new vscode.ThemeIcon('claude');
if (session.cwd) {
item.metadata = await this._buildSessionMetadata(session.cwd);
item.changes = await this._claudeWorkspaceFolderService.getWorkspaceChanges(
session.cwd,
session.gitBranch,
undefined,
);
const isTrusted = await this._workspaceService.isResourceTrusted(URI.file(session.cwd));
if (isTrusted) {
const [metadata, changes] = await Promise.all([
this._buildSessionMetadata(session.cwd, isTrusted),
this._claudeWorkspaceFolderService.getWorkspaceChanges(
session.cwd,
session.gitBranch,
undefined,
),
]);
item.metadata = metadata;
item.changes = changes;
} else {
item.metadata = await this._buildSessionMetadata(session.cwd, isTrusted);
}
}
return item;
}
Expand All @@ -759,8 +768,13 @@ export class ClaudeChatSessionItemController extends Disposable {
return repositories.length > 1;
}

private async _buildSessionMetadata(cwd: string): Promise<SessionMetadata> {
const repoContext = await this._gitService.getRepository(URI.file(cwd));
private async _buildSessionMetadata(cwd: string, isTrusted?: boolean): Promise<SessionMetadata> {
const cwdUri = URI.file(cwd);
if (!(isTrusted ?? await this._workspaceService.isResourceTrusted(cwdUri))) {
return { workingDirectoryPath: cwd };
}

const repoContext = await this._gitService.getRepository(cwdUri);
if (!repoContext) {
return { workingDirectoryPath: cwd };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export class SessionOptionGroupBuilder implements ISessionOptionGroupBuilder {
if (!uri) {
return undefined;
}
const isTrusted = await vscode.workspace.isResourceTrusted(uri);
const isTrusted = await this.workspaceService.isResourceTrusted(uri);
if (!isTrusted) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ describe('Notebook Prompt Rendering', function () {
override applyEdit(edit: vscode.WorkspaceEdit): Thenable<boolean> {
throw new Error('Method not implemented.');
}
override isResourceTrusted(_resource: vscode.Uri): Thenable<boolean> {
return Promise.resolve(true);
}
override requestResourceTrust(_options: vscode.ResourceTrustRequestOptions): Thenable<boolean | undefined> {
return Promise.resolve(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { FileSystem, NotebookData, NotebookDocument, NotebookDocumentChangeEvent, ResourceTrustRequestOptions, TextDocument, TextDocumentChangeEvent, TextEditorSelectionChangeEvent, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent, WorkspaceTrustRequestOptions } from 'vscode';
import type { FileSystem, NotebookData, NotebookDocument, NotebookDocumentChangeEvent, ResourceTrustRequestOptions, TextDocument, TextDocumentChangeEvent, TextEditorSelectionChangeEvent, Uri, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent, WorkspaceTrustRequestOptions } from 'vscode';
import { Event } from '../../../../util/vs/base/common/event';
import { URI } from '../../../../util/vs/base/common/uri';
import { NotebookDocumentSnapshot } from '../../../editing/common/notebookDocumentSnapshot';
Expand Down Expand Up @@ -87,6 +87,10 @@ export class MockWorkspaceService implements IWorkspaceService {
return Promise.resolve();
}

isResourceTrusted(_resource: Uri): Thenable<boolean> {
return Promise.resolve(true);
}

requestResourceTrust(_options: ResourceTrustRequestOptions): Thenable<boolean | undefined> {
return Promise.resolve(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export class SimulationWorkspaceService extends AbstractWorkspaceService {
return Promise.resolve(true);
}

override isResourceTrusted(_resource: vscode.Uri): Thenable<boolean> {
return Promise.resolve(true);
}

override requestResourceTrust(options: vscode.ResourceTrustRequestOptions): Thenable<boolean | undefined> {
return Promise.resolve(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface IWorkspaceService {
* has been downloaded before we can use them.
*/
ensureWorkspaceIsFullyLoaded(): Promise<void>;
isResourceTrusted(resource: Uri): Thenable<boolean>;
requestResourceTrust(options: ResourceTrustRequestOptions): Thenable<boolean | undefined>;
requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable<boolean | undefined>;
}
Expand All @@ -75,6 +76,7 @@ export abstract class AbstractWorkspaceService implements IWorkspaceService {
abstract showWorkspaceFolderPicker(): Promise<WorkspaceFolder | undefined>;
abstract getWorkspaceFolderName(workspaceFolderUri: URI): string;
abstract applyEdit(edit: WorkspaceEdit): Thenable<boolean>;
abstract isResourceTrusted(resource: Uri): Thenable<boolean>;
abstract requestResourceTrust(options: ResourceTrustRequestOptions): Thenable<boolean | undefined>;
abstract requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable<boolean | undefined>;

Expand Down Expand Up @@ -229,6 +231,10 @@ export class NullWorkspaceService extends AbstractWorkspaceService implements ID
this.disposables.dispose();
}

override isResourceTrusted(_resource: Uri): Thenable<boolean> {
return Promise.resolve(true);
}

override requestResourceTrust(options: ResourceTrustRequestOptions): Thenable<boolean | undefined> {
return Promise.resolve(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export class ExtensionTextDocumentManager extends AbstractWorkspaceService {
}


override isResourceTrusted(resource: Uri): Thenable<boolean> {
return workspace.isResourceTrusted(resource);
Comment thread
TylerLeonhardt marked this conversation as resolved.
}

override requestResourceTrust(options: ResourceTrustRequestOptions): Thenable<boolean | undefined> {
return workspace.requestResourceTrust(options);
}
Expand Down
Loading