Sesh
Browse, annotate, and resume saved Claude Code and Codex CLI sessions — without leaving VSCode.

Sesh indexes the JSONL transcripts that Claude Code and Codex CLI already write to disk, gives you a fast filterable session list, full-text search across every transcript, structured rendering of tool calls and diffs, and one-click resume — all in a single VSCode panel.
Source data is read-only. Annotations live in Sesh's own SQLite. Your CLI's session files are never modified.
Why Sesh
Claude Code and Codex CLI both quietly accumulate a long tail of sessions. After a few months you've got hundreds of transcripts in ~/.claude/ and ~/.codex/, none of them browsable, searchable, or recoverable. The CLIs themselves give you --resume <id> but no way to find the id you want. Sesh fills that gap.
- Find any past conversation. Search by title, body, tag, category, or folder. Hits highlight inline.
- Make sense of long sessions. Tool calls, thinking blocks, and tool results render as collapsible cards. Edit/Write blocks become colored diffs. Code fences get Prism syntax highlighting. Screenshots inline.
- Don't lose history when CLIs prune. Sesh imports Claude Code's
sessions-index.json ledger, so titles + metadata survive even after transcripts are deleted. Opt in to gzipped transcript archiving and the bodies stick around too.
- Resume in the right place. A click runs
claude --resume <id> or codex resume <id> in a terminal opened in the session's original cwd. For Claude sessions in your current workspace, resume in the editor panel directly.
Sources supported
| Source |
Storage location |
Resume command |
| Claude Code |
~/.claude/projects/<encoded-cwd>/<id>.jsonl |
claude --resume <id> |
| Codex CLI |
~/.codex/sessions/<year>/<month>/<day>/rollout-<ts>-<id>.jsonl |
codex resume <id> |
Schema is source-pluggable — adding another source means a parser + a source value, not a migration.
Features in detail
Tab navigation. The panel has six tabs — Sessions, Knowledge, Insights, Ideas, Reviewer, and a gear-icon Settings tab — all live. Heavyweight tabs (Knowledge, Ideas, Reviewer) are toggleable per-user; their underlying indexers are off by default and can be turned on from the Settings tab without a window reload.
Settings tab. Every sesh.* configuration lives behind a switch in the gear-icon tab. Switches optimistically update locally, then sync to VSCode user settings; round-trips with VSCode's native settings UI (Cmd+,) work both ways. Toggling embeddings, the embedder choice, idea mining, or git indexing applies live — onDidChangeConfiguration tears down and rebuilds the affected services on the spot, no reload required.
Knowledge tab. Three sections, all derived from semantic embeddings of your transcripts: Lessons (patterns where you've corrected the assistant repeatedly, distilled into ready-to-paste CLAUDE.md additions), Topics (clusters of related discussions across sessions, expandable to see source sessions), and Glossary (recurring project terms — click to filter). The Sessions tab also runs free-text searches through this index for hybrid lexical + semantic results.
Ideas tab. A graveyard of intent-bearing things you've said across sessions ("I should refactor X", "we need to add Y"), clustered by similarity. Nothing slips through the cracks.
Insights tab. Five sub-views:
- Standup — daily/7d/30d/1y/all-time view. Magazine mode is a card-laid-out summary with KPI tiles (cache hit, $/turn, corrections, $/shipped), colored outcome pills, project breakdown, models + tools chip rows, top file, and a vs-prior comparison. Standup mode is the same data as a copyable text block.
- By file — KPI tiles (top file's share, files-for-80% Pareto, avg per file, avg per call), an Action ideas card that auto-surfaces concentration, expensive-per-call hot files, and CLAUDE.md candidates, plus a sortable table where each row drills inline to the sessions that touched that file.
- Models — per-model turn counts and USD cost, sorted by spend.
- Records — personal bests: longest session, longest streak, total spend.
- Style — your writing fingerprint: voice (sentence length, lexical density), tone (politeness, hedging, gratitude), vocabulary breadth, by-outcome correlation, and your top conversation openings. Export as JSON via the command palette (
Sesh: Export style fingerprint).
Pick up where you left off banner. Above the session list. Surfaces unfinished idea clusters and recent commitments scoped to the current workspace by default (configurable to global). Each row shows the source session's title and — in global scope — a folder pill with the workspace name, so you see at a glance where each suggestion came from.
Reviewer tab. Three sub-tabs: Branch shows recent commits in the current repo with linked sessions and confidence percentages. Sessions groups sessions in this repo by their linked commits. PRs lists open pull requests (via gh CLI) with linked-session counts — shows a friendly empty-state when gh is missing or not authenticated.
Outcome auto-flip. Session outcome states upgrade automatically from git history: confidence ≥ 0.5 → shipped; 0.2–0.5 → shipped-partial; a later revert commit touching the same files → reverted. User-marked outcomes are never overwritten.
Status bar. When sesh.statusBarShowCost is on, the status bar shows $(history) $X.XX today and updates every minute. It hides on days with no activity. Click it to open the panel.
Session list. Virtualized 3-line rows with title (custom or auto-derived), category pill, tags, message count, source badge, last-active relative time. Each row shows an outcome dot, per-session cost, and model badge once the session has been indexed. Orphaned (pruned-transcript) sessions render in muted italics.
Detail pane. Sectioned layout: header, annotations, transcript. Folder pulled into its own labeled row. Tag input is inline, category dropdown supports inline create. Resume buttons hide when they'd be invalid (cross-workspace panel resume, codex panel resume).
Search. SQLite FTS5 across annotations and transcript bodies. Matches highlight in titles and the transcript body via <mark> tags. Auto-expands collapsed tool blocks when the body matches.
Scope. Three modes: current folder, all projects, or pick a specific folder from the dropdown (groups all known projects). Honors project remaps so renamed folders surface their legacy sessions.
Transcript rendering.
- Markdown + GFM via
react-markdown for assistant text.
- Prism syntax highlighting for fenced code (theme-aware: vsLight on
.vscode-light, vsDark otherwise).
- Default-collapsed
<details> cards for tool_use / tool_result / thinking blocks, each with a per-tool codicon (Bash → terminal, Read → file, Edit → edit, Grep → search, etc.).
- Line-by-line diff for Edit / Write tool calls, derived via
diff.diffLines and tinted with VSCode's diff editor colors.
- Inline images for screenshot-bearing messages.
Project remap. When the workspace path doesn't match any indexed project_path but a folder with the same basename does, Sesh suggests merging them. One click writes a project_remap row and legacy sessions show up under the new path.
Annotations. Custom titles, colored categories, free-form tags, notes, favorited, archived. All overlay-only — never written to source JSONL.
Settings
Every setting below also has a switch in the in-panel Settings tab (gear icon at the right of the tab bar). Heavyweight indexers default off — Sesh stays quiet on first run and only does the expensive work after you opt in.
| Key |
Default |
Effect |
sesh.openOnActivation |
false |
Auto-open the panel on VSCode startup. |
sesh.transcriptLimit |
10000 |
Max recent messages loaded into the detail pane. Lower this if very long transcripts feel slow to open. |
sesh.archiveTranscripts |
false |
Keep a gzipped sidecar of each transcript at ~/.sesh/transcripts/<id>.jsonl.gz so pruned sessions stay readable. Roughly 1/6 the raw JSONL size after gzip. |
sesh.statusBarShowCost |
true |
Show today's spend in the status bar. |
sesh.indexBackfillMode |
"eager" |
eager indexes all sessions in the background at activation. lazy defers until you open a session. |
sesh.outcomeInferenceDays |
30 |
Days of inactivity before an un-reviewed session is auto-marked abandoned. |
sesh.tabs.knowledge |
false |
Show the Knowledge tab. Off by default — needs embeddingsEnabled to populate. |
sesh.tabs.ideas |
false |
Show the Ideas tab. Off by default — needs ideaMining to populate. |
sesh.tabs.insights |
true |
Show the Insights tab. |
sesh.tabs.reviewer |
true |
Show the Reviewer tab. |
sesh.pickUpBanner |
true |
Show the "Pick up where you left off" banner above the session list. |
sesh.pickUpScope |
"workspace" |
Which sessions feed the banner. workspace restricts to the current repo; global uses every session. |
sesh.gitIndexerEnabled |
false |
Enable git-log indexing and commit-linkage. Off by default — turning it on triggers a full git reindex immediately. |
sesh.embeddingsEnabled |
false |
Enable local semantic indexing. Powers the Knowledge tab, Ideas tab, CLAUDE.md tips, and prompt linting. Off by default — turning it on triggers a one-time ~30 MB model download (local embedder) and a full embedding pass. |
sesh.embeddingsAutoStart |
false |
When on, the embedding chain auto-runs at activation. Off by default because the local @huggingface/transformers ONNX runtime can crash the extension host on some Electron builds. With it off, indexing only runs when you toggle embeddingsEnabled, change embedder config, or invoke Sesh: Reindex embeddings. |
sesh.embedder |
"local" |
Which embedder to use. local runs entirely on-device via @huggingface/transformers (no network, no key needed — recommended). ollama targets a local Ollama server. cloud targets an OpenAI-compatible endpoint. |
sesh.embedderModel |
"" |
Override the embedder's model. Leave blank for the default: Xenova/all-MiniLM-L6-v2 (local), nomic-embed-text (ollama), text-embedding-3-small (cloud). |
sesh.embedderApiKey |
"" |
API key for the cloud embedder. Stored in plaintext in VSCode settings — only set if you accept that risk. |
sesh.embedderApiUrl |
"" |
Override the embedder endpoint URL. Blank uses the embedder's default. |
sesh.ideaMining |
false |
Mine intent-bearing user messages into the Ideas tab graveyard. Off by default. Requires embeddingsEnabled on. |
sesh.ideaMiningSinceDays |
30 |
Only mine ideas from sessions active within this many days. |
Most settings (heavy indexers, embedder config) apply live — toggling them in the Settings tab triggers onDidChangeConfiguration which tears down and rebuilds the affected services without a window reload.
Commands
Sesh: Open — opens or focuses the panel.
Sesh: Show stats — info message with the indexed session count.
Sesh: Rescan all projects — re-scan + reimport ghosts + reindex FTS.
Sesh: Show archive size — disk size of the opt-in archive directory.
Sesh: Reindex analytics — rebuild all turn, tool-call, and outcome data. Run this after a pricing-table change or if Insights numbers look wrong.
Sesh: Reindex git — re-run the full git discovery → index → link → outcome-infer pipeline. Run this after cloning a new repo or if Reviewer data looks stale.
Sesh: Reindex embeddings — re-run the full embedding indexer. Use this after switching sesh.embedder or after a long offline period.
Sesh: Suggest CLAUDE.md improvements — run the correction miner and open the Knowledge tab tips panel. Copy the surfaced patterns straight into your CLAUDE.md.
Sesh: Export style fingerprint — compute your writing metrics and save as a JSON file via the system save dialog.
Sesh: Open is wired to the activity-bar entry, the secondary-sidebar entry (right strip, alongside Claude / Codex), the editor-title button (history icon), and the welcome-view links — pick whichever fits your layout.
Storage
| Path |
Purpose |
~/.sesh/db.sqlite |
Session metadata, annotations, FTS5 index. WAL mode. |
~/.sesh/transcripts/<id>.jsonl.gz |
Optional gzipped archive (only when sesh.archiveTranscripts: true). |
~/.claude/projects/**/*.jsonl |
Claude Code source. Read-only. |
~/.codex/sessions/**/rollout-*.jsonl |
Codex CLI source. Read-only. |
Deleting ~/.sesh/db.sqlite rebuilds from source on next activation. You only lose annotations.
Development
Sesh is a TypeScript-strict, esbuild-bundled extension with a Vite-bundled React 18 webview. Tests are Vitest, manifest is in package.json.
npm install
npm run typecheck # tsc --noEmit on host
npm test # 426 tests pass (host Node binary)
npm run build # bundles extension + webview
npx @electron/rebuild -f -w better-sqlite3 -v 39.0.0 # before pressing F5
Then open this directory in VSCode and press F5 to launch the Extension Development Host.
Native binary toggling
better-sqlite3 is a native module — its prebuilt binary at node_modules/better-sqlite3/build/Release/better_sqlite3.node targets one runtime at a time:
| Use case |
Command |
| Run vitest (host Node) |
npm rebuild better-sqlite3 |
| Run extension in dev host (VSCode's Electron) |
npx @electron/rebuild -f -w better-sqlite3 -v 39.0.0 |
The 39.0.0 matches the Electron version VSCode currently bundles. Re-check /Applications/Visual Studio Code.app/Contents/Resources/app/package.json if VSCode updates and the rebuild fails.
To minimize flips during a dev session: finish all code changes, then rebuild → test → rebuild → build in one chain. When changing package.json, do a full F5 restart of the dev host — Cmd+R only reloads the webview.
Architecture
src/
├── extension.ts activate() — status bar, commands, view registration, eager indexing chain
├── host/
│ ├── seshHost.ts DB + scan + ghost import + FTS + watcher + archive + turns backfill + semantic refs
│ ├── seshPanel.ts singleton WebviewPanel, message dispatcher, light/dark iconPath
│ ├── statusBar.ts SeshStatusBar — today's spend, refreshes every 60 s
│ └── transcriptArchive.ts opt-in gzipped sidecar (sesh.archiveTranscripts)
├── messaging.ts typed host↔webview protocol
├── embed/
│ ├── types.ts Embedder interface + EmbedderConfig union
│ ├── xenovaEmbedder.ts on-device WASM via @huggingface/transformers (default)
│ ├── ollamaEmbedder.ts local Ollama HTTP
│ ├── cloudEmbedder.ts OpenAI-compatible cloud endpoint
│ ├── factory.ts createEmbedder(cfg)
│ ├── cosine.ts cosineSimilarity(a, b)
│ └── chunkText.ts sliding-window text chunker
├── db/
│ ├── connection.ts, migrate.ts, migrations/
│ ├── sessions.ts, tags.ts, categories.ts, search.ts
│ ├── turns.ts TurnRepository
│ ├── toolCalls.ts ToolCallRepository
│ ├── outcomes.ts OutcomeRepository
│ ├── analyticsQueries.ts usdForTurn · costByFile · modelLeaderboard · personalRecords · todaysStandup · recentCommitments
│ ├── commits.ts CommitRepository
│ ├── sessionCommits.ts SessionCommitRepository
│ ├── chunks.ts ChunkRepository
│ ├── embeddings.ts EmbeddingRepository
│ ├── ideas.ts IdeaRepository
│ ├── claudeMd.ts ClaudeMdSuggestionRepository
│ ├── promptLints.ts PromptLintRepository
│ └── semanticQueries.ts cosine search + idea cluster retrieval
├── git/
│ ├── repoDiscovery.ts findRepoRoot — walks up to .git
│ ├── gitLog.ts parseGitLog — numstat → Commit[] + CommitFile[]
│ ├── runGit.ts runGitLog · runCurrentBranch async shell wrappers
│ ├── gitIndexer.ts GitIndexer — incremental, mirrors TurnsIndexer lifecycle
│ ├── discoverRepos.ts caches repo_path on sessions
│ ├── linker.ts linkSessionsToCommits — Jaccard × time-overlap × decay
│ ├── runFullGitReindex.ts discovery → index → link → infer pipeline
│ └── ghCompanion.ts gh CLI wrappers (isGhAvailable · listOpenPRsWithCommits)
└── scanner/
├── jsonl.ts streaming reader — handles .jsonl.gz transparently
├── extract.ts metadata extractor (Claude Code shape)
├── transcript.ts transcript reader (Claude Code shape)
├── scan.ts walks ~/.claude/projects/, top-level *.jsonl only
├── extractTurns.ts parse JSONL → Turn[] + ToolCall[]
├── turnsIndexer.ts incremental turn indexer (eager + lazy paths)
├── outcomeInferer.ts git-aware inference: shipped · shipped-partial · reverted · abandoned
├── sessionsIndex.ts imports ghost (pruned-transcript) sessions
├── systemTags.ts shared SYSTEM_TAG_RE
├── contentIndexer.ts background FTS populate, source-aware
├── watcher.ts chokidar add/change/unlink (Claude Code only for now)
├── chunkExtractor.ts split turns into overlapping text chunks
├── embeddingIndexer.ts incremental chunk → vector indexer (cancelable)
├── ideaDetector.ts pure classifier: is this turn intent-bearing?
├── ideaIndexer.ts mine + cluster ideas from recent sessions
├── correctionMiner.ts cluster correction turns → CLAUDE.md suggestions
├── promptLinter.ts match opening prompt against correction patterns
├── styleFingerprint.ts writing metrics: sentence length, hedging rate, top tokens
├── nextSessionSuggester.ts banner text from idea clusters + recent commitments
└── codex/ Codex CLI source adapter (extract + scan + transcript + sessionText)
webview/src/
├── App.tsx 6-tab layout: Sessions · Knowledge · Insights · Ideas · Reviewer · Settings
├── messaging.ts MIRROR of host messaging.ts
├── styles.css --sesh-* design tokens, theme-decoupled muted text
├── components/
│ ├── TabBar.tsx tab navigation
│ ├── SessionsTab.tsx session list + detail pane
│ ├── InsightsTab.tsx 5 sub-views: Today · By file · Models · Records · Style
│ ├── KnowledgeTab.tsx semantic search + CLAUDE.md tips panel
│ ├── IdeasTab.tsx idea graveyard, grouped by cluster
│ ├── AnalyticsChip.tsx outcome dot · cost · model badge on session rows
│ ├── ReviewerTab.tsx 3 sub-tabs: Branch · Sessions · PRs
│ ├── SettingsTab.tsx gear-icon tab — every sesh.* toggle as switches/dropdowns
│ ├── PromptLintBadge.tsx badge on session detail when a prompt lint has fired
│ ├── NextSessionBanner.tsx banner above sessions list: idea clusters + commitments
│ └── insights/ StandupView · CostView · LeaderboardView · RecordsView · StyleView
└── hooks/ useSessions, useSessionDetail, useCategories, useAllTags, useProjects, useInsights
Design invariants
These are intentionally load-bearing — don't refactor them away without thinking hard:
- Top-level JSONL only counts as a session.
<encoded-cwd>/<id>.jsonl is a session; <encoded-cwd>/<id>/subagents/agent-*.jsonl is not — it's a subagent invocation inside the parent session.
sessions-index.json is the ghost ledger. Claude Code keeps it as a permanent record. After pruning, the JSONL is gone but the entry remains. Sesh imports those as orphaned=1 rows so the user keeps title + metadata even though the transcript is gone.
- Annotations live in Sesh's DB only. Editing a title/category/tag/notes never touches the source JSONL.
- Project / folder is intrinsic. Every session has a
cwd field in its first JSONL record — that's the "Folder" value.
- Panel resume needs cwd match. Claude Code's bundled
extension.js resolves the panel resume command against workspaceFolders[0] only — there's no API path to resume cross-workspace.
Contributing
Issues and PRs welcome. The project follows a tight TDD-discipline pattern — every meaningful behavior change ships with a unit test next to the file it touches. See the commit history for the cadence.
Before opening a PR:
npm run typecheck
npm rebuild better-sqlite3 && npm test
npx @electron/rebuild -f -w better-sqlite3 -v 39.0.0
npm run build
All four must pass.
License
MIT © Michael Bluman.