From bd7566e798d4f42414e9e91a981d279225692501 Mon Sep 17 00:00:00 2001 From: tdwd Date: Tue, 8 Sep 2026 11:36:25 +0200 Subject: [PATCH] docs: stop describing a two-backend program as a claude one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README opened "A Bubble Tea TUI over the Claude Code stream-json protocol", the WRITEUP called itself a harness for Claude on a Max plan, and BUILD.md listed only the claude CLI as a requirement. All three were written when there was one backend and none of them had caught up. The feature list needed more than a find-and-replace. Several bullets describe things codex does not do — the approval bar, the command palette, AskUserQuestion — so a reader scanning it would conclude the codex backend does them too. It now says which backend it describes and points at the section that lists the differences, rather than qualifying every bullet. entClaude becomes entAgent. A codex reply was being stored under a constant named for the other backend, which is a small lie every reader of the codex adapter had to step over. -resume's help said "claude session id". It takes a claude session or a codex thread now, and the picker sets it from either. --- BUILD.md | 5 ++-- README.md | 57 ++++++++++++++++++++++++++++++--------------- WRITEUP.md | 24 ++++++++++++------- asset_gen_test.go | 2 +- chrome_test.go | 2 +- codexengine_test.go | 4 ++-- codexitems.go | 2 +- jump_test.go | 2 +- main.go | 2 +- model.go | 4 ++-- perf_bench_test.go | 4 ++-- render.go | 2 +- render_test.go | 2 +- replay_test.go | 4 ++-- sidebar_test.go | 2 +- stream.go | 2 +- stream_test.go | 4 ++-- transcript.go | 2 +- 18 files changed, 76 insertions(+), 50 deletions(-) diff --git a/BUILD.md b/BUILD.md index efcf904..32e240e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,7 +1,8 @@ # Building cathode locally -Requirements: **Go 1.22+** and the **`claude` CLI** (logged in with your Pro/Max -account: `claude login`). +Requirements: **Go 1.22+** and the CLI of whichever backend you drive — the +**`claude` CLI** logged in with your Pro/Max account (`claude login`), and/or +the **`codex` CLI** logged in with ChatGPT (`codex login`) for `-backend codex`. ```bash cd cathode diff --git a/README.md b/README.md index 01136a9..c41abe0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Cathode -A **Bubble Tea TUI over the Claude Code stream-json protocol** (wordmark: `cath0d3`). +A **Bubble Tea TUI over an agent CLI's own protocol** (wordmark: `cath0d3`). +Drives `claude` over Claude Code stream-json, or `codex` over its `app-server` +JSON-RPC — see [Backends](#backends).

cath0d3 boot splash

@@ -10,22 +12,26 @@ A **Bubble Tea TUI over the Claude Code stream-json protocol** (wordmark: `cath0 Rendered in the built-in **Catppuccin Mocha** theme — the look is switchable, see Themes. The agent loop, context management, tool execution, and auth all live in the -official `claude` binary, which runs as a long-lived subprocess. This program -owns only the terminal UI and the stdin/stdout plumbing — so you build your own -experience without re-implementing an agent, and you ride your **Max -subscription** because we never set an API key. +official vendor binary, which runs as a long-lived subprocess. This program owns +only the terminal UI and the stdin/stdout plumbing — so you build your own +experience without re-implementing an agent, and you ride your **subscription** +because we never set an API key. ## Features -- **Rides your Pro/Max subscription** — drives the real `claude` CLI as a subprocess and scrubs `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` from its env, so it never silently falls back to API billing. +This list describes the claude backend, which is the complete one. The codex +backend shares the UI but not all of it — [Backends](#backends) says exactly +what differs. + +- **Rides your subscription** — drives the real vendor CLI as a subprocess and scrubs the API-key variables from its env, so it never silently falls back to API billing. Pro/Max on claude, ChatGPT on codex. - **Four permission modes** — `plan`, `ask`, `build` (auto-accept edits), `bypass`; cycle with `shift+tab` or `/mode`. - **Inline approvals** — in `ask` mode every gated tool call raises a `[ENTER] allow · [ESC] deny` bar (served by a hand-rolled in-process MCP permission server); edits show the diff before you decide. -- **Answers Claude's questions** — when Claude asks (its `AskUserQuestion` tool), the options pop up as a picker to choose from — always presented (never auto-approved, even in `build`/`bypass`), so a question actually waits for your answer instead of erroring out. +- **Answers the agent's questions** — when claude asks (its `AskUserQuestion` tool), the options pop up as a picker to choose from — always presented (never auto-approved, even in `build`/`bypass`), so a question actually waits for your answer instead of erroring out. - **Visual diff cards** — `Edit` / `Write` / `MultiEdit` render as line-numbered red/green diffs instead of raw JSON, unified or side-by-side **split** (`/diff` or `/settings`). -- **Markdown replies** — Claude's output is rendered with Glamour and reflows on resize. +- **Markdown replies** — the agent's output is rendered with Glamour and reflows on resize. - **Clickable links** — `http(s)` URLs in the transcript are wrapped as OSC 8 terminal hyperlinks (Cmd/Ctrl-click, or a plain click with `/mouse` off). - **Extended thinking** — the model's reasoning renders dim above its reply; a hook that blocks or fails surfaces too (routine ones stay quiet). -- **Session resume** — `ctrl+r` (or `/sessions`) fuzzy-filters `claude`'s own session history and re-execs into the one you pick. +- **Session resume** — `ctrl+r` (or `/sessions`) fuzzy-filters your session history for the running backend and re-execs into the one you pick. - **Command palette** — `ctrl+t` (or `/commands`) browses every command — our in-process ones plus claude's built-ins, **skills**, and **plugin** commands (from the init handshake) — and runs or forwards the one you pick; `/agents` lists subagents. Any `/command` we don't own is forwarded to claude, so custom & plugin commands work. - **11 themes + header animations** — `/theme` and `/settings`, with live preview, persisted across launches (see [Themes](#themes)). - **Live status bar** — permission mode, session id, git branch, a context-pressure gauge that auto-grows 200K → 2M, output tokens, and running cost. @@ -33,15 +39,16 @@ subscription** because we never set an API key. - **Bring your own tools** — point `-mcp` at a `.mcp.json` to wire extra MCP tools alongside the built-in approvals server. - **Multi-line input** — Enter sends; insert a line break with `Alt+Enter`, `Ctrl+J`, or a trailing `\`. The prompt grows with your draft — line breaks *and* soft-wrap in narrow windows — up to 8 rows, then scrolls. - **Jump back through your prompts** — `Shift+↑` / `Shift+↓` scroll the transcript one *turn* at a time, parking each of your past prompts at the top of the view; stepping past the newest one drops you back at the live bottom. -- **Prompt history & steering** — `↑` / `↓` recalls past prompts (use `Ctrl+↑/↓` while composing a multi-line draft, where `↑/↓` move between lines); type while Claude is busy and the message is injected into the running turn, so you can course-correct mid-flight instead of waiting for it to finish (`Esc` interrupts the turn to undo a mis-sent steer). +- **Prompt history & steering** — `↑` / `↓` recalls past prompts (use `Ctrl+↑/↓` while composing a multi-line draft, where `↑/↓` move between lines); type while the agent is busy and the message is injected into the running turn, so you can course-correct mid-flight instead of waiting for it to finish (`Esc` interrupts the turn to undo a mis-sent steer). ## Why this architecture (vs forking Crush/OpenCode) Those are native API-client agents: to use Max they route a subscription OAuth token through the API, the pattern Anthropic restricted in early 2026. Here the -engine *is* Claude Code, so subscription use stays inside its intended path. We -borrow their **TUI craft** (all MIT-licensed) — markdown rendering, message -cards, plan/build modes — not their engine. +engine *is* the vendor's own CLI — Claude Code, or codex — so subscription use +stays inside its intended path, and the same argument holds for both. We borrow +their **TUI craft** (all MIT-licensed) — markdown rendering, message cards, +plan/build modes — not their engine. ## Run it @@ -49,10 +56,13 @@ cards, plan/build modes — not their engine. claude login # one-time, with your Pro/Max credentials only go mod download # fetch deps (go.sum is checked in) go run . # AUTO (build) by default; -mode ask | plan | bypass to switch +go run . -backend codex # or drive codex instead (needs `codex login`) ``` Preflight: run `claude` once interactively and confirm `/status` shows the -subscription route (not API credits) before relying on this. +subscription route (not API credits) before relying on this. On codex, run +`codex exec` once — `codex login status` reports stored state and claims success +even when the token has expired, so it is not a check. ## Build & install @@ -150,7 +160,11 @@ Small files by responsibility (the project keeps each one scannable). | file | role | |------|------| | `main.go` | flags, mode→permission mapping, wires engine + Bubble Tea program + reader goroutine | +| `backend.go` | the `Engine` seam: the calls the UI makes, and nothing about the wire format | +| `backendpick.go` | picks the backend `-backend` asked for; the only file that knows both exist | | `engine.go` | the long-lived `claude` subprocess: spawn, env-scrub, bidirectional NDJSON stdin/stdout | +| `codex*.go` | the codex backend: JSON-RPC framing, thread and turn lifecycle, event adapter | +| `agentname.go` | every user-visible mention of the agent, so a label cannot name the wrong one | | `events.go` | `Envelope` structs + parser for the stream-json output | | `control.go` | control-request envelopes on stdin (set permission mode, interrupt) | | `stream.go` | routes one parsed envelope into the model (`handleEvent`) | @@ -195,7 +209,7 @@ Small files by responsibility (the project keeps each one scannable). | `commands.go` | the slash-command table + help modal | | `settings.go` | persisted settings (header / theme / fps / diff / sidebar) + their pickers | | `approvals.go` | the in-process MCP permission server (`--permission-prompt-tool`) | -| `question.go` | intercepts Claude's `AskUserQuestion` and answers it via a picker | +| `question.go` | intercepts claude's `AskUserQuestion` and answers it via a picker | **State & persistence** @@ -218,7 +232,7 @@ splash (`splash.go`) opens with the wordmark, a faux modem handshake, and a `press [ENTER] to logon` prompt (dismissed by the first keypress). Discipline: the leet/studly/ornament treatment runs on *chrome only* — banner, -dividers, status, labels, splash. Claude's replies and the diff code stay +dividers, status, labels, splash. The agent's replies and the diff code stay plain and readable. The `leet`, `studly`, `flavor`, and `sceneDivider` helpers live in `text.go`; reskin by adding or editing a palette row (ten colors) in `theme.go` — that's how all 12 built-in themes are defined (see @@ -234,7 +248,7 @@ The splash shows one of several wide block logos at random each launch variant by running `figlet -f -w 200 "cath0d3" | tr '\140' "'"` (any font — `colossal`, `epic`, `poison`, `cosmic`, or `toilet -f pagga` for shade-block CP437) and pasting the output as a new entry; narrow terminals fall -back to the compact `logoCompact`. While Claude works, an animated throbber runs in the +back to the compact `logoCompact`. While the agent works, an animated throbber runs in the status bar; choose its frames with `-spinner` (the `shade` pulse `░▒▓█` and the `scan` knight-rider are the most period-correct). @@ -244,7 +258,7 @@ Done: markdown rendering (Glamour), bordered message cards, plan/build/ask modes, MCP tool-wiring hook, visual diff cards for `Edit`/`Write`/`MultiEdit` (unified and side-by-side split), the inline permission/approval pane (in `ask` mode each gated tool routes through our in-process MCP server and raises an -`[ENTER] allow / [ESC] deny` bar, diffs shown first), Claude's questions +`[ENTER] allow / [ESC] deny` bar, diffs shown first), the agent's questions answered via a picker, multi-line input, session resume, 11 themes, extended thinking, clickable links, slash-command forwarding (skills & plugins), and the merged command palette. @@ -253,7 +267,7 @@ Next / deferred: (a) token-by-token streaming via `--include-partial-messages` ( off against markdown); (b) syntax-token highlighting inside the diff — chroma is already in the tree via glamour, so per-line token coloring on top of the red/ green background is a natural follow-on; (c) multi-select and free-text "Other" -answers for Claude's questions (single-select works today). +answers for the agent's questions (single-select works today). ## Known sharp edges @@ -261,6 +275,11 @@ answers for Claude's questions (single-select works today). the protocol; its shape matches the Agent SDK streaming-input format. - `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` are stripped from the subprocess env on purpose — either present would silently bill the API. +- On codex, `OPENAI_API_KEY` and `OPENAI_BASE_URL` are stripped for symmetry. + The key is belt-and-braces there: codex reads its credential from + `~/.codex/auth.json` and an environment key does not divert billing, verified + by probe. `OPENAI_BASE_URL` is the one that matters, because it decides which + host the conversation is sent to. - `CLAUDE_CODE_CHILD_SESSION` is stripped too. It is set inside a running Claude Code session and marks children as subsessions, which turns transcript saving off — so cathode started from inside one would leave `ctrl+r` empty and diff --git a/WRITEUP.md b/WRITEUP.md index a3e40a9..f87f6d7 100644 --- a/WRITEUP.md +++ b/WRITEUP.md @@ -1,6 +1,7 @@ # Cathode -*A personal, BBS-styled terminal harness for Claude — running on your Max plan.* +*A personal, BBS-styled terminal harness for a coding agent — running on your +subscription, not the API.* > The binary is `cathode`, the Go module is `ccharness`, and the wordmark renders > as `cath0d3` (`appName` in `theme.go`). The repo lives at @@ -8,22 +9,27 @@ ## What it is -Cathode is a single-binary terminal UI that drives Claude Code. You type, Claude -works, and the conversation — replies, tool calls, file edits — streams into a -custom TUI with a 90s bulletin-board aesthetic. It is built for one user (you), -on a Mac and on Ubuntu, and it bills against your Claude Max subscription rather -than the pay-per-token API. +Cathode is a single-binary terminal UI that drives a coding agent's own CLI. You +type, the agent works, and the conversation — replies, tool calls, file edits — +streams into a custom TUI with a 90s bulletin-board aesthetic. It is built for +one user (you), on a Mac and on Ubuntu, and it bills against your subscription +rather than the pay-per-token API. + +Two backends: Claude Code over its stream-json protocol, and codex over its +`app-server` JSON-RPC (`-backend codex`). The rest of this piece describes the +claude one, which came first and is the more complete of the two — the codex +backend is documented in the README. The design carries a deliberate BBS nod: on a board, a *door* was an external program the BBS shelled out to — door games and the like. This does exactly -that with the `claude` binary (the harness is the board; Claude is the door), +that with the agent's binary (the harness is the board; the agent is the door), which is why the project was first called *Doorway*. *Cathode* keeps the same era's glow — the CRT the whole aesthetic is drawn on. ## The constraint that shaped everything -The whole design follows from one requirement: **use the Max subscription, not -API billing.** That sounds like a small detail, but it dictates the +The whole design follows from one requirement: **use the subscription, not API +billing.** That sounds like a small detail, but it dictates the architecture, because there are only two ways software talks to Claude: The clean way is to drive the official `claude` binary as a subprocess. The diff --git a/asset_gen_test.go b/asset_gen_test.go index 96ad931..c4f3313 100644 --- a/asset_gen_test.go +++ b/asset_gen_test.go @@ -111,7 +111,7 @@ func previewModel(banner string) model { } m.entries = []entry{ {kind: entUser, text: "refactor add() to take a third arg and update the caller"}, - {kind: entClaude, text: "Here's the change to both the function and its caller:"}, + {kind: entAgent, text: "Here's the change to both the function and its caller:"}, {kind: entDiff, diffs: []fileDiff{{ file: "math.go", old: "func add(a, b int) int {\n\treturn a + b\n}", diff --git a/chrome_test.go b/chrome_test.go index d3562f0..4cec346 100644 --- a/chrome_test.go +++ b/chrome_test.go @@ -78,7 +78,7 @@ func TestFrameFitsHeight(t *testing.T) { m.resizeViewport() m.makeRenderer() for i := 0; i < 80; i++ { - m.add(entClaude, "streamed assistant output line") + m.add(entAgent, "streamed assistant output line") } m.busy = true m.resizeViewport() diff --git a/codexengine_test.go b/codexengine_test.go index a75ac03..01ba16e 100644 --- a/codexengine_test.go +++ b/codexengine_test.go @@ -201,8 +201,8 @@ func TestCodexAdapterMapsFramesToEntries(t *testing.T) { Params: json.RawMessage(`{"item":{"type":"agentMessage","id":"m1","text":"hello there"}}`), }) last := m.entries[len(m.entries)-1] - if last.kind != entClaude || last.text != "hello there" { - t.Errorf("agent message = %+v, want entClaude", last) + if last.kind != entAgent || last.text != "hello there" { + t.Errorf("agent message = %+v, want entAgent", last) } // The user's own turn is already in the transcript; echoing it would double it. diff --git a/codexitems.go b/codexitems.go index f814fa0..bc26ec5 100644 --- a/codexitems.go +++ b/codexitems.go @@ -37,7 +37,7 @@ func (m *model) codexItem(f codexFrame, started bool) { return // text arrives on completion } if t := strings.TrimSpace(head.Text); t != "" { - m.add(entClaude, t) + m.add(entAgent, t) } case "reasoning": if started { diff --git a/jump_test.go b/jump_test.go index 44e23da..8cf7f3f 100644 --- a/jump_test.go +++ b/jump_test.go @@ -19,7 +19,7 @@ func jumpModel() model { for _, q := range []string{"first question", "second question", "third question"} { m.entries = append(m.entries, entry{kind: entUser, text: q}, - entry{kind: entClaude, text: reply}) + entry{kind: entAgent, text: reply}) } m.rebuild() return m diff --git a/main.go b/main.go index 54cb6b0..1021b6c 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func main() { modelID := flag.String("model", "", "pin a model (e.g. sonnet); empty uses account default") spin := flag.String("spinner", "bar", "working throbber: bar | shade | block | arrow | scan") dbg := flag.String("debug", "", "tee raw stream-json and MCP traffic to this logfile") - resume := flag.String("resume", "", "claude session id to resume (also set automatically when picking from Ctrl-R)") + resume := flag.String("resume", "", "session (claude) or thread (codex) id to resume; also set when picking from Ctrl-R") ctx := flag.String("ctx", "200k", "context window for the pressure gauge — \"200k\", \"500k\", \"1m\", or a raw token count. auto-grows if observed input exceeds it.") flag.Parse() diff --git a/model.go b/model.go index 1b9f896..3347596 100644 --- a/model.go +++ b/model.go @@ -24,8 +24,8 @@ import ( type entryKind int const ( - entUser entryKind = iota - entClaude + entUser entryKind = iota + entAgent // an assistant reply, from whichever backend is running entThinking entTool entToolResult diff --git a/perf_bench_test.go b/perf_bench_test.go index 495eb07..a8e0978 100644 --- a/perf_bench_test.go +++ b/perf_bench_test.go @@ -38,7 +38,7 @@ func benchModel(n int) model { case 0: m.entries = append(m.entries, entry{kind: entUser, text: fmt.Sprintf("question number %d about the code", i)}) case 1: - m.entries = append(m.entries, entry{kind: entClaude, text: fmt.Sprintf("Here is a **markdown** reply #%d with a list:\n\n- one\n- two\n- three\n\nand a `code` span.", i)}) + m.entries = append(m.entries, entry{kind: entAgent, text: fmt.Sprintf("Here is a **markdown** reply #%d with a list:\n\n- one\n- two\n- three\n\nand a `code` span.", i)}) case 2: m.entries = append(m.entries, entry{kind: entTool, toolName: "Bash", toolInput: json.RawMessage(`{"command":"go test ./..."}`)}) case 3: @@ -59,7 +59,7 @@ func BenchmarkAddEntry(b *testing.B) { base := benchModel(n) b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) { for i := 0; i < b.N; i++ { - base.entries = append(base.entries, entry{kind: entClaude, text: "a **new** reply with `code`"}) + base.entries = append(base.entries, entry{kind: entAgent, text: "a **new** reply with `code`"}) base.rebuild() } }) diff --git a/render.go b/render.go index 28053c7..938b855 100644 --- a/render.go +++ b/render.go @@ -82,7 +82,7 @@ func (m *model) renderEntry(e entry) string { switch e.kind { case entUser: return userBox.Render(cYou.Render(ornBullet+" "+studly("you")) + "\n" + e.text) - case entClaude: + case entAgent: body := e.text if m.md != nil { if out, err := m.md.Render(e.text); err == nil { diff --git a/render_test.go b/render_test.go index e0af205..8857efa 100644 --- a/render_test.go +++ b/render_test.go @@ -19,7 +19,7 @@ func TestRebuildRendersMarkdown(t *testing.T) { t.Fatal("glamour renderer not constructed") } m.add(entUser, "fix the bug in **main.go**") - m.add(entClaude, "Here's a fix:\n\n```go\nfmt.Println(\"hi\")\n```\n\n- step one\n- step two") + m.add(entAgent, "Here's a fix:\n\n```go\nfmt.Println(\"hi\")\n```\n\n- step one\n- step two") m.add(entTool, "Edit\n{\"file\":\"main.go\",\"old\":\"x\",\"new\":\"y\"}") m.add(entInfo, "— done · 0.0012 USD —") out := strings.ToLower(m.vp.View()) diff --git a/replay_test.go b/replay_test.go index d0c1daa..0fef632 100644 --- a/replay_test.go +++ b/replay_test.go @@ -107,7 +107,7 @@ func TestLoadPriorTranscriptHidesMeta(t *testing.T) { {kind: entUser, text: "commit to 81"}, {kind: entUser, text: "/compact"}, {kind: entInfo, text: compactDoneText}, - {kind: entClaude, text: "picking up #42"}, + {kind: entAgent, text: "picking up #42"}, } if len(entries) != len(want) { t.Fatalf("entries = %d, want %d: %+v", len(entries), len(want), entries) @@ -148,7 +148,7 @@ func TestLoadPriorTranscriptAutoCompact(t *testing.T) { want := []entry{ {kind: entUser, text: "carry on"}, {kind: entInfo, text: compactDoneText}, - {kind: entClaude, text: "on it"}, + {kind: entAgent, text: "on it"}, } if len(entries) != len(want) { t.Fatalf("entries = %d, want %d: %+v", len(entries), len(want), entries) diff --git a/sidebar_test.go b/sidebar_test.go index 1ddcdc3..751ba2a 100644 --- a/sidebar_test.go +++ b/sidebar_test.go @@ -16,7 +16,7 @@ func TestSidebarPosition(t *testing.T) { m.vp = newTranscriptViewport(100-1-sidebarWidth, 4) m.ready = true m.makeRenderer() - m.entries = []entry{{kind: entClaude, text: "the reply"}} + m.entries = []entry{{kind: entAgent, text: "the reply"}} m.rebuild() return stripANSI(strings.SplitN(m.renderBody(), "\n", 2)[0]) // first row } diff --git a/stream.go b/stream.go index d6dac69..4743e3b 100644 --- a/stream.go +++ b/stream.go @@ -106,7 +106,7 @@ func (m *model) handleEvent(e Envelope) { switch b.Type { case "text": if t := strings.TrimSpace(b.Text); t != "" { - m.add(entClaude, t) + m.add(entAgent, t) } case "thinking": // Extended thinking — show it (dim) when present; many turns carry diff --git a/stream_test.go b/stream_test.go index 07fcadd..5640770 100644 --- a/stream_test.go +++ b/stream_test.go @@ -15,7 +15,7 @@ func TestThinkingBlocks(t *testing.T) { if len(m.entries) != 2 { t.Fatalf("want a thinking + a claude entry, got %d", len(m.entries)) } - if m.entries[0].kind != entThinking || m.entries[1].kind != entClaude { + if m.entries[0].kind != entThinking || m.entries[1].kind != entAgent { t.Fatalf("kinds: got %v,%v want thinking,claude", m.entries[0].kind, m.entries[1].kind) } @@ -24,7 +24,7 @@ func TestThinkingBlocks(t *testing.T) { {Type: "thinking", Thinking: " "}, // empty after trim → skipped {Type: "text", Text: "hi"}, }}}) - if len(m2.entries) != 1 || m2.entries[0].kind != entClaude { + if len(m2.entries) != 1 || m2.entries[0].kind != entAgent { t.Fatalf("empty thinking should be skipped, got %d entries", len(m2.entries)) } } diff --git a/transcript.go b/transcript.go index 7359743..7b8787c 100644 --- a/transcript.go +++ b/transcript.go @@ -148,7 +148,7 @@ func loadPriorTranscript(sessionID string, maxEntries int) (entries []entry, ctx } flushCompact() if t := strings.TrimSpace(c.Text); t != "" { - entries = append(entries, entry{kind: entClaude, text: t}) + entries = append(entries, entry{kind: entAgent, text: t}) } case "tool_use": flushCompact()