From f21782a53f0a5700723515f56c5d4f3d4e78d5af Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 19:27:28 +0800 Subject: [PATCH 1/8] feat: poll IDE lock file for VS Code resume, replace fixed 2s delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isVSCodeProjectOpen(): check lock files for matching workspace + alive PID → instant URI handler if found - waitForVSCodeExtensionReady(): poll every 300ms until extension creates lock file (max 8s timeout) - Fallback to 2s delay if IDE lock dir doesn't exist - Also applied to launchNewClaudeSession() VS Code path - Fixes cold-start duplicate tab (waits for extension ready) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 111 ++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 0f9f8bb..a310b1a 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -1311,16 +1311,21 @@ export const launchNewClaudeSession = ( ): void => { if (terminalApp === 'vscode') { const { execFile } = require('child_process'); - // Use `open -b` with bundle ID to avoid extra Dock icon (vs `code` CLI) + if (isVSCodeProjectOpen(projectPath)) { + // Project already open → instant + execFile('open', ['vscode://anthropic.claude-code/open']); + return; + } + // Launch VS Code, poll for extension ready, then open new session const bundleId = getCurrentIDEBundleId(); execFile('open', ['-b', bundleId, projectPath], (error: any) => { if (error) { console.error('[launchNewClaudeSession] failed to open VS Code:', error); return; } - setTimeout(() => { + waitForVSCodeExtensionReady(projectPath).then(() => { execFile('open', ['vscode://anthropic.claude-code/open']); - }, 2000); + }); }); return; } @@ -1333,38 +1338,94 @@ export const launchNewClaudeSession = ( runCommandInTerminal(`cd "${projectPath}" && claude`, 'claude', projectPath, terminalApp, terminalMode); }; +/** + * Check if VS Code has a specific project open by reading IDE lock files. + * Lock files are created by Claude Code extension per VS Code window. + * Returns true if a lock file with matching workspaceFolders + alive PID exists. + */ +const isVSCodeProjectOpen = (projectPath: string): boolean => { + const ideDir = path.join(os.homedir(), '.claude', 'ide'); + if (!fs.existsSync(ideDir)) return false; + try { + for (const file of fs.readdirSync(ideDir)) { + if (!file.endsWith('.lock')) continue; + try { + const content = JSON.parse(fs.readFileSync(path.join(ideDir, file), 'utf-8')); + if (!content.pid || !content.workspaceFolders) continue; + const hasFolder = content.workspaceFolders.some((f: string) => f === projectPath); + if (!hasFolder) continue; + // Verify PID is alive + try { process.kill(content.pid, 0); return true; } catch { /* dead PID */ } + } catch {} + } + } catch {} + return false; +}; + +/** + * Poll IDE lock files until a matching project appears (extension ready). + * Resolves when lock file with matching workspaceFolders + alive PID is found, + * or after timeout (fallback). + */ +const waitForVSCodeExtensionReady = (projectPath: string, timeoutMs = 8000, intervalMs = 300): Promise => { + return new Promise((resolve) => { + // Quick check — maybe it's already ready + if (isVSCodeProjectOpen(projectPath)) { resolve(); return; } + + // If IDE lock dir doesn't exist, lock file mechanism may not be available + // Fall back to fixed 2s delay instead of waiting 8s + const ideDir = path.join(os.homedir(), '.claude', 'ide'); + if (!fs.existsSync(ideDir)) { + setTimeout(resolve, 2000); + return; + } + + const startTime = Date.now(); + const timer = setInterval(() => { + if (isVSCodeProjectOpen(projectPath) || Date.now() - startTime > timeoutMs) { + clearInterval(timer); + resolve(); + } + }, intervalMs); + }); +}; + /** * Open a Claude Code session in VS Code via URI handler. - * For active sessions: switches to the existing session tab. - * For closed sessions: opens the project folder first, then resumes via URI handler. + * - Active sessions: instant switch via URI handler. + * - Closed sessions (VS Code has project open): instant URI handler. + * - Closed sessions (project not open): opens project, polls for extension ready, then URI handler. */ export const openSessionInVSCode = (sessionId: string, projectPath?: string): void => { const { execFile } = require('child_process'); + const uri = `vscode://anthropic.claude-code/open?session=${sessionId}`; - if (projectPath) { - // Closed session: open project folder first, then resume after a short delay - // Use `open -b` with bundle ID to avoid extra Dock icon (vs `code` CLI) - const bundleId = getCurrentIDEBundleId(); - execFile('open', ['-b', bundleId, projectPath], (error: any) => { - if (error) { - console.error('[openSessionInVSCode] failed to open project:', error); - return; - } - // Wait for VS Code to open the workspace before resuming session - setTimeout(() => { - const uri = `vscode://anthropic.claude-code/open?session=${sessionId}`; - execFile('open', [uri]); - }, 2000); - }); - } else { + if (!projectPath) { // Active session: directly switch via URI handler - const uri = `vscode://anthropic.claude-code/open?session=${sessionId}`; execFile('open', [uri], (error: any) => { - if (error) { - console.error('[openSessionInVSCode] failed:', error); - } + if (error) console.error('[openSessionInVSCode] failed:', error); }); + return; } + + // Closed session: check if VS Code already has this project open + if (isVSCodeProjectOpen(projectPath)) { + // Project already open + extension ready → instant URI handler + execFile('open', [uri]); + return; + } + + // Project not open → launch VS Code, poll for extension ready, then URI handler + const bundleId = getCurrentIDEBundleId(); + execFile('open', ['-b', bundleId, projectPath], (error: any) => { + if (error) { + console.error('[openSessionInVSCode] failed to open project:', error); + return; + } + waitForVSCodeExtensionReady(projectPath).then(() => { + execFile('open', [uri]); + }); + }); }; /** From 3049840f2c3a35f331c46982d95a6531b50683f0 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 19:34:55 +0800 Subject: [PATCH 2/8] chore: reduce poll timeout from 8s to 5s Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index a310b1a..a3160ac 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -1367,7 +1367,7 @@ const isVSCodeProjectOpen = (projectPath: string): boolean => { * Resolves when lock file with matching workspaceFolders + alive PID is found, * or after timeout (fallback). */ -const waitForVSCodeExtensionReady = (projectPath: string, timeoutMs = 8000, intervalMs = 300): Promise => { +const waitForVSCodeExtensionReady = (projectPath: string, timeoutMs = 5000, intervalMs = 250): Promise => { return new Promise((resolve) => { // Quick check — maybe it's already ready if (isVSCodeProjectOpen(projectPath)) { resolve(); return; } From 84ba0f53121d28dabf276f5f0f70018bcd70b6a3 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 19:44:36 +0800 Subject: [PATCH 3/8] fix: focus correct VS Code window before URI handler switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Active VS Code sessions were opening new sessions in the wrong window because openSessionInVSCode was called without projectPath — URI handler fired into whichever window was focused. Now always passes projectPath and uses open -b to focus the correct project window before URI handler. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index a3160ac..adbc05e 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -1043,9 +1043,10 @@ export const openSession = async ( } // Check if this is an active VS Code session — switch via URI handler + // Always pass projectPath so the correct VS Code window gets focused first const entrypoint = cachedEntrypoints?.get(sessionId); if (isActive && entrypoint === 'claude-vscode') { - openSessionInVSCode(sessionId); + openSessionInVSCode(sessionId, projectPath); return; } @@ -1401,17 +1402,20 @@ export const openSessionInVSCode = (sessionId: string, projectPath?: string): vo const uri = `vscode://anthropic.claude-code/open?session=${sessionId}`; if (!projectPath) { - // Active session: directly switch via URI handler + // No project path (shouldn't happen normally) — try URI handler directly execFile('open', [uri], (error: any) => { if (error) console.error('[openSessionInVSCode] failed:', error); }); return; } - // Closed session: check if VS Code already has this project open + // Check if VS Code already has this project open (extension ready) if (isVSCodeProjectOpen(projectPath)) { - // Project already open + extension ready → instant URI handler - execFile('open', [uri]); + // Focus the correct window first, then instant URI handler + const bundleId = getCurrentIDEBundleId(); + execFile('open', ['-b', bundleId, projectPath], () => { + execFile('open', [uri]); + }); return; } From e5281ffa4b4f8b8ae14783eb3736a87057bcd0e6 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 20:12:43 +0800 Subject: [PATCH 4/8] fix: add 500ms delay after window focus for URI handler (case 2) open -b callback fires before VS Code window is fully focused, causing URI handler to silently fail. 500ms delay gives VS Code time to complete window activation. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index adbc05e..9fc92d4 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -1414,7 +1414,10 @@ export const openSessionInVSCode = (sessionId: string, projectPath?: string): vo // Focus the correct window first, then instant URI handler const bundleId = getCurrentIDEBundleId(); execFile('open', ['-b', bundleId, projectPath], () => { - execFile('open', [uri]); + // Small delay to let VS Code window fully focus before URI handler + setTimeout(() => { + execFile('open', [uri]); + }, 500); }); return; } From 44d4f1e49816ee31bafbca19323e43dfa7c6fb04 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 20:26:53 +0800 Subject: [PATCH 5/8] fix: skip URI handler for active sessions on restore, 1.5s post-ready delay (cases 3, 5, 7) - Active VS Code sessions + project not open: only open -b, skip URI handler (VS Code restore handles the session tab) - Closed sessions + project not open: poll + 1.5s post-ready delay (lets VS Code finish restoring before URI handler, avoids duplicate tab) - Fixes cases 3, 5, and 7 from test matrix Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 9fc92d4..c25bacb 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -1046,7 +1046,7 @@ export const openSession = async ( // Always pass projectPath so the correct VS Code window gets focused first const entrypoint = cachedEntrypoints?.get(sessionId); if (isActive && entrypoint === 'claude-vscode') { - openSessionInVSCode(sessionId, projectPath); + openSessionInVSCode(sessionId, projectPath, true); return; } @@ -1394,15 +1394,16 @@ const waitForVSCodeExtensionReady = (projectPath: string, timeoutMs = 5000, inte /** * Open a Claude Code session in VS Code via URI handler. * - Active sessions: instant switch via URI handler. - * - Closed sessions (VS Code has project open): instant URI handler. - * - Closed sessions (project not open): opens project, polls for extension ready, then URI handler. + * - Active + project open: focus window + URI handler (instant switch). + * - Active + project not open: just open -b (let VS Code restore handle the session tab). + * - Closed + project open: focus window + 500ms delay + URI handler. + * - Closed + project not open: open -b + poll extension ready + 500ms delay + URI handler. */ -export const openSessionInVSCode = (sessionId: string, projectPath?: string): void => { +export const openSessionInVSCode = (sessionId: string, projectPath?: string, isActiveSession = false): void => { const { execFile } = require('child_process'); const uri = `vscode://anthropic.claude-code/open?session=${sessionId}`; if (!projectPath) { - // No project path (shouldn't happen normally) — try URI handler directly execFile('open', [uri], (error: any) => { if (error) console.error('[openSessionInVSCode] failed:', error); }); @@ -1411,10 +1412,9 @@ export const openSessionInVSCode = (sessionId: string, projectPath?: string): vo // Check if VS Code already has this project open (extension ready) if (isVSCodeProjectOpen(projectPath)) { - // Focus the correct window first, then instant URI handler + // Focus the correct window, then URI handler after short delay const bundleId = getCurrentIDEBundleId(); execFile('open', ['-b', bundleId, projectPath], () => { - // Small delay to let VS Code window fully focus before URI handler setTimeout(() => { execFile('open', [uri]); }, 500); @@ -1422,15 +1422,27 @@ export const openSessionInVSCode = (sessionId: string, projectPath?: string): vo return; } - // Project not open → launch VS Code, poll for extension ready, then URI handler + // Project not open → need to open it const bundleId = getCurrentIDEBundleId(); execFile('open', ['-b', bundleId, projectPath], (error: any) => { if (error) { console.error('[openSessionInVSCode] failed to open project:', error); return; } + + if (isActiveSession) { + // Active session: VS Code restore will reopen the session tab automatically. + // Don't fire URI handler — it would create a duplicate. + return; + } + + // Closed session: poll for extension ready, then URI handler to resume + // Extra 1.5s delay after extension ready to let VS Code finish restoring + // session tabs (avoids duplicate if tab was restored by VS Code) waitForVSCodeExtensionReady(projectPath).then(() => { - execFile('open', [uri]); + setTimeout(() => { + execFile('open', [uri]); + }, 1500); }); }); }; From 2d664b4ec93bbfa85130e854885ab77d114c943e Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 20:51:07 +0800 Subject: [PATCH 6/8] chore: bump version to 1.0.69 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c27a06..944ddc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 1.0.69 + +- Feat: adaptive VS Code resume via IDE lock file polling + - Replaces fixed 2s delay with `~/.claude/ide/*.lock` detection + - Already-open project: instant (~0.5s vs ~2s before) + - Cold start: adaptive poll + 1.5s post-ready delay +- Fix: duplicate Claude Code tab on VS Code window restore (cases 3, 5, 7) + - Active sessions skip URI handler when project needs to be opened + - Closed sessions wait for extension ready before URI handler +- Fix: resume not opening session tab when project window already open (case 2) +- Fix: active VS Code session switching to wrong window + ## 1.0.68 - Feat: quick-launch new Claude session from Projects tab diff --git a/package.json b/package.json index f00d213..ad9779f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "CodeV", "productName": "CodeV", - "version": "1.0.68", + "version": "1.0.69", "description": "Quick switcher for VS Code, Cursor, and Claude Code sessions", "repository": { "type": "git", From a0d804e89bd00f40074f89109c3259fce495fd6c Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 20:53:57 +0800 Subject: [PATCH 7/8] docs: update design doc + README with lock file polling details Replace outdated '2s fixed delay' references with adaptive lock file polling description. Add latency numbers and VS Code-specific resume flow to README terminal table. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- docs/vscode-session-support-design.md | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8895731..c83c087 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ For the full same-cwd accuracy matrix (detection + switch by launch method and t | Terminal.app | Title match → TTY fallback | AppleScript `do script` | Built-in macOS terminal; same TTY accuracy as iTerm2 | | Ghostty | Title match → cwd fallback | AppleScript new tab/window | Needs `/rename` for same-cwd. **Note:** Ghostty may not support `⌘+V` (paste) and `⌘+Z` (undo) in CodeV's search bar by default — add `keybind = super+v=paste_from_clipboard` and `keybind = super+z=undo` to `~/.config/ghostty/config` ([ghostty#10749](https://github.com/ghostty-org/ghostty/issues/10749#issuecomment-4131892831)) | | cmux | Title match → TTY fallback | CLI new-workspace | Same as iTerm2 (requires cmux v0.63+); requires socket access in cmux Settings (`automation` or `allowAll`) | -| VS Code | URI handler (session-level) | `open -b` + URI handler | Requires Claude Code VS Code extension v2.1.72+; `[VSCODE]` badge on active sessions | +| VS Code | URI handler (session-level) | `open -b` + URI handler | Requires Claude Code VS Code extension v2.1.72+; `[VSCODE]` badge on active sessions; adaptive resume via IDE lock file polling (~0.5s if project already open) | ### Embedded Terminal diff --git a/docs/vscode-session-support-design.md b/docs/vscode-session-support-design.md index 88b3249..b72e419 100644 --- a/docs/vscode-session-support-design.md +++ b/docs/vscode-session-support-design.md @@ -64,18 +64,22 @@ Verified behavior: ### Layer 4: Resume (closed sessions) -Two-step process for VS Code resume: -1. Open project folder: `code ""` -2. After 2s delay (for VS Code to load workspace): URI handler `vscode://anthropic.claude-code/open?session=` +Adaptive resume via IDE lock file polling (PR #109): +1. Check `~/.claude/ide/*.lock` for matching project + alive PID +2. If project already open → focus window + 500ms delay + URI handler (~0.5s) +3. If project not open → `open -b bundleId projectPath` + poll lock files (250ms interval, 5s timeout) + 1.5s post-ready delay + URI handler +4. Active sessions + project not open → only `open -b` (skip URI handler, let VS Code restore handle tab) +5. Fallback to fixed 2s delay if `~/.claude/ide/` doesn't exist If the user's Launch Terminal is not set to VS Code, closed sessions resume in the default terminal (standard behavior). Measured latency: | Scenario | Time | |----------|------| -| Active session switch | Instant | -| Resume in already-open VS Code project | ~1-2s | -| Resume in new VS Code project | ~3-5s | +| Active session switch (project open) | ~0.5s | +| Resume in already-open VS Code project | ~0.5s | +| Resume, project closed, VS Code open | ~2.5s | +| Resume, VS Code cold start | ~3-4s | ### Layer 5: Settings @@ -142,7 +146,8 @@ open "vscode://anthropic.claude-code/open?prompt=help%20me%20review%20this%20PR" | head/tail read per session | ~5-10ms | Parallel head-20 + tail-100 + grep-c | | ai-title grep | +~1ms/session | Parallel with existing greps | | URI handler switch | instant | Single `open` command | -| URI handler resume | ~1-5s | `code ` + 2s delay + URI handler | +| URI handler resume (project open) | ~0.5s | Lock file detect + 500ms + URI handler | +| URI handler resume (project not open) | ~2.5-4s | `open -b` + poll + 1.5s delay + URI handler | | Hooks index write | ~5ms/event | Per hook event, marker file prevents duplicates | | Session count | capped at 100 | Sort by timestamp, then slice after merge | | Timestamp normalization | +0ms | ISO string → unix ms conversion in reader | @@ -164,7 +169,7 @@ First call shows a permission dialog in VS Code. User can check "Do not ask me a 1. **Closed sessions not in `history.jsonl`**: Workaround via JSONL scan + hooks index. Upstream fix may come via [#24579](https://github.com/anthropics/claude-code/issues/24579). 2. **No `/rename` in VS Code**: Use `ai-title` as fallback ([#33165](https://github.com/anthropics/claude-code/issues/33165)). -3. **Resume delay**: 2s fixed delay for workspace loading. Could be optimized by detecting if project is already open. +3. **Resume delay**: ~~2s fixed delay for workspace loading.~~ Replaced by IDE lock file polling (PR #109). Already-open projects are instant (~0.5s). Cold start uses adaptive poll + 1.5s post-ready delay. 4. **URI handler one-time dialog**: First use requires user to click "Allow" in VS Code. 5. **JSONL timestamp format**: VS Code uses ISO strings, CLI uses unix ms. Normalized at read time. From cd9cff78873d0837607682f92784b90bf0e887fd Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 20:55:49 +0800 Subject: [PATCH 8/8] docs: add VS Code-specific handling comparison table Consolidates all differences between VS Code and terminal sessions across every layer (detection, switch, resume, readiness, restore conflict, etc.) in one table. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/vscode-session-support-design.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/vscode-session-support-design.md b/docs/vscode-session-support-design.md index b72e419..e76aba4 100644 --- a/docs/vscode-session-support-design.md +++ b/docs/vscode-session-support-design.md @@ -136,6 +136,26 @@ open "vscode://anthropic.claude-code/open" open "vscode://anthropic.claude-code/open?prompt=help%20me%20review%20this%20PR" ``` +### VS Code-specific handling (differences from terminals) + +VS Code sessions require fundamentally different handling from terminal sessions at every layer: + +| Layer | Terminals (iTerm2/Ghostty/cmux/Terminal.app) | VS Code | +|-------|----------------------------------------------|---------| +| **Detection** | `history.jsonl` (append-only log) | JSONL scan + hooks index (not in `history.jsonl`) | +| **Active detection** | Process tree walk → terminal type | `entrypoint: "claude-vscode"` in session JSON | +| **Status** | Same hooks, same status files | Same hooks, same status files (via `$CLAUDE_CODE_ENTRYPOINT` env var) | +| **Switch (active)** | AppleScript / CLI per-terminal | URI handler `vscode://...?session=` | +| **Resume (closed)** | `cd && claude --resume ` in new tab | `open -b` + IDE lock file polling + URI handler | +| **Window focus** | AppleScript per-terminal | `open -b bundleId projectPath` (macOS `open` with bundle ID) | +| **Readiness detection** | Not needed (terminal is always ready) | Poll `~/.claude/ide/*.lock` for extension ready | +| **Restore conflict** | N/A (terminals don't restore session tabs) | VS Code restores Claude Code tabs → skip URI handler for active sessions to avoid duplicate | +| **Title** | `/rename` custom title | `ai-title` (auto-generated, no `/rename` support) | +| **User message** | Direct text | Filter `` context blocks | +| **Same-cwd ambiguity** | TTY/title cross-reference | N/A — UUID-based URI handler is precise | +| **Dock icon** | N/A | Use `open -b bundleId` instead of `code` CLI to avoid extra icon | +| **Cold start timing** | Immediate (terminal launches fast) | Adaptive: lock file poll (250ms × max 20) + 1.5s post-ready delay | + ## Performance | Operation | Cost | Notes |