feat: implement ClaudeCodeFolderMruService for managing recently used folders#310349
feat: implement ClaudeCodeFolderMruService for managing recently used folders#310349TylerLeonhardt merged 1 commit intomainfrom
Conversation
… folders This will help us adopt SessionOptionGroupBuilder
Screenshot ChangesBase: Changed (1)blocks-ci screenshots changedReplace the contents of Updated blocks-ci-screenshots.md<!-- auto-generated by CI — do not edit manually -->
#### editor/codeEditor/CodeEditor/Dark

#### editor/codeEditor/CodeEditor/Light
 |
There was a problem hiding this comment.
Pull request overview
Implements a Claude-specific folder MRU service and wires Claude chat session UX to use the new IChatFolderMruService API (instead of the deprecated IFolderRepositoryManager.getFolderMRU()), supporting upcoming adoption of SessionOptionGroupBuilder.
Changes:
- Added
ClaudeCodeFolderMruServiceto compute/merge “recent folders” from Claude sessions, recent Git repos, and current workspace folders (with caching + delete support). - Updated
ClaudeChatSessionContentProviderand its unit tests to consumeIChatFolderMruService.getRecentlyUsedFolders(...). - Registered
IChatFolderMruServicefor Claude sessions in the chat sessions contribution and added dedicated unit tests for the new MRU service.
Show a summary per file
| File | Description |
|---|---|
| extensions/copilot/src/extension/chatSessions/vscode-node/claudeChatSessionContentProvider.ts | Swaps MRU source to IChatFolderMruService for empty-workspace defaults/options. |
| extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts | Updates mocks/wiring to provide IChatFolderMruService in tests. |
| extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts | Registers ClaudeCodeFolderMruService as the Claude implementation of IChatFolderMruService. |
| extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeFolderMru.ts | New MRU service implementation for Claude sessions (merge/sort/cache/delete). |
| extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeFolderMru.spec.ts | New unit tests covering extraction, filtering, merge behavior, sorting, caching, and deletion. |
Copilot's findings
- Files reviewed: 5/5 changed files
- Comments generated: 2
| const WORKTREE_PATH_PATTERNS = ['.claude/worktrees/', '.worktrees/copilot-'] as const; | ||
|
|
||
| function isWorktreePath(path: string): boolean { | ||
| return WORKTREE_PATH_PATTERNS.some(pattern => path.includes(pattern)); | ||
| } | ||
|
|
||
| export class ClaudeCodeFolderMruService implements IChatFolderMruService { | ||
| declare _serviceBrand: undefined; | ||
| private readonly removedFolders = new ResourceSet(); | ||
| private cachedEntries: FolderRepositoryMRUEntry[] | undefined = undefined; | ||
|
|
||
| constructor( | ||
| @IClaudeCodeSessionService private readonly sessionService: IClaudeCodeSessionService, | ||
| @IGitService private readonly gitService: IGitService, | ||
| @IWorkspaceService private readonly workspaceService: IWorkspaceService, | ||
| ) { } | ||
|
|
||
| async getRecentlyUsedFolders(token: CancellationToken): Promise<FolderRepositoryMRUEntry[]> { | ||
| const cachedEntries = this.cachedEntries; | ||
| const entries = this.getRecentlyUsedFoldersImpl(token).then(entries => { | ||
| this.cachedEntries = entries; | ||
| return entries; | ||
| }); | ||
|
|
||
| return (cachedEntries ? cachedEntries : await entries).filter(e => !this.removedFolders.has(e.folder)); | ||
| } | ||
|
|
||
| private async getRecentlyUsedFoldersImpl(token: CancellationToken): Promise<FolderRepositoryMRUEntry[]> { | ||
| const mruEntries = new ResourceMap<Mutable<FolderRepositoryMRUEntry>>(); | ||
|
|
||
| // We're getting MRU, don't delay session retrieve by more than 5s | ||
| const sessions = await raceTimeout(this.sessionService.getAllSessions(token), 5_000); | ||
|
|
||
| for (const session of (sessions ?? [])) { | ||
| if (!session.cwd) { | ||
| continue; | ||
| } | ||
| if (isWorktreePath(session.cwd)) { | ||
| continue; | ||
| } | ||
| const folderUri = URI.file(session.cwd); |
There was a problem hiding this comment.
Worktree path filtering for sessions is based on isWorktreePath(session.cwd) where cwd is a raw filesystem path string. On Windows this will typically contain backslashes, so checks for patterns like .claude/worktrees/ and .worktrees/copilot- (with /) won’t match and worktree folders may incorrectly appear in the MRU. Consider normalizing the path before matching (e.g., build a URI and check its .path, or replace \\ with / / use a platform-agnostic regex) and add a regression test for a Windows-style path.
| return entries; | ||
| }); | ||
|
|
||
| return (cachedEntries ? cachedEntries : await entries).filter(e => !this.removedFolders.has(e.folder)); |
There was a problem hiding this comment.
When cachedEntries is set, getRecentlyUsedFolders() returns the cache without awaiting the refresh promise (entries). If getRecentlyUsedFoldersImpl() rejects (e.g. session service throws), that rejection can become unhandled because nothing awaits/catches it. Consider explicitly handling errors for the background refresh (e.g. void entries.catch(...) and keep the last good cache).
| return (cachedEntries ? cachedEntries : await entries).filter(e => !this.removedFolders.has(e.folder)); | |
| if (cachedEntries) { | |
| void entries.catch(() => undefined); | |
| return cachedEntries.filter(e => !this.removedFolders.has(e.folder)); | |
| } | |
| return (await entries).filter(e => !this.removedFolders.has(e.folder)); |
This will help us adopt SessionOptionGroupBuilder