|
| 1 | +# VS Code Claude Code Session Support |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Add VS Code Claude Code sessions to CodeV's session list, enabling detection, display, switching, resuming, and launching — on par with terminal-based sessions. |
| 6 | + |
| 7 | +## Background |
| 8 | + |
| 9 | +Claude Code runs inside VS Code as an extension (`entrypoint: "claude-vscode"`). These sessions share the same `~/.claude/` data layer as CLI sessions, but with key differences: |
| 10 | + |
| 11 | +- **Not in `history.jsonl`**: VS Code sessions are excluded from the global history log (upstream bugs [#24579](https://github.com/anthropics/claude-code/issues/24579), [#18619](https://github.com/anthropics/claude-code/issues/18619)) |
| 12 | +- **No `/rename` support**: Cannot set custom titles ([#33165](https://github.com/anthropics/claude-code/issues/33165)) |
| 13 | +- **Has `ai-title`**: Auto-generated titles stored in session JSONL (e.g., `"Casual greeting and session start"`) |
| 14 | +- **URI handler available**: `vscode://anthropic.claude-code/open?session=<UUID>` (requires extension v2.1.72+) |
| 15 | +- **Hooks work**: Status hooks fire for VS Code sessions via `$CLAUDE_CODE_ENTRYPOINT` env var (verified experimentally) |
| 16 | + |
| 17 | +## Data Availability |
| 18 | + |
| 19 | +| Data | Available | Source | |
| 20 | +|------|-----------|--------| |
| 21 | +| Active session metadata | Yes | `~/.claude/sessions/<PID>.json` with `entrypoint: "claude-vscode"` | |
| 22 | +| Session status (working/idle/needs-attention) | Yes | Hooks write to `~/.claude/codev-status/{sessionId}.json` | |
| 23 | +| Conversation history | Yes | `~/.claude/projects/{path}/{sessionId}.jsonl` (same format as CLI) | |
| 24 | +| AI-generated title | Yes | `ai-title` entry in session JSONL (written once per session) | |
| 25 | +| Custom title (`/rename`) | No | Not supported in VS Code extension | |
| 26 | +| First/last user prompt | Yes | Readable from session JSONL head/tail | |
| 27 | +| Last assistant response | Yes | Readable from session JSONL tail (shared algorithm) | |
| 28 | +| Git branch | Yes | In JSONL entries | |
| 29 | +| PR links | Yes | If created during session | |
| 30 | +| Closed session record | **No** | Not in `history.jsonl` — solved via JSONL scan + hooks index | |
| 31 | +| IDE workspace info | Yes | `~/.claude/ide/<PID>.lock` (workspace folders, IDE name) | |
| 32 | + |
| 33 | +## Architecture |
| 34 | + |
| 35 | +### Layer 1: Detection |
| 36 | + |
| 37 | +**Active sessions** — Removed the `entrypoint !== 'cli'` filter in `detectActiveSessions()`. VS Code sessions appear alongside CLI sessions. Uses async `readVSCodeSessionFromJSONL()` with head/tail for metadata. |
| 38 | + |
| 39 | +**Closed sessions** — Two-pronged approach: |
| 40 | +1. **Hooks index**: `codev-status-hook.sh` detects `$CLAUDE_CODE_ENTRYPOINT === "claude-vscode"` and appends to `~/.claude/codev-status/vscode-sessions.jsonl`. Uses marker files (`.vs-{sessionId}`) to avoid duplicate writes. Works even when CodeV is not running. |
| 41 | +2. **JSONL scan**: `scanClosedVSCodeSessions()` reads first 4KB of each JSONL file to check entrypoint. Hooks-indexed sessions are skipped (no 4KB read needed). Results cached for 30s. |
| 42 | + |
| 43 | +### Layer 2: Display |
| 44 | + |
| 45 | +- **Badge**: `[VSCODE]` badge shown for **active** sessions only (consistent with CLI terminal badges). Closed sessions do not show terminal badges to avoid visual noise and stale badge issues. |
| 46 | +- **Title**: `ai-title` from JSONL as fallback. Priority: `custom-title > ai-title > first prompt` |
| 47 | +- **Status dot**: Same as CLI sessions (hooks fire for VS Code via `$CLAUDE_CODE_ENTRYPOINT`) |
| 48 | +- **First/last prompt**: Read from JSONL via shared `parseUserMessageFromLines()`, skips `<ide_>` context blocks via `extractUserText()` |
| 49 | +- **Search**: Includes `ai-title`, terminal type (`vscode`/`ghostty`/`iterm2`), and PR URLs |
| 50 | +- **Badge highlights**: Search matches highlight terminal badge and PR badge text |
| 51 | + |
| 52 | +### Layer 3: Switch (active sessions) |
| 53 | + |
| 54 | +Uses the VS Code URI handler: |
| 55 | +``` |
| 56 | +open "vscode://anthropic.claude-code/open?session=<UUID>" |
| 57 | +``` |
| 58 | + |
| 59 | +Verified behavior: |
| 60 | +- Existing session UUID: switches to that session tab in VS Code |
| 61 | +- First call requires user to allow the URI handler (one-time dialog, check "Do not ask me again") |
| 62 | +- No Accessibility API or AppleScript needed |
| 63 | +- MAS-compatible (uses standard URI scheme) |
| 64 | + |
| 65 | +### Layer 4: Resume (closed sessions) |
| 66 | + |
| 67 | +Two-step process for VS Code resume: |
| 68 | +1. Open project folder: `code "<projectPath>"` |
| 69 | +2. After 2s delay (for VS Code to load workspace): URI handler `vscode://anthropic.claude-code/open?session=<UUID>` |
| 70 | + |
| 71 | +If the user's Launch Terminal is not set to VS Code, closed sessions resume in the default terminal (standard behavior). |
| 72 | + |
| 73 | +Measured latency: |
| 74 | +| Scenario | Time | |
| 75 | +|----------|------| |
| 76 | +| Active session switch | Instant | |
| 77 | +| Resume in already-open VS Code project | ~1-2s | |
| 78 | +| Resume in new VS Code project | ~3-5s | |
| 79 | + |
| 80 | +### Layer 5: Settings |
| 81 | + |
| 82 | +- **Launch Terminal dropdown**: Added "VS Code" option alongside iTerm2, Terminal, Ghostty, cmux |
| 83 | +- When set to VS Code, closed session clicks use the URI handler resume flow |
| 84 | +- Launch Mode (tab/window) does not apply to VS Code |
| 85 | + |
| 86 | +### Layer 6: Shared Algorithm (head/tail reads) |
| 87 | + |
| 88 | +`readVSCodeSessionFromJSONL()` performs a single set of parallel reads: |
| 89 | +- `head -n 20` → first user prompt (via `parseUserMessageFromLines()`) |
| 90 | +- `tail -n 100` → last user prompt + last assistant message (via `parseUserMessageFromLines(lines, true)` + `parseAssistantMessageFromLines()`) |
| 91 | +- `grep -c '"type":"user"'` → message count |
| 92 | +- Also extracts `cwd` from JSONL content (directory name decode is lossy) |
| 93 | + |
| 94 | +This avoids duplicate tail reads — the assistant response is extracted from the same tail output, so `loadLastAssistantResponses()` is not called again for VS Code sessions. |
| 95 | + |
| 96 | +Shared helper functions: |
| 97 | +| Function | Purpose | Used by | |
| 98 | +|----------|---------|---------| |
| 99 | +| `extractUserText(content)` | Extract text from message content, skip `<ide_>` blocks | `parseUserMessageFromLines()` | |
| 100 | +| `parseUserMessageFromLines(lines, fromEnd?)` | Find user message in JSONL lines | `readVSCodeSessionFromJSONL()` | |
| 101 | +| `parseAssistantMessageFromLines(lines)` | Find last assistant message in JSONL lines | `readVSCodeSessionFromJSONL()` | |
| 102 | + |
| 103 | +### Layer 7: Title Enrichment (`ai-title`) |
| 104 | + |
| 105 | +Added `ai-title` grep to `loadSessionEnrichment()` alongside existing `custom-title` grep. Priority: `custom-title > ai-title`. |
| 106 | + |
| 107 | +Format in JSONL: `{"type":"ai-title","sessionId":"...","aiTitle":"..."}` |
| 108 | + |
| 109 | +Characteristics: |
| 110 | +- Written once per session (does not change) |
| 111 | +- Present in both VS Code and newer CLI sessions |
| 112 | +- AI-generated, descriptive (e.g., "Casual greeting and session start") |
| 113 | +- See #104 for applying ai-title fallback to all sessions |
| 114 | + |
| 115 | +### Layer 8: Real-time Preview Refresh |
| 116 | + |
| 117 | +When a session becomes idle (Stop hook → fs.watch → status update): |
| 118 | +1. fs.watch fires (debounced 50ms to avoid 3-6x duplicate triggers on macOS) |
| 119 | +2. Status update sent to renderer with `{status, timestamp}` per session |
| 120 | +3. Renderer compares status timestamp vs last fetch timestamp (avoids unreliable working→idle transition detection) |
| 121 | +4. After 300ms delay (ensures JSONL fully flushed): `refreshSessionPreview()` reads `tail -n 100` |
| 122 | +5. Single tail read extracts both last user message + last assistant message |
| 123 | +6. Updates `assistantResponses`, `lastUserMessage`, `lastTimestamp`, and re-sorts session list |
| 124 | + |
| 125 | +### Layer 9: Launch New Session |
| 126 | + |
| 127 | +```bash |
| 128 | +# Open new Claude Code tab in VS Code |
| 129 | +open "vscode://anthropic.claude-code/open" |
| 130 | + |
| 131 | +# Open with pre-filled prompt |
| 132 | +open "vscode://anthropic.claude-code/open?prompt=help%20me%20review%20this%20PR" |
| 133 | +``` |
| 134 | + |
| 135 | +## Performance |
| 136 | + |
| 137 | +| Operation | Cost | Notes | |
| 138 | +|-----------|------|-------| |
| 139 | +| Detection (active) | +0ms | Same `sessions/*.json` read, removed filter | |
| 140 | +| JSONL scan (closed) | ~50ms | 218 files, 4KB read per file, cached 30s | |
| 141 | +| Hooks index skip | saves ~0.2ms/file | Known VS Code sessions skip 4KB entrypoint check | |
| 142 | +| head/tail read per session | ~5-10ms | Parallel head-20 + tail-100 + grep-c | |
| 143 | +| ai-title grep | +~1ms/session | Parallel with existing greps | |
| 144 | +| URI handler switch | instant | Single `open` command | |
| 145 | +| URI handler resume | ~1-5s | `code <path>` + 2s delay + URI handler | |
| 146 | +| Hooks index write | ~5ms/event | Per hook event, marker file prevents duplicates | |
| 147 | +| Session count | capped at 100 | Sort by timestamp, then slice after merge | |
| 148 | +| Timestamp normalization | +0ms | ISO string → unix ms conversion in reader | |
| 149 | +| Real-time refresh | ~2ms/session | Single tail-100, 300ms delay, debounced fs.watch | |
| 150 | + |
| 151 | +## VS Code URI Handler Reference |
| 152 | + |
| 153 | +| URL | Effect | |
| 154 | +|-----|--------| |
| 155 | +| `vscode://anthropic.claude-code/open` | Open new Claude Code tab | |
| 156 | +| `vscode://anthropic.claude-code/open?session=<UUID>` | Switch to / resume session | |
| 157 | +| `vscode://anthropic.claude-code/open?prompt=<text>` | Open with pre-filled prompt | |
| 158 | + |
| 159 | +Requires Claude Code VS Code extension **v2.1.72+** (released 2026-03-10). |
| 160 | + |
| 161 | +First call shows a permission dialog in VS Code. User can check "Do not ask me again" to suppress future dialogs. |
| 162 | + |
| 163 | +## Known Limitations |
| 164 | + |
| 165 | +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). |
| 166 | +2. **No `/rename` in VS Code**: Use `ai-title` as fallback ([#33165](https://github.com/anthropics/claude-code/issues/33165)). |
| 167 | +3. **Resume delay**: 2s fixed delay for workspace loading. Could be optimized by detecting if project is already open. |
| 168 | +4. **URI handler one-time dialog**: First use requires user to click "Allow" in VS Code. |
| 169 | +5. **JSONL timestamp format**: VS Code uses ISO strings, CLI uses unix ms. Normalized at read time. |
| 170 | + |
| 171 | +## Alternatives Considered |
| 172 | + |
| 173 | +### Accessibility API (AXUIElement) |
| 174 | +- Can find Claude Code tab in VS Code's AX tree (`AXRadioButton title="Claude Code"`) |
| 175 | +- More complex, requires Accessibility permission, not MAS-compatible |
| 176 | +- **Decision**: Not needed — URI handler provides better precision (session-level vs tab-level) |
| 177 | +- Reference: [mediar-ai/mcp-server-macos-use](https://github.com/mediar-ai/mcp-server-macos-use) |
| 178 | + |
| 179 | +### `code -r <path>` (c9watch / claude-control approach) |
| 180 | +- Only switches to VS Code window, cannot target specific session |
| 181 | +- **Decision**: URI handler is strictly better |
| 182 | + |
| 183 | +### Wait for upstream `history.jsonl` fix |
| 184 | +- Upstream bugs #24579, #18619 may eventually add VS Code sessions to history |
| 185 | +- **Decision**: Don't block on this — hooks index + JSONL scan covers the gap |
0 commit comments