What we want
Every machine flue runs on already has a second history sitting next to the shell history: agent transcripts. Claude Code writes ~/.claude/projects/<cwd-slug>/<session-id>.jsonl, Codex writes ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl, Pi keeps its own store. flue is the fleet view of your terminals; it should be the fleet view of these too.
Concretely, a new Agents screen in the web UI:
- A browser. All agent sessions across all reachable machines: tool, project, when, first prompt, model, message count, token totals. Filterable and sortable like the sessions list.
- A viewer. Open a transcript and read it as a conversation: user and assistant turns, collapsible tool calls and results, timestamps. Fast on a 40MB file, usable on a phone, and able to follow a transcript that is still being written.
- Search. Across sessions ("where did I fix that nginx config"), by project, by tool, by date, full text into message content. Results in a bounded, ranked list that jumps into the viewer.
- Metrics. Tokens per day, per model, per project, per tool; session counts; a cost estimate. Charts, not just tables.
All of it must work from a paired remote device over the relay, not just the loopback tab.
Why
The transcripts are already there, on disk, unencrypted, and nothing renders them well. The CLIs' own resume pickers are single-machine and single-tool. And flue is uniquely placed: the daemon already lives on every machine with filesystem access, the pairing and relay story already answers "how does my phone read files on my desktop securely", and internal/daemon/file.go:27 already names ~/.claude transcripts as a motivating case for the file-read verb.
Design sketch
Transport: wire verbs, not HTTP
The relay carries only the WebSocket protocol (relay/src/hub.ts:89, internal/transport/relay/relay.go); a phone on a relay origin cannot reach any /api/* route on the daemon. So this feature is a set of control-protocol verbs, like everything else the UI does.
The good news is that half the plumbing already exists and has no consumer yet: read/file/eof/cancel plus binary FRAME_FILE 0x02 are fully implemented daemon-side (internal/daemon/file.go, 483 lines), typed in web/src/client/protocol.ts:526, and spec'd in spec/protocol.md:200, but FlueClient implements none of it. This feature would be the first consumer. The spec even anticipates the tail-follow case: an eof is not a completeness claim, and a transcript still being appended to legitimately delivers more bytes than file.size.
What is genuinely missing on the wire:
- Enumeration. There is no directory-listing verb, only
stat of caller-named paths. Rather than a generic ls, add an agent-shaped verb: agents returns the summarized session index (per tool, per project, with the summary fields above). Keeps the wire surface intentional instead of growing a general file manager.
- Ranged reads.
read streams a whole file with maxFileBytes = 8MiB truncation and maxReads = 2 per connection (file.go:29). A transcript viewer needs paged reads (byte or line ranges) and a higher or per-verb read budget; those constants were sized for "peek at a file the session mentioned", not for this.
- Search.
agents.search runs server-side. Streaming 40MB of JSONL to a phone to grep client-side is the naive path and exactly what we should not do; the daemon scans and returns bounded matches with context.
Daemon: adapters and an index
- One adapter per tool behind a common interface: locate store, enumerate session files, parse summary, parse messages, tolerate unknown line types by skipping them (these formats are unversioned and change; parsers must be forward-compatible, and the exact current shapes for all three tools need verifying against the CLIs at build time, not from memory).
- Never parse the whole store per request. A lazy index keyed by (path, mtime, size): first touch of a file parses it once into a summary record (id, project, cwd, start and end time, first user prompt, models seen, message count, token totals per type); subsequent requests reuse it; an appended file re-indexes from its previous offset. Flat JSON in
~/.config/flue/agents/ alongside sessions/, written with the existing temp-plus-rename idiom (config/paths.go:91). The config dir is 0700/0600 throughout and an index derived from ~/.claude inherits exactly that sensitivity.
- Full-text search v1 is a bounded streaming scan over the JSONL with the index narrowing candidates (by tool, project, date) first. If that proves slow at real corpus sizes, SQLite FTS is the escalation path, but it is a new dependency and should be earned by measurement, not assumed.
Metrics
Computed entirely from the index, no new collection machinery (the repo currently has none: no counters, no telemetry, nothing). Tokens by day, model, project, and tool; cache read/write split where the transcript records it; sessions per day; tool-call histograms. Cost is an estimate from a bundled pricing table and must be labeled as such, since pricing tables go stale; token counts are the durable truth. Charting is greenfield in web/ too, so the library choice is open; something small and canvas-friendly beats a kitchen sink.
Web UI
- New route in the shell:
/agents, list plus detail. The list reuses the sessions screen's shape (pure view functions like sessions/view.ts, saved-view store).
- The viewer is the first thing in this codebase that needs list virtualization; nothing in
web/ windows today and SessionTable maps every row. Take @tanstack/react-virtual or similar for the message list.
- Fan-out follows the existing fleet pattern: the web asks every online machine's client (
FleetClient, web/src/fleet/fleet.ts) and merges, the same way session lists work. No cross-machine aggregation server-side.
- Request shape copies
FlueClient.peek() (web/src/client/client.ts:526): reqId correlation, promise, timeout.
Tie-in with live sessions
Today flue's Claude awareness is a one-shot mtime guess at snapshot time (internal/session/claude.go), surfaced only as a claude --resume hint printed into revived scrollback; Info has no agent field and Codex and Pi have no detection at all. Two additive upgrades fall out of this feature:
Info gains an optional agent field (tool plus transcript session id) so a running flue session links to its transcript and the transcript viewer links back to the live terminal.
- The viewer gets "resume in a terminal": spawn
claude --resume <id> (or the Codex/Pi equivalent) in the transcript's cwd, via the existing spawn flow.
Backward compatibility
- All new wire verbs and the
Info.agent field are additive. Old daemon with new web: the verbs fail or the capability is absent from hello, and the Agents screen shows that machine as "daemon too old" rather than breaking. New daemon with old web: nothing changes, extra field ignored.
- No HTTP surface changes, so
methodPolicy and the GET-read-only invariant (server.go:531, TestNoStateChangeIsReachableByGET) are untouched. Index writes happen on the daemon's own initiative, not via a mutating route.
- Reading
~/.claude and friends does not widen the trust model: a paired device can already read those paths by name through FRAME_FILE. This feature makes that power discoverable, which is worth saying out loud in review, but it is not new power.
Open questions, worth deciding before building
- Tokens only, or cost too, in v1? Cost needs a pricing table with an update story. Suggest tokens first, cost behind it once the table's staleness is handled (bundled defaults plus user override).
- Index format. Flat JSON summaries versus SQLite from day one. Suggest flat JSON until search measurements say otherwise.
- How live is "live"? Following an in-progress transcript can be poll-on-focus (cheap, matches the 3s fleet poll) or a real tail subscription over the wire. Suggest poll first; the ranged-read verb makes either possible.
- Scope of tools. Claude, Codex, Pi to start, behind an adapter interface so others (OpenCode, Amp, whatever is next) are additions, not rewrites. Verify each tool's current on-disk format during implementation.
- Deletion and redaction. Transcripts contain secrets pasted into prompts and tool output. Is delete-from-UI in scope, and does it delete the CLI's file or only flue's index entry? Deleting another tool's data is a sharp knife; suggest out of scope for v1, index-only hide at most.
Where to look
internal/daemon/file.go (the whole file-read verb set; limits at :29), internal/wire/control.go:177 and internal/wire/binary.go:16, spec/protocol.md:200
internal/session/claude.go (existing detection), internal/session/snapshot.go:52 (ClaudeSession)
internal/daemon/server.go:435 (route table), :531 (methodPolicy), package doc at :5 for the read-only invariant
relay/src/hub.ts:89, internal/transport/relay/relay.go, web/src/relay/socket.ts (why HTTP will not travel)
web/src/client/client.ts (peek at :526 as the request pattern; no read client yet), web/src/client/protocol.ts:526
web/src/routes/sessions.tsx, web/src/sessions/view.ts, web/src/fleet/fleet.ts (list and fan-out patterns to reuse)
internal/config/paths.go:15 (config dir, 0700 discipline, atomic write idiom)
What we want
Every machine flue runs on already has a second history sitting next to the shell history: agent transcripts. Claude Code writes
~/.claude/projects/<cwd-slug>/<session-id>.jsonl, Codex writes~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl, Pi keeps its own store. flue is the fleet view of your terminals; it should be the fleet view of these too.Concretely, a new Agents screen in the web UI:
All of it must work from a paired remote device over the relay, not just the loopback tab.
Why
The transcripts are already there, on disk, unencrypted, and nothing renders them well. The CLIs' own resume pickers are single-machine and single-tool. And flue is uniquely placed: the daemon already lives on every machine with filesystem access, the pairing and relay story already answers "how does my phone read files on my desktop securely", and
internal/daemon/file.go:27already names~/.claudetranscripts as a motivating case for the file-read verb.Design sketch
Transport: wire verbs, not HTTP
The relay carries only the WebSocket protocol (
relay/src/hub.ts:89,internal/transport/relay/relay.go); a phone on a relay origin cannot reach any/api/*route on the daemon. So this feature is a set of control-protocol verbs, like everything else the UI does.The good news is that half the plumbing already exists and has no consumer yet:
read/file/eof/cancelplus binaryFRAME_FILE 0x02are fully implemented daemon-side (internal/daemon/file.go, 483 lines), typed inweb/src/client/protocol.ts:526, and spec'd inspec/protocol.md:200, butFlueClientimplements none of it. This feature would be the first consumer. The spec even anticipates the tail-follow case: an eof is not a completeness claim, and a transcript still being appended to legitimately delivers more bytes thanfile.size.What is genuinely missing on the wire:
statof caller-named paths. Rather than a genericls, add an agent-shaped verb:agentsreturns the summarized session index (per tool, per project, with the summary fields above). Keeps the wire surface intentional instead of growing a general file manager.readstreams a whole file withmaxFileBytes = 8MiBtruncation andmaxReads = 2per connection (file.go:29). A transcript viewer needs paged reads (byte or line ranges) and a higher or per-verb read budget; those constants were sized for "peek at a file the session mentioned", not for this.agents.searchruns server-side. Streaming 40MB of JSONL to a phone to grep client-side is the naive path and exactly what we should not do; the daemon scans and returns bounded matches with context.Daemon: adapters and an index
~/.config/flue/agents/alongsidesessions/, written with the existing temp-plus-rename idiom (config/paths.go:91). The config dir is 0700/0600 throughout and an index derived from~/.claudeinherits exactly that sensitivity.Metrics
Computed entirely from the index, no new collection machinery (the repo currently has none: no counters, no telemetry, nothing). Tokens by day, model, project, and tool; cache read/write split where the transcript records it; sessions per day; tool-call histograms. Cost is an estimate from a bundled pricing table and must be labeled as such, since pricing tables go stale; token counts are the durable truth. Charting is greenfield in
web/too, so the library choice is open; something small and canvas-friendly beats a kitchen sink.Web UI
/agents, list plus detail. The list reuses the sessions screen's shape (pure view functions likesessions/view.ts, saved-view store).web/windows today andSessionTablemaps every row. Take@tanstack/react-virtualor similar for the message list.FleetClient,web/src/fleet/fleet.ts) and merges, the same way session lists work. No cross-machine aggregation server-side.FlueClient.peek()(web/src/client/client.ts:526): reqId correlation, promise, timeout.Tie-in with live sessions
Today flue's Claude awareness is a one-shot mtime guess at snapshot time (
internal/session/claude.go), surfaced only as aclaude --resumehint printed into revived scrollback;Infohas no agent field and Codex and Pi have no detection at all. Two additive upgrades fall out of this feature:Infogains an optionalagentfield (tool plus transcript session id) so a running flue session links to its transcript and the transcript viewer links back to the live terminal.claude --resume <id>(or the Codex/Pi equivalent) in the transcript's cwd, via the existing spawn flow.Backward compatibility
Info.agentfield are additive. Old daemon with new web: the verbs fail or the capability is absent fromhello, and the Agents screen shows that machine as "daemon too old" rather than breaking. New daemon with old web: nothing changes, extra field ignored.methodPolicyand the GET-read-only invariant (server.go:531,TestNoStateChangeIsReachableByGET) are untouched. Index writes happen on the daemon's own initiative, not via a mutating route.~/.claudeand friends does not widen the trust model: a paired device can alreadyreadthose paths by name throughFRAME_FILE. This feature makes that power discoverable, which is worth saying out loud in review, but it is not new power.Open questions, worth deciding before building
Where to look
internal/daemon/file.go(the whole file-read verb set; limits at :29),internal/wire/control.go:177andinternal/wire/binary.go:16,spec/protocol.md:200internal/session/claude.go(existing detection),internal/session/snapshot.go:52(ClaudeSession)internal/daemon/server.go:435(route table),:531(methodPolicy), package doc at :5 for the read-only invariantrelay/src/hub.ts:89,internal/transport/relay/relay.go,web/src/relay/socket.ts(why HTTP will not travel)web/src/client/client.ts(peekat :526 as the request pattern; noreadclient yet),web/src/client/protocol.ts:526web/src/routes/sessions.tsx,web/src/sessions/view.ts,web/src/fleet/fleet.ts(list and fan-out patterns to reuse)internal/config/paths.go:15(config dir, 0700 discipline, atomic write idiom)