A local desktop GUI to discover, browse, and manage sessions created by agentic coding tools (Claude Code, Codex CLI, OpenCode, …).
- Platforms: Linux, macOS and Windows (M8).
- Language: Go (backend) + web frontend via Wails.
- Principle: local-only, read-only by default. The app never writes to a tool's own session store unless the user explicitly triggers one of two opt-in actions (see §7): a destructive action, or restoring a session from a portable bundle.
- Auto-detect installed agents and scan all their existing sessions.
- Present sessions grouped by working directory and by agent type.
- Render full session content (user/assistant messages, reasoning, tool calls and results, diffs, subagents, compaction markers).
- Fast on large histories (hundreds of MB, single transcripts > 40 MB).
- Survive format drift: unknown record types/fields are skipped, never fatal.
- Pluggable providers so new agents (Gemini CLI, Aider, Copilot CLI, Cursor…) can be added without touching the core.
- Starting/driving agents from within the app (only "copy/launch resume command", and a handoff prompt the user pastes or launches themselves).
- Syncing sessions across machines or general background network access. Remote
browsing over SSH (M7) uses only the user's existing system
sshand~/.ssh/configto connect directly to hosts they choose. On Windows, a WSL distribution the user picks is browsed the same way through the systemwsl.exe, with no ssh involved (M8-13). The app never opens listening ports, connects to telemetry services, or makes unprompted network calls. The one other network use is message translation (§9), which the user sets up and triggers. - Editing transcripts.
| Layer | Choice | Why |
|---|---|---|
| App shell | Wails v2 (Go ↔ WebView) | Native binary, Go backend, HTML/CSS frontend. Rich rendering (markdown, code highlighting, diffs) is much easier in a WebView than in Fyne/Gio. Uses WebKitGTK on Linux, WKWebView on macOS, so the same code runs on both. |
| Frontend | Svelte + TypeScript + Vite (alt: React) | Lightweight; good fit for virtualized lists. |
| Markdown / code | marked + shiki (or highlight.js) |
Transcripts are markdown-heavy. |
| SQLite (read) | modernc.org/sqlite (pure Go) |
No cgo/sqlite version coupling; open tool DBs with mode=ro. |
| Own index/cache | SQLite + FTS5 at $XDG_CACHE_HOME/agent-sessions/index-v<N>-<roots>.db |
Fast startup + full-text search; files named by schema and by a hash of the provider roots let different app versions, and processes scanning different roots, share a host safely. |
| File watching | fsnotify |
Live updates of active sessions. |
Linux build deps: gcc, pkg-config, libgtk-3-dev, libwebkit2gtk-4.1-dev
(build with -tags webkit2_41 on distros that ship only 4.1).
Decision (2026-09-27): Wails v2. v2 is the stable line; v3 is beta, and moving from v2 to v3 later is a port rather than a version bump. To keep that port cheap:
- Only
internal/appandcmd/agent-sessionsimport Wails.- The frontend reaches the backend through one thin
frontend/src/lib/api.tswrapper around the generated bindings and events. Components never importwailsjs/*directly.v2 specifics:
- Services are bound with
options.App{Bind: []interface{}{svc}}, which generates TypeScript intofrontend/wailsjs/go/....- Go → UI events use
runtime.EventsEmit(ctx, name, data), with thectxcaptured inOnStartup.- v2 supports a single window, which is enough for the three-pane layout.
cmd/
agent-sessions/ # Wails GUI entrypoint
agent-sessions-cli/ # headless: `scan`, `show <id>`, `--json` (parser debugging, tests)
internal/
model/ # unified Session / Message / Part types
provider/ # Provider interface + explicit provider Set
claude/
codex/
opencode/
index/ # cache DB, incremental rescan, FTS
watch/ # fsnotify → provider-specific invalidation
paths/ # per-OS default locations, env overrides
app/ # Wails-bound service (the API the frontend calls)
frontend/
testdata/<provider>/ # sanitized fixture sessions (golden tests)
The authoritative definitions live in
docs/plan/M0.md (M0-02 internal/model, M0-03
internal/provider). In short:
Provider:Detect,Scan(ctx, prev ScanState)(incremental: returnsChanged/Removedsessions plus a new resume state),Load(full transcript on demand),Blob(lazy large outputs and images),WatchPaths, andResumeCommand.LiveDetectoris an optional extra.SessionMeta: identity, cwd/repo root, title, model, timestamps,Counts(user / assistant / tool calls), tokens, cost, and archived/live flags.Message→Part: kindstext | reasoning | tool | patch | file | compaction | notice. A tool call and its result are merged into onetoolpart. Large outputs and images are never inlined; they are referenced by a blob key.
Verified on-disk formats are documented in docs/formats.md.
- Root:
$CLAUDE_CONFIG_DIRor~/.claude. - Sessions:
projects/<encoded-cwd>/<session-uuid>.jsonl, one JSON record per line.<encoded-cwd>is lossy (/and.→-). Never decode it; takecwdfrom the records.- Side data:
<uuid>/subagents/*.jsonl(subagent transcripts → child sessions),<uuid>/tool-results/*.txt(large tool outputs, lazy-load).
- Record
types seen:user,assistant,attachment,system(e.g.subtype: compact_boundary),ai-title,last-prompt,mode,cost-state,queue-operation,atis-latch. Ignore unknown types. - Parsing rules:
- Assistant records are split per content block, so merge by
message.id. - Thread via
uuid/parentUuid.isSidechain,isMetaflags. - Title priority: last
ai-title.aiTitle→summaryrecord → first non-meta user text. - Model:
message.model; tokens:message.usage.
- Assistant records are split per content block, so merge by
- Live detection:
~/.claude/sessions/<pid>.jsoncontainssessionId,cwd,pid. The session counts as live if the pid is alive. - Scale: 306 MB total here, largest file 38 MB. The metadata scan must
stream the file and must not build the whole transcript in memory.
- Message counts need one pass over every line. Use a fast line filter that
checks the
"type"prefix before full JSON decoding. - Cache the count together with the byte offset where the scan stopped. Sessions are append-only, so a changed file resumes from that offset instead of rescanning.
- Message counts need one pass over every line. Use a fast line filter that
checks the
- Resume:
claude --resume <id>(run incwd).
- Root:
$CODEX_HOMEor~/.codex. - Sessions:
sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl, plusarchived_sessions/.- Line 1:
session_meta(id, cwd, cli_version, git info). Thenturn_context,response_item(message,reasoning,function_call,function_call_output, …), andevent_msg(token counts, etc.).
- Line 1:
- Index DB:
state_<N>.sqlite(pick the highest N), tablethreads:id,rollout_path,cwd,title,first_user_message,model,tokens_used,archived,git_branch,created_at_ms,updated_at_ms. Use it for fast metadata when populated, and fall back to scanning rollout files.thread_spawn_edgesgives parent→child. ⚠️ No Codex sessions exist on this machine (thethreadstable is empty and there is nosessions/dir). We need a sample rollout file for fixtures.- Resume:
codex resume <id>.
- Root:
$XDG_DATA_HOME/opencodeor~/.local/share/opencode(the same path is used on macOS). - Three storage generations (v2 and v1 are read; legacy is detected only, M1-06):
opencode.db→session_v2+session_message(type∈ user, assistant, compaction, synthetic, system, idle, …;datais JSON with acontent[]of text/reasoning/tool entries). (v2.x)opencode.db→session+message+part(part types: text, reasoning, tool, patch, file, step-start/finish, compaction). (v1.x)- Legacy JSON files:
storage/session/<projectID>/<id>.json,storage/message/<sessionID>/*.json,storage/part/<messageID>/*.json.
- Dedupe by session id: the v2 row wins unless the v1 row's
time_updatedis strictly newer (Scan and Load share this rule). Here all 60 v1 sessions also exist among the 64 in v2.
- Metadata comes directly from the session row:
directory,title,parent_id(subagents),model,agent,cost,tokens_*,time_archived,summary_additions/deletions/files.project.worktreegives the repo root;worktreetable maps worktree dirs. - DB is 248 MB with an active WAL. Open with
mode=ro, keep queries short, and never hold long read transactions. - Resume:
opencode --session <id>(run indirectory).
- Group modes (toggle):
- Directory → Agent → Sessions (default)
- Agent → Directory → Sessions
- Flat, sorted by recency
- Directory normalization:
filepath.Clean, resolve symlinks, strip trailing/. - "Fold by repository" option: group by git toplevel so that subdirectories
and worktrees (
.claude/worktrees/*, OpenCodeworktreetable,git worktree list) collapse under the main repo. - Directories that no longer exist on disk are shown dimmed with a "missing" tag.
- Filters: agent, date range, model, has-subagents, archived, live-only.
- Sort: last updated (default), created, message count, tokens/cost.
- Search: FTS5 over title and message text, with results showing snippets. Clicking a result opens the session scrolled to the matching message.
- Subagent/child sessions are nested under their parent, not listed as top-level.
Three-pane layout:
┌──────────────┬─────────────────────────────┬──────────────────────────────────┐
│ Search [...] │ Filters ▾ Sort ▾ │ Title · agent badge · model │
├──────────────┼─────────────────────────────┤ cwd · branch · 128 msgs │
│ ▾ ~/ws/app │ ● Fix login bug 128💬 2m │ tokens · cost │
│ Claude (4) │ Add tests 42💬 1d │ [Copy resume cmd] [Open term] │
│ OpenCode(2)│ Refactor API 9💬 3d │──────────────────────────────────│
│ ▸ ~/ws/cli │ │ 👤 user message (markdown) │
│ ▸ ~/ws/site │ │ 🤖 assistant text │
│ │ │ ▸ reasoning (collapsed) │
│ Agents │ │ ▸ tool: bash `make test` ✓ │
│ Claude 34 │ │ ▸ patch: 3 files +40 −12 │
│ Codex 0 │ │ ── context compacted ── │
│ OpenCode 64 │ │ ▸ subagent: Explore (→ open) │
└──────────────┴─────────────────────────────┴──────────────────────────────────┘
-
Session list row (column 2): live dot, title, agent badge (in flat and agent-less group modes), message count, and relative updated time. The full timestamp shows on hover.
-
Message count definition: the number of conversation messages shown in the transcript by default:
- human user prompts, excluding meta/injected records and tool results
- assistant messages, merged per message id (Claude splits them per content block)
Tool calls/results, reasoning, and bookkeeping records are not counted. Subagent messages are counted on the subagent session, not the parent. The hover tooltip gives a breakdown:
user 12 · assistant 116 · tool calls 340. -
The transcript view is virtualized, so sessions with 10k+ records stay smooth.
-
Tool call and result are paired and collapsed by default, with syntax-highlighted input/output.
-
Toggles: show meta/system messages, reasoning, and raw JSON per record (for debugging).
-
Live sessions show a green dot, and their transcript auto-updates (tail).
-
Light and dark theme that follows the system. Keyboard navigation:
/search,j/klist,Enteropen.
| Action | Milestone | Notes |
|---|---|---|
| Copy resume command | M1 | Per-provider command plus cd <cwd>. On Windows a local command uses PowerShell 5.1 syntax (Set-Location -LiteralPath …; & …); remote commands stay POSIX (M8-06), to paste into a shell opened with ssh <alias> or, for a WSL distribution, wsl -d <distro> (M8-13). |
| Open in terminal | M8 | Local Resume and Continue in open a new terminal window in the session directory running the agent CLI, found on PATH or where standalone installs, desktop apps and editor extensions keep it. Windows uses a PowerShell console (see docs/plan/M8.md M8-11). Linux and macOS use the terminal app chosen in Settings from the ones installed (GNOME Terminal, Ptyxis, Console, Konsole, Ghostty, kitty, …; Terminal, iTerm2, Ghostty on macOS), or one picked automatically (M8-12). On Windows, a WSL distribution's sessions open a console that runs the distribution's Linux agent in the project directory and stays open in the user's shell; the script is built by the server in the distribution, which skips a Windows copy of the agent that WSL's appended PATH finds (M8-13). SSH sessions keep Copy. |
| Open terminal in cwd | M3 | A plain shell in the session directory, in the same chosen terminal. |
| Reveal source file | M1 | xdg-open / open -R. |
| Export session | M3 | Markdown, JSON (unified model), HTML. |
| Export bundle | M6 | Portable <title>.agent-session.zip (manifest v1, see docs/plan/M6.md). Complete (private) is the default and includes the verbatim native records, so it can be restored; share-safe is redacted and handoff-only. The dialog warns that a complete bundle can contain secrets. |
| Continue in… | M6 | Cross-agent handoff: a pre-rendered Markdown context (goal, user messages, working state, recent turns) delivered as the first prompt of the target agent. Native transcripts are not converted. |
| Import bundle | M6 | Asks for the directory to import into first (a directory row's menu, or the Import button, which opens a small directory chooser: a path field, the directories that already have sessions, and on this computer a Browse button for the folder dialog), then opens a bundle the user picked, shows its summary, and offers import or "continue in". The original path is never reused by default: a bundle moves work to another device. |
| Restore | M6 | "Import" in the bundle dialog. Writes the native records into the source agent's store under the chosen directory (cwd rewritten) so its own resume command works. Previewed with a short-lived HMAC token, refused while live, and never overwrites an existing session; an existing id can be imported as a copy with a new id (not for OpenCode). The explicit Import confirmation is the consent, so there is no settings gate. |
| Delete / archive | M4, opt-in | Destructive. Requires confirmation. Claude files are moved to the OS trash (not rm). Codex uses codex delete, and OpenCode deletes v2 copies via opencode session delete and v1 rows via one SQL transaction; both are permanent and need the separate allow_permanent_delete opt-in. Never run while a session is live or recently updated. |
- Incremental index: key file-based sessions by
(path, size, mtime)and re-parse only changed files. Append-only JSONL files (Claude, Codex) resume from the last scanned byte offset, which keeps message counts cheap to maintain. If the file shrank, rescan it fully. - OpenCode: query
time_updated > last_seen. Message counts come from SQL aggregates:session_messagerows with typeuser/assistant- v1 fallback:
messagerows grouped by role
- Codex: count
response_itemmessages with roleuser(excluding injected environment/instructions context) and roleassistant. - Targets (≈300 MB history): cold first scan < 5 s, warm start < 300 ms to first paint, opening a 40 MB transcript < 1 s to first screen (stream and render progressively).
- Scan providers concurrently with a bounded worker pool.
- Per-record parse errors are counted and surfaced in a "diagnostics" panel, never crashing the scan.
- Record provider format versions (
version,cli_version) to diagnose drift.
- Network use only through the user's own system
ssh, to hosts they explicitly pick. No listening ports, no telemetry, and no network calls on the remote beyond the ssh session stdio process. - A WSL distribution is reached through the system
wsl.exeon the same machine, not the network: no ssh, keys or~/.ssh/config. The host picker reads the registered distributions from the current user's registry and starts nothing until the user connects. - Translation is the only other network use. It sends the text of the one
message the user chooses to translate to the OpenAI-compatible endpoint they
configured, and nothing before that is set up. The request carries the API
key as a Bearer token and goes through Go, not the WebView; errors never
include the key. The key is stored in
config.toml(0600) and the UI never shows it again once saved. - Never read credential files (
.credentials.json,auth.json, OpenCodecredential/accounttables). - Treat transcript content as untrusted when rendering: sanitize markdown/HTML and allow no script execution in the WebView.
- The cache DB contains transcript text. Store it with
0600permissions and offer "clear cache". - Bundles can hold secrets. A complete bundle is a verbatim copy of the
native records plus the full transcript, so it may contain credentials the
user typed or tools printed. It is written
0600, the export dialog warns about this, and share-safe bundles run the redaction pass (private keys, known token prefixes, JWTs,KEY=assignments, home paths rewritten to~) and omit the native records entirely. Redaction is best-effort, not a guarantee; counts per rule are shown before saving.
$XDG_CONFIG_HOME/agent-sessions/config.toml:
- Per-provider enable flag and root path overrides (supporting multiple roots,
e.g. several
CLAUDE_CONFIG_DIRs). - Default grouping, hidden directories, theme.
- The terminal app (
[terminal] app): an id of an installed terminal, never a command; empty means Automatic. It belongs to this computer, whichever host is selected. - Translation (
[translate]):base_url,api_key,modelandlanguage(Vietnamese by default). Values are plain printable strings.
- Paths are mostly identical (
~/.claude,~/.codex,~/.local/share/opencode). Centralize them ininternal/pathswith_darwin.go/_linux.go. - Cache at
~/Library/Caches/agent-sessions, config at~/Library/Application Support/agent-sessions. - Build a universal binary (arm64 + amd64). Code signing and notarization are needed for distribution.
- Live-pid check: Linux reads
/proc/<pid>and matches the start time to guard pid reuse; macOS needs an equivalent (M5-01/02).
- Golden tests per provider:
testdata/<provider>/*→ expected unified JSON. - Fixtures cover every OpenCode generation, Claude compaction/subagents/split
assistant blocks, and Codex with and without the
threadsindex. - A fuzz test for JSONL parsers (truncated or partially written last line, which is normal for live sessions).
- A fuzz test for the bundle reader (zip-slip, oversize, bad checksums, unsafe IDs) and golden tests for the handoff renderer (per source→target pair, budget trimming, injected markup).
- The CLI
agent-sessions-cli scan --jsonis used in CI without a display.
| # | Scope |
|---|---|
| M0 | Repo scaffold, unified model, provider interface, Claude provider, headless CLI, fixtures. |
| M1 | OpenCode + Codex providers; Wails app with grouping tree, session list, and transcript viewer; copy resume / reveal file. ← MVP |
| M2 | Cache index + FTS search, fsnotify live updates, live badges, diagnostics panel. |
| M3 | Filters/sort polish, export, open-terminal, repo folding, keyboard nav, stats (tokens/cost per dir/agent). |
| M4 | Opt-in destructive actions (trash/archive). |
| M5 | macOS build, signing, packaging (Linux: AppImage/.deb; macOS: .dmg). |
| M6 | Portable session bundles (export, import, opt-in native restore) and cross-agent handoff. |
| M7 | Remote over SSH (VS Code Remote-SSH style): headless server via system SSH, browse, search, and manage remote sessions with full local UI fidelity. |
| M8 | Windows support: paths, Recycle Bin, PowerShell commands, open in terminal, MSI; key/agent-only SSH; WSL distributions as remotes through wsl.exe (see docs/plan/M8.md). |
- Scope of "manage": is read-only and resume enough for v1, or is delete/archive needed early?
- Frontend framework: Svelte (recommended) or React?
- Codex samples: can you produce a real Codex session for fixtures?
- More agents later? Gemini CLI, Aider, Copilot CLI, Cursor, … (these affect how generic the model must be).
- Distribution: personal use only, or packaged releases?