diff --git a/README.md b/README.md index 8875c13..6614efd 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ **A beautiful [Bubble Tea](https://github.com/charmbracelet/bubbletea) terminal interface for the [odek](https://github.com/BackendStack21/odek) agent.** -[πŸ“Ί Watch intro β†’](bodek-intro-july-2026.mov) - ``` β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ @@ -23,9 +21,32 @@ approvals, skills, and memory β€” as a polished TUI. Every bit of agent behaviour (tools, danger gating, sandbox, skills, memory, sessions) comes from **odek itself**; bodek never re-implements any of it. +## Quick start + +```bash +# 1 Β· Install the engine and the TUI +go install github.com/BackendStack21/odek/cmd/odek@latest +go install github.com/BackendStack21/bodek/cmd/bodek@latest + +# 2 Β· Provide an LLM key (any OpenAI-compatible provider) +export ODEK_API_KEY= + +# 3 Β· Chat +bodek +``` + +Prefer a compiled binary? Grab one from the +[releases page](https://github.com/BackendStack21/bodek/releases) β€” see +[Install](#install). + +**Contents:** [What & why](#what--why) Β· [Install](#install) Β· +[Usage](#usage) Β· [Features](#features) Β· [Using bodek](#using-bodek) Β· +[Configuration](#configuration) Β· [Security model](#security-model) Β· +[Troubleshooting](#troubleshooting) Β· [Development](#development) + --- -## Why a separate front-end? +## What & why odek already ships a streaming WebSocket protocol (the one its Web UI speaks). bodek reuses that exact protocol from the terminal, which means: @@ -44,7 +65,7 @@ bodek reuses that exact protocol from the terminal, which means: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” WebSocket (RFC 6455, JSON) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ bodek β”‚ ◄────────────────────────────► β”‚ odek serve β”‚ β”‚ (Bubble Tea) β”‚ tokens Β· tools Β· approvals β”‚ (ReAct engine, β”‚ -β”‚ TUI client β”‚ β”‚ tools, sandbox) β”‚ +β”‚ TUI client β”‚ β”‚ tools, sandbox) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` @@ -56,7 +77,7 @@ bodek reuses that exact protocol from the terminal, which means: engine. See [odek's install instructions](https://github.com/BackendStack21/odek) (any OpenAI-compatible provider key via `ODEK_API_KEY`). -### Prebuilt binaries (bodek only) +### Prebuilt binaries Download the latest compiled binary from the [releases page](https://github.com/BackendStack21/bodek/releases) β€” archives @@ -85,7 +106,7 @@ go install github.com/BackendStack21/odek/cmd/odek@latest go install github.com/BackendStack21/bodek/cmd/bodek@latest # Provide an LLM key (any OpenAI-compatible provider) -export ODEK_API_KEY=sk-... +export ODEK_API_KEY= bodek ``` @@ -122,13 +143,131 @@ detected and connected to as before. Configuration (model, base URL, API key, MCP servers, memory, skills) is read by `odek serve` from its usual chain β€” `~/.odek/config.json` β†’ `./odek.json` β†’ -`ODEK_*` env vars β€” so bodek inherits whatever you've already set up. +`ODEK_*` env vars β€” so bodek inherits whatever you've already set up. bodek's +own front-end settings are separate; see [Configuration](#configuration). -bodek keeps its **own front-end settings** in `~/.bodek/config.json` (override -with `BODEK_CONFIG`): `theme`, `mouse`, `bel`, `notify`, `plain`. Switching -the theme with `/theme` persists it there automatically; the other values can -be written by hand and seed the matching flag defaults. Resolution order: -**flag β†’ `BODEK_THEME` env (theme) β†’ settings file β†’ built-in default**. +--- + +## Features + +### Interface + +- **EMBER Terminal** β€” the WebUI's design language (electric amber on + blue-charcoal) as terminal tokens; four themes (`ember-dark` Β· + `ember-light` Β· `high-contrast` Β· `classic`), switchable live with `/theme` + and persisted to `~/.bodek/config.json`. +- **The palette (`^K`)** β€” every command, session, model, and drawer tab one + fuzzy search away; every row teaches its chord. +- **Turn cards** β€” telemetry rides the turn head, `^F` folds noisy turns, + `alt+↑`/`alt+↓` jump turn-to-turn, reasoning accordions auto-expand live + and collapse on the next turn, and `^E` expands every tool step's details. +- **Typed tool renderers** β€” diffs tint with a `+N βˆ’M` chip, file reads get + line numbers, JSON pretty-prints, test runs show pass/fail verdicts. +- **Streaming answers** rendered as Markdown + ([glamour](https://github.com/charmbracelet/glamour)). +- **Tool activity** β€” every `tool_call`/`tool_result` shown live with a glyph + per tool, a spinner, an argument preview, and a result excerpt rendered as + a tree (`⎿`) β€” multi-line, blank-stripped, capped with a `+N more lines` + footer, and tinted with a `βœ—` when the call fails. +- **Fluent by default** β€” gradient wordmark, smooth braille spinner, smart + autoscroll that never yanks you while you read history, and a + scroll-position indicator. +- **Version display & update hint** β€” bodek's version rides the header next + to the logo (the spawned odek's next to the model); a quiet note appears at + startup when a newer release is available (`bodek upgrade` installs it; + dev builds are never nagged). + +### Working with the agent + +- **Live reasoning** β€” the model's pre-tool thinking streams in dimmed text + with an elapsed timer and cycling status. Long turns keep every + thinkβ†’reply pair intact: each reasoning block is followed by its own + answer card, in arrival order. +- **Context-aware progress** β€” while the agent works, a status line right + below your last message shows what it's actually doing (`πŸ§ͺ running + tests`, `πŸ“– reading client.go`, `πŸš€ pushing`) with a live elapsed timer. +- **Sub-agents** β€” delegations are labelled and their `subagent_log` activity + nests beneath the delegating call, so a sub-agent's progress reads as its + own branch of the step tree. Per-task `subagent_state` telemetry + (odek v1.30+) drives live cards β€” step, tool, iterations, tokens, + duration β€” and terminal status glyphs (`βœ“` success, `◐` partial, `βœ—` error, + `⊘` cancelled, `⏱` timeout), with a `1/2 agents Β· 6.3k tok` rollup on the + collapsed line. Framed delegate results render as a structured card β€” + status, summary, changed files, and usage. `ctrl+s` (or `/stop `, + two-step confirmed) stops one running sub-agent, and the `/agents` drawer + tab lists the serve instance's registry snapshot. +- **Model switcher** (`^O`) β€” change the model for the next turn. The picker + merges the server's configured model with its built-in profile catalog + (`/api/profiles`), each annotated with its context window. +- **Session browser** (`^R`) β€” resume, replay, delete, pin (`p`), rename + (`r`), export a transcript (`e` markdown, `E` JSON), and search server-side + (`/`); `n` loads the next page. Resuming sends a `session_switch` so the + server-side memory buffer is restored before you type. +- **Skill suggestions** β€” when odek's learn loop proposes a skill, a passive + card above the composer answers on `alt+s` (save) / `alt+x` (skip); it + never blocks sending, and auto-save governs real persistence. +- **Engine notices** β€” skill loads, memory merges, and actionable agent + signals appear as quiet status lines; internal housekeeping (context + trims, tool execution times) stays silent. Info traces fade after 3s; + errors, warnings, and disconnect notes autoclose after 10s. +- **Cancellation** (`Esc`) β€” abort a running turn via odek's cancel API. + +### Safety + +- **Inline approvals** β€” odek's `danger` engine prompts surface where the + context is; decisions go straight back over the socket. See + [Approvals](#approvals). +- **Friction & expiry** β€” repeated same-class approvals require typing + `approve`; every request is time-boxed, autocloses on expiry, and can + never collect an approval for a prompt the engine already abandoned. +- **Death-gates everywhere** β€” deletes are two-step, `/stop` and `^L` are + two-step, and the server shutdown requires typing the literal word + `shutdown`. See the [Security model](#security-model). + +### Telemetry & cost + +- **Context gauge** β€” a pressure-tinted context-window gauge in the header + (`ctx β–ˆβ–‰β–‘β–‘β–‘ 38% 380/1k`, eighth-block fill, greenβ†’amberβ†’red). +- **Per-turn footers & `/stats`** β€” token counts and latency ride every turn + head; `/stats` rolls up the session (cost, cache, context). +- **Cost tracking** β€” when odek has token prices configured, the header shows + the running session spend, each turn footer its estimated cost, and + `/stats` adds the `max_cost_usd` cap when set; hidden entirely otherwise. +- **Live server snapshot** β€” a 25s heartbeat measures WebSocket round-trip + latency and refreshes server uptime, connection count, and streaming state + (the ⚑ badge beside the model); `/server` opens the full cockpit card. + +### Accessibility & plain mode + +- **Linear mode (`--plain`)** β€” skips the alt-screen entirely: agent events + print as append-only text in the terminal's native scrollback above a + minimal input chrome (`β–Έ` tool calls, `[think]`, `[error]`, `⚠ approval`, + `❯` your prompts, `βœ“ done Β· N tools Β· Xs Β· N tok`). Streamed fragments stay + suppressed; the reply lands whole when the turn ends. +- **Severity never rides color alone** β€” state always carries a glyph, which + makes `--plain` the accessible surface for screen readers β€” and the + natural one for pipes: `bodek --plain < task > run.log`. +- **`NO_COLOR`** degrades the entire EMBER palette to plain text in every + mode; **`NO_MOTION=1`** swaps animated spinners for static frames. + +### Connectivity & lifecycle + +- **Spawn or attach** β€” bodek launches a private `odek serve` by default, or + attaches to a running one with `--url` (+`--token`). +- **Auto-reconnect** β€” if the socket drops, bodek redials with backoff + (500ms β†’ 8s, 5 attempts) and re-adopts the session over the fresh socket; + only a server that stays down leaves the `disconnected` badge (empty input + + `⏎` retries manually). +- **Attention when backgrounded** β€” turn completion and pending approvals + set the terminal window title (`βœ“ done β€” ` / `⚠ approval needed β€” + `) and ring the bell (`--bel=false` mutes); `--notify` adds OSC 9 + desktop notifications. Fires only on terminal states β€” never per token. +- **Sandbox aware** β€” the header shows `πŸ›‘ sandboxed` or `⚠ host access`; + pass `--sandbox` to run tool calls inside odek's Docker isolation. + +--- + +## Using bodek ### Key bindings @@ -161,28 +300,28 @@ be written by hand and seed the matching flag defaults. Resolution order: | `⏎` (disconnected, empty input) | Retry the connection | | `^C` | Quit | -Prompts sent while a turn is running are **queued** and sent automatically -when the turn ends β€” a transient note acknowledges each hold, and the count -rides both the busy status line and the footer (one drains per turn-end). -Queued prompts stay visible in a **strip directly above the input area**: one -row per prompt with per-row `β–² β–Ό βœ•` controls (`--mouse`) to reorder or delete, -and a `^Q` keyboard focus mode for the same actions (`↑↓` select, `←→` move, -`d` delete). The strip collapses to zero rows when the queue is empty. -While the transcript is scrolled up mid-run, the footer flags `↓ new output`; press -`^G` to jump to the latest. If the connection drops, bodek retries with -backoff and, after giving up, keeps your draft and offers a manual retry -on `⏎` with an empty input. - **Every printable character always types.** No bare letter, digit, or punctuation key is ever bound in the composer β€” actions live on chords and non-character keys (`^K` palette, `alt+↑↓` turn jumps, `F1` help), so a prompt can start with `?`, `[`, or any other character. +### The prompt queue + +Prompts sent while a turn is running are **queued** and sent automatically +when the turn ends β€” a transient note acknowledges each hold, and the count +rides both the busy status line and the footer (one drains per turn-end). +Queued prompts stay visible in a **strip directly above the input area**: one +row per prompt with per-row `β–² β–Ό βœ•` controls (`--mouse`) to reorder or +delete, and a `^Q` keyboard focus mode for the same actions (`↑↓` select, +`←→` move, `d` delete). The strip collapses to zero rows when the queue is +empty. While the transcript is scrolled up mid-run, the footer flags +`↓ new output`; press `^G` to jump to the latest. + ### Commands (`/`) Type `/` at the start of the input for a command palette. `↑`/`↓` to choose, -`β‡₯` to complete, `⏎` to run, `esc` to dismiss. You can also just type the full -command and press `⏎`. +`β‡₯` to complete, `⏎` to run, `esc` to dismiss. You can also just type the +full command and press `⏎`. | Command | Action | |---------|--------| @@ -213,10 +352,10 @@ command and press `⏎`. ### The management drawer -`/sessions`, `/runs`, `/agents`, `/events`, `/plan`, `/memory`, `/skills`, `/tools`, and -`/config` all open tabs of **one drawer** with a shared grammar: +`/sessions`, `/runs`, `/agents`, `/events`, `/plan`, `/memory`, `/skills`, +`/tools`, and `/config` all open tabs of **one drawer** with a shared grammar: -- `]` / `[` cycle tabs Β· `1`–`8` jump Β· `r` refresh Β· `esc` closes. +- `]` / `[` cycle tabs Β· `1`–`9` jump Β· `r` refresh Β· `esc` closes. - **Every management row opens a detail view on `⏎`** β€” the full text behind the gate: a skill's description and provenance, a fact or pending episode's body, an MCP server's command/args/limits, raw JSON for nested @@ -242,7 +381,7 @@ command and press `⏎`. level (`sandbox.enabled`), nested values show raw JSON in the detail; lifetime usage, `d` kick a connection, `S` typed shutdown death-gate. -### File attachments (`@`) +### File attachments Type `@` to attach a file. bodek searches the working tree and shows a completion popup; `↑`/`↓` to choose, `⏎` or `β‡₯` to insert, `esc` to dismiss. @@ -252,9 +391,11 @@ completion popup; `↑`/`↓` to choose, `⏎` or `β‡₯` to insert, `esc` to dism ``` odek resolves and inlines the file content **server-side** (wrapped in its -untrusted-content boundary), so attachments go through the same security model -as any other external input β€” bodek doesn't special-case them. (Saved sessions -are resumed via `/sessions` or `^R`, not `@`.) +untrusted-content boundary), so attachments go through the same security +model as any other external input β€” bodek doesn't special-case them. (Saved +sessions are resumed via `/sessions` or `^R`, not `@`.) + +### Approvals When the agent requests approval for a dangerous operation, pick an outcome from the panel and confirm β€” typing never answers by accident: @@ -267,119 +408,85 @@ from the panel and confirm β€” typing never answers by accident: | `Tab` | Expand/collapse the full command & description text | | `PgUp` / `PgDn` / `^U` / `^D` | Scroll the transcript while the panel is open | -After three same-class approvals inside a minute the server engages **friction -mode**: the panel shows the recent-approval count, the trust shortcut is -withdrawn, and approving requires typing the literal word `approve` and -pressing `⏎` (a mistyped word resets β€” retyping is the point). Denying stays -one `Esc`. +After three same-class approvals inside a minute the server engages +**friction mode**: the panel shows the recent-approval count, the trust +shortcut is withdrawn, and approving requires typing the literal word +`approve` and pressing `⏎` (a mistyped word resets β€” retyping is the point). +Denying stays one `Esc`. Approvals are time-boxed by the engine (60s by default), and an expired -request is dead β€” odek fails the tool call and picks an alternative path. The -panel shows a live `expires in Ns` countdown (red in the last 10 seconds) and -autocloses expired requests with an expiry notice, so a stale form can never -collect an approval for a prompt the engine already abandoned. +request is dead β€” odek fails the tool call and picks an alternative path. +The panel shows a live `expires in Ns` countdown (red in the last 10 +seconds) and autocloses expired requests with an expiry notice, so a stale +form can never collect an approval for a prompt the engine already abandoned. --- -## What you see +## Configuration -- **EMBER Terminal** β€” the WebUI's design language (electric amber on - blue-charcoal) as terminal tokens; `BODEK_THEME=ember-light|high-contrast|classic` - or `/theme` to switch live (persisted to `~/.bodek/config.json`), and - `NO_MOTION=1` for a fully static UI. -- **The palette (`^K`)** β€” every surface one fuzzy search away, every row - teaching its chord. -- **Turn cards** β€” telemetry rides the turn head, `^F` folds noisy turns, - `alt+↑`/`alt+↓` jump turn-to-turn, reasoning accordions auto-expand live - and collapse on the next turn, and `^E` expands every tool step's details. -- **Typed tool renderers** β€” diffs tint with diffstats, file reads get line - numbers, JSON indents, test runs show pass/fail verdicts. -- **Streaming answers** rendered as Markdown ([glamour](https://github.com/charmbracelet/glamour)). -- **Tool activity** β€” every `tool_call`/`tool_result` shown live with a glyph - per tool, a spinner, an argument preview, and a result excerpt rendered as a - tree (`⎿`) β€” multi-line, blank-stripped, capped with a `+N more lines` - footer, and tinted with a `βœ—` when the call fails. -- **Sub-agents** β€” delegations are labelled and their `subagent_log` activity - nests beneath the delegating call, so a sub-agent's progress reads as its own - branch of the step tree. Per-task `subagent_state` telemetry (odek v1.30+) - drives live cards β€” step, tool, iterations, tokens, duration β€” and terminal - status glyphs (`βœ“` success, `◐` partial, `βœ—` error, `⊘` cancelled, `⏱` - timeout), with a `1/2 agents Β· 6.3k tok` rollup on the collapsed line. - Framed delegate results render as a structured card β€” status, summary, - changed files, and usage β€” while prose results keep the generic preview. - `ctrl+s` (or `/stop `, two-step confirmed) stops one running - sub-agent, and the `/agents` drawer tab lists the serve instance's - registry snapshot β€” recent delegated tasks with goal, status, and usage. -- **Security approvals** β€” odek's `danger` engine prompts surface as an inline - panel; your answer is sent straight back over the socket. -- **Live reasoning** β€” the model's pre-tool thinking streams in dimmed text, - with a running elapsed timer and cycling status while it works. Long turns - keep every thinkβ†’reply pair intact: each reasoning block is followed by its - own answer card, in arrival order. -- **Command palette (`/`)** and **file attachments (`@`)** β€” live, navigable popups. -- **Context-aware progress** β€” while the agent works, a status line just - above the input (right below your last message) shows what it's actually - doing (`πŸ§ͺ running tests`, `πŸ“– reading client.go`, `πŸš€ pushing`) with a live - elapsed timer and the queued-prompt count when prompts are held. -- **Session browser** (`^R`) β€” resume, replay, delete, pin (`p`), rename - (`r`), export a transcript (`e` markdown, `E` JSON), and search server-side - (`/`); `n` loads the next page. Resuming sends a `session_switch` so the - server-side memory buffer is restored before you type. -- **Auto-reconnect** β€” if the socket drops, bodek redials with backoff - (500ms β†’ 8s, 5 attempts) and re-adopts the session over the fresh socket; - only a server that stays down leaves the `disconnected` badge. -- **Model switcher** (`^O`) β€” change the model for the next turn. The picker - merges the server's configured model with its built-in profile catalog - (`/api/profiles`), each annotated with its context window. -- **Skill suggestions** β€” when odek's learn loop proposes a skill, a passive - card above the composer answers on `alt+s` (save) / `alt+x` (skip); it never - blocks sending, and auto-save governs real persistence. -- **Server shutdown** β€” the config tab's `S` requires typing the literal word - `shutdown` (the approval-friction pattern); the socket drop that follows is - expected state, with `⏎` starting a fresh instance in spawn mode. -- **Live server snapshot** β€” a 25s heartbeat measures WebSocket round-trip - latency and refreshes server uptime, connection count, and streaming state - (the ⚑ badge beside the model); `/stats` surfaces the full link row. -- **Cancellation** (`Esc`) β€” abort a running turn via odek's cancel API. -- **Sandbox aware** β€” the header shows `πŸ›‘ sandboxed` or `⚠ host access`; pass - `--sandbox` to run tool calls inside odek's Docker isolation. -- **Telemetry** β€” a pressure-tinted context-window gauge in the header - (`ctx β–ˆβ–‰β–‘β–‘β–‘ 38% 380/1k`, eighth-block fill, greenβ†’amberβ†’red), per-turn - token/latency footers, and the full session roll-up in `/stats`. -- **Cost tracking** β€” when odek has token prices configured (limits), the - header shows the running session spend, each turn footer its estimated - cost, and `/stats` rolls up the session (with the `max_cost_usd` cap when - set); hidden entirely otherwise. -- **Fluent by default** β€” gradient wordmark and hairline, smooth braille - spinner, smart autoscroll that never yanks you while you read history, and a - scroll-position indicator. -- **Engine notices** β€” skill loads, memory merges, and actionable agent - signals appear as quiet status lines; internal housekeeping (context - trims, tool execution times) stays silent. Nothing lingers: info traces - fade after 3s, and errors, warnings, and disconnect notes autoclose - after 10s (connection state stays visible in the header badge). -- **Attention when backgrounded** β€” turn completion and pending approvals set - the terminal window title (`βœ“ done β€” ` / `⚠ approval needed β€” - `) and ring the bell (`--bel=false` mutes); `--notify` adds OSC 9 - desktop notifications. Fires only on terminal states β€” never per token. -- **Linear mode (`--plain`)** β€” skips the alt-screen entirely: agent events - print as append-only text in the terminal's native scrollback above a - minimal input chrome (`β–Έ` tool calls, `[think]`, `[error]`, `⚠ approval`, - `❯` your prompts, `βœ“ done Β· N tools Β· Xs Β· N tok`). Severity never rides - color alone, which makes this the accessible surface for screen readers β€” - and the natural one for pipes: `bodek --plain < task > run.log`. Streamed - fragments stay suppressed; the reply lands whole when the turn ends. - `NO_COLOR` degrades the entire EMBER palette to plain text in every mode. -- **Diff-aware tool steps** β€” expanding a step (`^E`) renders its output by - shape: unified diffs tint (`+` green, `-` red, hunk headers steel, file - markers dim) with a `+N βˆ’M` chip on the step head, fenced ` ```diff ` - blocks tint inside prose without mis-styling the surrounding text, file - reads get line numbers, JSON pretty-prints, and test runs summarize - pass/fail on the step line. -- **Version display** β€” the header shows bodek's own version next to the logo - and the spawned odek's version next to the model name. -- **Update hint** β€” at startup, a quiet note appears when a newer bodek release - is available (`bodek upgrade` installs it; dev builds are never nagged). +bodek keeps its **own front-end settings** in `~/.bodek/config.json` +(override the location with `BODEK_CONFIG`): `theme`, `mouse`, `bel`, +`notify`, `plain`. Switching the theme with `/theme` persists it there +automatically; the other values can be written by hand and seed the matching +flag defaults. Resolution order: +**flag β†’ `BODEK_THEME` env (theme) β†’ settings file β†’ built-in default**. + +The full settings reference β€” every key, every `BODEK_*`/display env var, +and an example file β€” lives in +[docs/CONFIGURATION.md](docs/CONFIGURATION.md). odek's server-side +configuration is unaffected and stays in `~/.odek/config.json`. + +--- + +## Security model + +bodek is a renderer, not an enforcer β€” and that's the point. All agent +behaviour, danger gating, and sandboxing live in odek; the TUI's job is to +never become the weakest link: + +- **Everything from the wire is sanitized.** Any remote-rendered content β€” + tool output, file contents, titles, server strings β€” passes through + `sanitize()` before it touches the screen. Raw terminal escapes from the + server can't repaint your session. +- **Untrusted content stays fenced.** Tool results arrive as JSON envelopes + wrapped in odek's untrusted-content markers; bodek decodes the envelope and + folds the wrappers away β€” the content is displayed as data, never executed, + and prompt-injection text inside it renders like any other text. +- **Approvals can't be faked or stale.** The panel is inline and keyed, the + trust shortcut dies under friction mode, and expired requests autoclose β€” + a dead prompt can't collect an approval. See [Approvals](#approvals). +- **Destructive actions are deliberately slow.** Session/fact deletes are + two-step (`y`), stopping a sub-agent is two-step, clearing the + conversation is two-step, and shutting the server down requires typing + `shutdown` letter by letter. +- **Server config is read-only** β€” the config tab renders a sanitized + snapshot; mutation stays with the operator on disk. +- **Tokens stay local.** The per-instance token is adopted automatically on + spawn and stored only on your machine (`internal/tokens`); attaching asks + you to supply it explicitly. + +--- + +## Troubleshooting + +- **`bodek` can't find `odek`** β€” the engine must be on your `PATH`, or pass + `--odek-bin /path/to/odek`. +- **Auth errors when attaching** β€” copy the *full* token URL `odek serve` + printed (`--url 'http://…/?token=…'`) or pass the token via `--token`. + Spawned instances adopt the token automatically. +- **Clipboard (`^Y` / `alt+y`) does nothing** β€” it uses OSC 52, which needs + a supporting terminal; over SSH your terminal must pass OSC 52 through. +- **Mouse scrolling eats text selection** β€” that's the `--mouse` trade-off + (terminals can't have both). Launch without it when you need to copy. +- **Colors look wrong** β€” try `/theme classic`, check `TERM`; `NO_COLOR=1` + forces a colorless render everywhere. +- **Connection dropped mid-turn** β€” bodek retries with backoff (5 attempts) + and re-adopts the session; if it gives up, your draft is kept β€” press `⏎` + on an empty input to reconnect. +- **Where are my settings?** β€” `~/.bodek/config.json` (override with + `BODEK_CONFIG`). See [docs/CONFIGURATION.md](docs/CONFIGURATION.md). +- **Windows** β€” use Windows Terminal or another ANSI/OSC-capable + emulator; desktop notifications depend on OSC 9 support. --- @@ -395,28 +502,43 @@ make vet make tidy ``` -Continuous integration runs build, `go vet`, `golangci-lint`, and the race- -enabled test suite on every push (see [`.github/workflows`](.github/workflows)). -Tagged releases (`vX.Y.Z`) are built and published automatically by +Continuous integration runs build, `go vet`, `golangci-lint`, and the +race-enabled test suite on every push (see +[`.github/workflows`](.github/workflows)). Tagged releases (`vX.Y.Z`) are +built and published automatically by [GoReleaser](https://goreleaser.com). Project layout: | Path | Responsibility | |------|----------------| -| `cmd/bodek` | CLI entry point: flags, lifecycle, wiring | +| `cmd/bodek` | CLI entry point: flags, lifecycle, `version` / `upgrade` subcommands | | `internal/server` | Launch / attach to `odek serve`, resolve the auth token | | `internal/client` | odek serve WebSocket protocol (transport + REST + decoding) | | `internal/tokens` | Local persistence of per-session auth tokens | +| `internal/settings` | Front-end settings (`~/.bodek/config.json`) | +| `internal/update` | Self-upgrade: fetch and swap in the latest GitHub release binary | | `internal/tui` | The Bubble Tea model, update loop, panels, and view | ### Architecture & testing -bodek is a pure client, so it is highly testable: the WebSocket protocol, REST -endpoints, token store, and the full Bubble Tea update/view loop are exercised -by unit and integration tests against an in-process `odek serve` stand-in. -Internal-package statement coverage is **~99%** (client 100%, tui 99%, tokens -98%, server 95% β€” the remainder is unreachable OS-error handling). +bodek is a pure client, so it is highly testable: the WebSocket protocol, +REST endpoints, token store, and the full Bubble Tea update/view loop are +exercised by unit and integration tests against an in-process `odek serve` +stand-in. Internal-package statement coverage is **~99%** (client 100%, +tui 99%, tokens 98%, server 95% β€” the remainder is unreachable OS-error +handling). + +### For contributors + +- [AGENTS.md](AGENTS.md) β€” the contributor contract: commands, the + mandatory pre-commit checklist, testing expectations, and code + conventions. +- [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md) β€” the audit matrix mapping + every `odek serve` REST endpoint and WebSocket message to its client + method and UI surface. Update it when either side changes. +- [docs/CONFIGURATION.md](docs/CONFIGURATION.md) β€” the front-end settings + and environment variable reference. --- diff --git a/bodek-intro-july-2026.mov b/bodek-intro-july-2026.mov deleted file mode 100644 index c9ea02c..0000000 Binary files a/bodek-intro-july-2026.mov and /dev/null differ diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..fe2e878 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,63 @@ +# bodek configuration reference + +bodek (the terminal UI) keeps its own front-end settings, completely separate +from odek's server-side configuration (`~/.odek/config.json` β€” model, MCP, +memory, skills; see the [odek docs](https://github.com/BackendStack21/odek)). + +## Settings file + +Location: `~/.bodek/config.json` β€” override with the `BODEK_CONFIG` +environment variable. The file is optional; a missing file means "everything +defaulted". A *malformed* file is surfaced as an error at startup rather than +silently ignored. + +```json +{ + "theme": "ember-dark", + "mouse": true, + "bel": false, + "notify": true, + "plain": false +} +``` + +| Key | Type | Default | Flag | What it does | +|-----|------|---------|------|--------------| +| `theme` | string | `ember-dark` | `--theme` | Color palette: `ember-dark` Β· `ember-light` Β· `high-contrast` Β· `classic`. `/theme ` switches live **and persists it here**. | +| `mouse` | bool | `false` | `--mouse` | Enable mouse wheel scrolling and click targets. Trade-off: the terminal can't do native text selection while mouse mode is on. | +| `bel` | bool | `true` | `--bel` | Ring the terminal bell when a turn completes or an approval is waiting (`--bel=false` mutes; the title still updates). | +| `notify` | bool | `false` | `--notify` | Raise desktop notifications (OSC 9) on turn completion and pending approvals. | +| `plain` | bool | `false` | `--plain` | Linear mode: no alt-screen; agent events print to the terminal's native scrollback (screen readers, pipes, logs). | + +Unset keys fall back to their defaults β€” the file only ever stores choices +you actually made (`/theme` writes `theme`; the rest you write by hand). + +## Resolution order + +Every setting resolves the same way: + +``` +explicit flag β†’ BODEK_THEME env (theme only) β†’ settings file β†’ built-in default +``` + +`/theme` persists to the settings file, so the next launch starts where you +left off unless a flag or `BODEK_THEME` overrides it. + +## Environment variables + +| Variable | Affects | Behavior | +|----------|---------|----------| +| `BODEK_THEME` | startup theme | Startup palette override (still beaten by an explicit `--theme`). | +| `BODEK_CONFIG` | settings path | Alternative path for the settings file (used by tests and shared-dotfile setups). | +| `NO_COLOR` | all rendering | Any non-empty value degrades the color profile to plain text in every mode β€” standard nocolor.org semantics. | +| `NO_MOTION` | animations | Any non-empty value replaces animated spinners/progress with single static frames. | +| `ODEK_API_KEY` | odek (not bodek) | Read by the `odek serve` engine bodek spawns β€” any OpenAI-compatible provider key. | + +## odek-side configuration + +Everything the agent does is configured on the odek side +(`~/.odek/config.json` β†’ `./odek.json` β†’ `ODEK_*` env vars): model, base +URL, API keys, MCP servers, memory, skills, sandbox. bodek inherits it +unchanged β€” see the +[odek repository](https://github.com/BackendStack21/odek) for the full +server configuration reference. diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index 52fbe47..0677b3e 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -42,9 +42,9 @@ update it when either side changes. | `GET /api/tools` | `Tools` | tools tab | | `POST /api/shutdown` | `Shutdown` | config tab (`S` + typed "shutdown" death-gate) | -All seven management surfaces are drawer tabs (sessions Β· runs Β· events Β· -memory Β· skills Β· tools Β· config): `]`/`[` cycle, `1–7` jump, `r`/`⏎` -refresh β€” one grammar everywhere. +All nine management surfaces are drawer tabs (sessions Β· runs Β· agents Β· +events Β· plan Β· memory Β· skills Β· tools Β· config): `]`/`[` cycle, `1–9` +jump, `r`/`⏎` refresh β€” one grammar everywhere. ## Not exposed by odek REST (documented gaps) diff --git a/docs/PLANNING_MODE_UI.md b/docs/PLANNING_MODE_UI.md deleted file mode 100644 index 5f01d66..0000000 --- a/docs/PLANNING_MODE_UI.md +++ /dev/null @@ -1,269 +0,0 @@ -# Planning Mode β€” UI Integration Design (bodek) - -Status: **implemented** on `feat/planning-mode-ui` (Option A strip, semantic -transcript rows, drawer tab after Events β€” decisions Β§8.1–8.3 resolved). - -odek's planning system (`docs/PLANNING.md` in the odek repo) gives the engine a -structured task plan: one `plan` tool (`create`/`update`/`complete`/`get`), a -mutex-serialized store, and a protected `[Current plan:` system message that -survives trimming and restarts. Planning is **on by default**; kill switches are -CLI β†’ env β†’ global config. This document designs bodek's read-only surface onto -that state. - -bodek is a pure front-end β€” everything here consumes existing odek protocol. -**No changes to odek are required or proposed.** - ---- - -## 1. Engine contract (verified facts) - -Protocol surfaces available today: - -| Surface | Shape | Notes | -|---|---|---| -| `GET /api/sessions/{id}/plan` | `{session_id, version, found, steps:[{id,title,status,note?}]}` | Read-only, GET-only by contract (POST falls through to session mutators). 404 = unknown session; `found:false` = no parseable plan; all-done collapsed plan = version set, `steps: []`. Auth + rate limiting identical to sibling session endpoints (`handleSessionByID`). | -| Session WS `tool_call{name:"plan"}` | `Data` = full JSON args | Fires for every mutation; plan calls ride ordinary parallel tool batches. | -| Session WS `tool_result{name:"plan"}` | `Data` = rendered plan text | Pairs with its call via the existing LIFO name matcher. | -| Runtime events `plan_created`/`plan_updated` | counts + version only | Delivered via `GET /api/events` ring β†’ **already visible in bodek's Events tab**. Not forwarded on the session WS. | - -Plan state model (engine): `steps[].status ∈ {pending, in_progress, done, -blocked}`; titles ≀200 chars, notes flattened; store caps from resolved config -(defaults 12 steps / 2000 render chars); monotonic `version` per effective -mutation (no-op mutations don't bump). The plan is advisory steering, not a -contract. - -Two properties matter for the UI: - -1. **WS carries triggers, REST carries truth.** The session WS has no dedicated - plan event, but every mutation shows up as a `plan` tool_call/result pair. - The structured view lives behind the REST GET. So: watch the WS, then fetch - REST. -2. **REST survives restarts.** The endpoint parses the newest parseable plan - message out of the persisted transcript (`loop.ExtractPlan`), so a fresh - fetch after reconnect/attach resumes correct state with no replay work. - ---- - -## 2. Design goals - -- **Ambient visibility**: "what is the agent doing overall?" answerable at a - glance, without opening anything. -- **Zero-footprint absence**: planning disabled, no plan yet, or an old engine - without the endpoint β‡’ literally zero new pixels. Same discipline as the - redundant-indicator removal rule. -- **Read-only**: REST is GET-only by engine contract; bodek never mutates plan - state (steering lives with the model). -- **Order-faithful transcript**: plan tool steps stay timeline items in arrival - order; we specialize rendering, not ingestion. -- **Everything model-derived through `sanitize()`** β€” titles/notes come off the - wire. - ---- - -## 3. Architecture - -### 3.1 Client layer (`internal/client`) β€” `runs.go` sibling file: `plan.go` - -```go -type PlanStep struct { - ID string `json:"id"` - Title string `json:"title"` - Status string `json:"status"` // pending | in_progress | done | blocked - Note string `json:"note"` // omitted when empty -} - -type PlanSnapshot struct { - SessionID string `json:"session_id"` - Version int `json:"version"` - Found bool `json:"found"` - Steps []PlanStep `json:"steps"` // nil-safe: treated as empty -} -``` - -`Client.SessionPlan(sessionID string) (PlanSnapshot, error)` β€” plain GET, -`url.PathEscape(id)`, mirrors the `/api/sessions/{id}` call path. New -dependency-free code; tested against `httptest` fixtures like `spec_test.go` -(200 found, `found:false`, 404, malformed body). - -### 3.2 State layer (`internal/tui/plan.go` β€” new file) - -Model additions: - -```go -plan PlanSnapshot // last accepted snapshot -planFetchSeq int // request ordinal β€” guards out-of-order replies -planPolling bool // drawer plan tab visible (arms tick poll) -``` - -TEA messages: `planMsg{snap PlanSnapshot; seq int; err error}`, -`planTickMsg{seq int}` (reuses the runs-tab tick pattern). - -Refresh triggers (all funnel into one debounced fetch): - -1. **WS trigger**: any `tool_call`/`tool_result` with `Name == "plan"` schedules - a fetch ~250 ms trailing-edge β€” collapses the common createβ†’update burst of - one iteration into a single request. Never fires while idle-to-plan changes - (there are none: idle means no tool calls). -2. **Lifecycle triggers** (immediate, not debounced): tab activation, session - switch (`SessionSwitch`), successful reconnect resume (`reconnect.go` hook), - attach to an existing session. -3. **Fallback poll**: every 3 s while the drawer plan tab is visible only - (identical lifecycle to `runPollEvery`). No blind timers when hidden. - -Acceptance rule: a fetched snapshot replaces local state iff -`snap.Version >= m.plan.Version` and it belongs to the current session. -Monotonic guard makes duplicate/out-of-order responses harmless β€” ingestion -stays idempotent. - -Error posture: silent degradation. A failed/404 fetch hides the strip, marks -the tab "unavailable", never surfaces as an error card. The feature must not -generate noise when an old `odek serve` lacks the route. - -### 3.3 Event handling hook (`internal/tui/events.go`, minimal diff) - -In the `tool_call` case: if `ev.Name == "plan"`, also call -`m.schedulePlanRefresh()`. That's the entire change to hot-path code β€” one line -plus the helper. - ---- - -## 4. UX surfaces - -### A. Transcript step specialization *(recommended, medium effort)* - -Today each plan step renders as a generic row with JSON arg preview β€” and plan -calls fire *per status change*, so long runs would drown in rows like -`plan {"verb":"update","updates":[…]}`. - -Specialize rendering, keep ingestion untouched: - -``` -βœ” plan create Β· 5 steps [Ctrl+E reveals raw args/result] -πŸ”„ plan s2 β†’ in_progress -βœ” plan s1 β†’ done -``` - -Verbs map to compact glyphs: create `+N steps`, complete `id β†’ done`, -update per-update list, get folded to nothing visible beyond the row glyph. -All rendered text still passes through `sanitize()`; `expandAll` (Ctrl+E) -keeps revealing the raw args/result exactly like other tools. - -Open sub-question for the tally/stats: keep counting plan calls in `toolTotal`. -They are genuine tool calls; hiding them from stats would be dishonest. - -### B. Live plan strip *(recommended, small)* - -A single transient line adjacent to the busy indicator (thinking spinner / -last-tool line β€” placed below the last user input per established preference), -shown only when: run active **and** `plan.found`. - -``` -β Έ 🧠 thinking β–Έ plan 2/5 Β· s3 wire flag parsing Β· β›”1 -``` - -- Content: `done/total`, first `in_progress` title truncated to fit, blocked - count when nonzero. Version silently consistency-checks the poll cadence. -- Hidden when: no plan, all done (collapsed), disconnected, old engine. -- All-done confirmation rides the final WS trigger: strip is replaced by the - normal idle state; the persistent record lives in the tab, not chrome. - -### C. Drawer Plan tab *(recommended, medium)* - -New `panelPlan` placed after Events (drawer tabs today: sessions, runs, events, -memory, skills, tools, config β€” models stays its own ^O overlay), giving -1 sessions Β· 2 runs Β· 3 events Β· 4 plan Β· 5 memory Β· 6 skills Β· 7 tools Β· -8 config; digits shift by one from memory onward. Rendered exactly like the Telegram surface for -cross-surface consistency: - -``` -πŸ“‹ Plan β€” v7 Β· 2/4 done Β· 1 blocked ⏎ detail Β· esc fold/close - - ⬜ p1 scaffold command skeleton - βœ… p2 wire flag parsing - πŸ”„ p3 resolve config precedence note preview… - β›” p4 license policy decision blocked -``` - -- Header summary first; one row per step: status glyph, id, title (flattened, - truncated), note preview. -- Detail submode follows house rules: `⏎` expands the selected row's full - note/title through `sanitize()`; `esc`/`q` folds back; `p` promote is a - no-op here (nothing to promote); tab switches reset the submode - (`switchDrawerTab` already does this). -- Strictly read-only β€” no mutation controls exist to fake. -- Empty states: `found:false` β†’ muted "no active plan in this session."; - collapsed all-done β†’ "βœ“ all steps done Β· vN"; unavailable β†’ "plan endpoint - unavailable". - -### D. `/plan` slash command *(recommended, trivial)* - -Registry entry ("structured task plan of this session") opening panelPlan. Free -autocomplete + palette listing. No inline printing variant until wanted β€” the -tab is cheap to open. - -### E. Block notifications *(deferred)* - -A `β›”` arriving mid-turn is arguably notable, but notices expire/distract and -the WS-triggered strip update already lands within ~300 ms. Skip v1; -revisit if long-run ergonomics demand it. - ---- - -## 5. Edge cases - -| Case | Handling | -|---|---| -| Old `odek serve` without the route | Silent degrade: strip hidden, tab unavailable. No retries while degraded except explicit tab activation. | -| Unknown session (404) pre-first-prompt | Same silent path; once the `session` event names us, normal behavior applies. | -| Out-of-order REST replies | Monotonic `version` guard + request seq mismatch discard. | -| Createβ†’update burst in one iteration | Trailing-edge debounce (~250 ms) collapses to one fetch. | -| Parallel batch plan calls | Store serializes server-side; client pairs results via existing LIFO matcher unchanged. | -| Reconnect / restart resume | Fetch on reconnect-success β€” REST reads persisted transcript. | -| Session switch / attach | Clear snapshot before switching; immediate fetch after acceptance. | -| Hostile titles/notes | `sanitize()` on every render; server already flattens + caps lengths. | -| Collapsed all-done plan | `steps == []`, version intact β€” tab summary line, strip hidden. | -| Drawer width overflow | Tab strip already collapses to ellipsis + active β€” free. | -| Tests & flakiness | Drive `handleEvent`/msgs directly (newTestModel); no wall-clock assertions; poll ticks are explicit messages. | - ---- - -## 6. Testing strategy (per AGENTS.md) - -- `internal/client/plan_test.go`: fixtures mirroring `spec_test.go` β€” shape, - `found:false`, 404 error mapping, malformed body tolerance. -- `internal/tui/plan_test.go`: - - strip appears/hides across run-active Γ— found Γ— collapsed matrix; - - WS trigger β†’ scheduled refresh cmd observed (not executed); - - monotonic guard rejects stale versions; duplicate WS triggers idempotent; - - tab rendering incl. truncation, note preview, blocked glyph, detail expand, - sanitize coverage with hostile strings; - - reconnect/session-switch hooks issue fresh fetches. -- Regression rules: race detector suite green, no timing assertions, coverage - stays at the package norm. - -## 7. Milestones (strictly sequential) - -| # | Scope | -|---|---| -| M-P1 | client types + `SessionPlan` + tui state, debounce, guards, hooks (transcript unaffected) | -| M-P2 | transcript semantic rendering for `plan` steps (+ stats decision documented above) | -| M-P3 | drawer Plan tab + detail submode + visible-poll lifecycle | -| M-P4 | live strip + `/plan` command + README/keybinding sync | - -Each milestone ships README touch-ups in the same commit where user-visible -behaviour changes land (repo rule). - ---- - -## 8. Open questions for brainstorm - -1. **Strip vs chip**: dedicated mini-line next to the busy indicator (as drawn) - vs appending `β–Έ plan 2/5` into the existing status line? The mini-line can - hold the current step title; the chip costs zero layout risk. -2. **Transcript visibility**: semantic single-liners (proposed) vs suppressing - plan rows entirely and surfacing only the final "plan updated" heartbeat? - Suppression is quieter but breaks the "every act leaves evidence" property. -3. **Tab position**: accept digits 5–8 shifting (insert plan at 5) for - semantic grouping? -4. Anything else wanted from v1 β€” e.g. copy-step-as-text action, or a jump - from strip to tab? diff --git a/docs/REDESIGN.md b/docs/REDESIGN.md deleted file mode 100644 index 62d03c7..0000000 --- a/docs/REDESIGN.md +++ /dev/null @@ -1,279 +0,0 @@ -# bodek 2.0 β€” "EMBER Terminal": A Holistic TUI Redesign - -Status: **proposal for review** Β· Owner: design + engineering Β· Baseline: post-spec-v2 client (streaming deltas, sessions/pin/export, profiles, friction approvals, heartbeat) - ---- - -## 1. Where we are β€” an honest critique - -### What earns its keep (we keep it) - -- **The chat-first transcript.** Prompt β†’ live reasoning β†’ tool tree β†’ answer is the right mental model for an agent client. No dashboard will beat it. -- **Contextual narration** (`progress.go`). "πŸ§ͺ running tests", "πŸ“– reading client.go" is genuinely delightful and load-bearing for perceived speed. -- **The inline approval panel** β€” decisions happen where the context is visible, not in a modal void. -- **Footgun-averse key design.** Typed friction confirmation, `^G` instead of bare `G`, letters that never decide during approvals. This discipline is a brand asset. -- **Render discipline.** Coalesced streaming flushes, cached transcript prefix, sanitize-everything trust posture. - -### What holds us back (the redesign drivers) - -| # | Problem | Evidence | -|---|---------|----------| -| D1 | **Brand schizophrenia** | bodek is Charm purple/pink (`styles.go`); the WebUI is EMBER β€” electric amber on blue-charcoal, its own type scale, its own motion rules. One product, two personalities. | -| D2 | **No scalable IA** | Two full-screen panels today (sessions, models). Nine more integrations are coming (memory, skills, tools, runs, events, usage, connections, config, mcp). Full-screen takeover Γ— 11 = disorientation. There is no navigation spine. | -| D3 | **Status fragmentation** | State lives in four places: header bar, busy status line, footer, `/stats` card. Token counts appear in header *and* stat lines *and* `/stats`. The cockpit concept doesn't exist. | -| D4 | **Approval queue is a correctness bug** | `m.approval` is a single pointer (`events.go`). odek runs parallel tools β€” a second `approval_request` silently overwrites the first. The user approves the wrong thing or the run deadlocks to timeout. | -| D5 | **Long turns bury the answer** | 20 tool steps push the answer off-screen. No per-turn collapse, no turn-to-turn navigation. Tool results render as capped plain text β€” diffs, JSON, test output all look the same. | -| D6 | **Discoverability cliff** | The power is in chords (`^R ^O ^T ^E ^P`) that are invisible until `/help` is discovered. Footer teaches 3 hints. | -| D7 | **Modality ladder is undocumented** | `esc` means cancel / close / deny / exit-edit depending on context. Each panel invents its own keys (`d`, `p`, `e`, `/`). Nothing systematic. | -| D8 | **Empty & error states are bare** | First run: banner + empty input. Disconnect: a notice line. Spawn failure: a Go error string. No recovery affordances. | -| D9 | **Flat visual hierarchy** | Answer prose, tool noise, reasoning, and chrome have similar weight. The eye has no anchor. | -| D10 | **No graceful degradation** | One dark theme; color-only signals; no light mode; no reduced-motion; narrow terminals get truncation instead of adaptation. | - ---- - -## 2. Design pillars - -1. **The transcript is the stage.** Chrome recedes; the answer is the hero. Everything outside the transcript earns pixels by being glanceable in under a second β€” or it moves into a drawer. -2. **One spine, no dead ends.** Every surface is reachable from one universal palette (`^K`). Every overlay shares one frame with one grammar. `esc` always climbs exactly one rung down the modality ladder. You can never be lost. -3. **Progressive disclosure, three depths.** *Glance* (one line) β†’ *inspect* (expand) β†’ *audit* (drawer/export). Applied uniformly to tool results, approvals, stats, and events. -4. **Teach at the point of need.** The status bar hints follow focus and state. Empty states demonstrate rather than describe. Nothing essential hides behind a memorized chord β€” chords are power moves, not gates. -5. **Calm by default, loud on signal.** Motion budget: spinner and gauge flashes only. Semantic color is reserved for meaning β€” risk, failure, budget pressure. Decoration is hairlines and spacing, never fills. -6. **Degrade gracefully.** Three width breakpoints, light + high-contrast themes, 16-color fallback, `NO_MOTION` switch. The design must survive a 70Γ—24 terminal and a 250Γ—60 one. - ---- - -## 3. The concept β€” a calm cockpit over a live engine - -Two ideas carry the redesign: - -**The Cockpit** β€” one persistent strip that answers, at a glance: *am I connected, what model, how full is my context, what is this costing me.* Everything else (uptime, conns, limits, usage) lives one keypress behind it in a popover. This replaces the current header/footer/`/stats` trichotomy. - -**The Spine** β€” a universal command palette (`^K`) plus one tabbed drawer frame that hosts *all* management surfaces. This is how nine upcoming integrations enter the UI without nine modality explosions. Think Linear's ⌘K mapped to terminal idiom; think k9s, but calm. - ---- - -## 4. Layout - -### 4.1 Standard (80–119 cols) β€” the default canvas - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ ⬑ bodek ⟨glm-5.3 ⚑⟩ ● sandbox Β· $0.41 ctx β–“β–“β–“β–‘β–‘ 42% Β· ● 34ms β”‚ ← cockpit (2 rows) -β”‚ ══════════════════════════════════════════════════════════════════ β”‚ amber rule -β”‚ β”‚ -β”‚ TRANSCRIPT β€” the stage β”‚ -β”‚ β”‚ -β”‚ β–Œyou Β· fix the login bug 2m ago β”‚ ← turn head: author+age -β”‚ β”‚ -β”‚ β–Œodek Β· ⚑ 8.2s Β· 4 tools Β· β‡₯9k ↦1k Β· $0.031 β”‚ ← turn head: telemetry -β”‚ β‹― reasoning Β· 12 lines [tab expand] β”‚ ← accordion (auto-collapsed) -β”‚ βŒ• search_files "login" 143ms βœ“ β”‚ ← step line -β”‚ ✎ patch auth.go 312ms βœ“ +42 βˆ’7 β”‚ ← typed renderer: diffstat -β”‚ The bug was a stale session cookie on line 88. I've… β”‚ ← the hero: answer prose -β”‚ β”‚ -β”‚ ⏳ approval 1 of 2 Β· shell_exec Β· rm -rf build/ A Β· D Β· T β”‚ ← approval queue (D4 fix) -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ ❯ ask odek… Β· @file Β· /command Β· ⌘K everything β”‚ β”‚ ← composer -β”‚ β”‚ πŸ“Ž notes.txt βœ• β”‚ β”‚ ← attachment chips -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ ● ready Β· β‡₯12k ↦3k this session Β· rtt 34ms ? help Β· ^K β”‚ ← status bar (context-aware) -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### 4.2 Wide (β‰₯ 120 cols) β€” split drawer, opt-in - -The drawer opens as a 38–44 col right pane beside a β‰₯ 70 col transcript, so a live run stays visible while you browse runs/events/sessions. Toggle with `w`; remember per profile. On standard widths the drawer overlays full-area (today's behavior, refined). - -``` -β”‚ TRANSCRIPT (β‰₯70 cols) β”‚ β•žβ• runs β•β•β•β•β•β•β•β•β•β•β•β•β•ž β”‚ -β”‚ β–Œyou Β· ship it β”‚ β”‚ β–Ά 3m running 1.2k β”‚ β”‚ -β”‚ ✎ patch ci.yml +8 βˆ’1 β”‚ β”‚ βœ“ 9m completed 12k β”‚ β”‚ -β”‚ Tests pass. Rolling out… β”‚ β”‚ ↳ approve pending β”‚ β”‚ -β”‚ β”‚ β”‚ [A]pprove [D]eny β”‚ β”‚ -``` - -### 4.3 Narrow (< 80 cols) β€” survive gracefully - -Cockpit collapses to one row (model chip + gauge + status dot). Composer drops to 2 rows + placeholder teaching. Turn heads merge into one line. Drawer always full-overlay. Stat lines shed segments by the existing drop-priority machinery (`statLine`). - ---- - -## 5. The Cockpit (kills D3, D9) - -**Persistent row** (left β†’ right): brand Β· model chip (`⚑` streaming badge, `βŒ₯⏎`/click to switch) Β· sandbox dot Β· session cost (when priced) Β·Β·Β· context gauge (amber > 60%, red > 85%, one flash on `context_trimmed` signal) Β· session tokens Β· connection dot (green / amber > 1s RTT / red lost). - -**Popover** (`h`, or click the cockpit): server card β€” version, uptime, ws connections, stream state; budget card β€” limits from `/api/limits` + spend-vs-cap bar; lifetime usage from `/api/usage`; connection list with kick. `/stats` becomes an alias that opens this popover on the session tab, which keeps per-turn history (turn count, thinking ratio, cache totals) β€” nothing is lost, everything is finally in *one* place. - -Typography of the cockpit: values bright, labels muted, glyphs amber. Numbers never repeat elsewhere except the per-turn heads (which are about *that* turn, not the session). - ---- - -## 6. The transcript β€” turn cards & typed renderers (kills D5, D9) - -**Turn cards.** Every exchange gets a head row: author glyph + age (user) or telemetry summary (assistant: latency, tools, tokens, cost). Per-turn stat lines move from the *foot* to the *head* β€” you scan what a turn cost *before* reading it. Collapse a turn (`c` or click head) to its head row + final sentence. Jump between turns with `[` / `]`. - -**Reasoning accordions** adopt the WebUI's proven rule: auto-expand while its turn is live (with auto-follow), auto-collapse when the next turn starts; manually-opened history stays open; resumed transcripts start collapsed. (Today: always-capped excerpt β€” close, but the live auto-expand is what makes thinking models feel fast.) - -**Typed tool renderers.** The step line stays one-line (glyph Β· name Β· arg Β· status chip). What changes is *inspect depth*: expanding picks a renderer by tool/shape β€” - -| Renderer | Trigger | Inspect view | -|----------|---------|--------------| -| **diff** | `patch`, `batch_patch`, `write_file` | unified diff, `+`/`βˆ’` tinted, per-file diffstat in the step line (`+42 βˆ’7`) | -| **file** | `read_file`, `batch_read` | line-numbered excerpt with current-line marker | -| **tree** | `tree`, `glob`, `search_files` (paths) | indented tree, matching lines dimmed | -| **json** | `json_query`, JSON-shaped results | collapsible key path view | -| **tests** | shell containing `go test`/`pytest`/`jest` | pass/fail summary line, failing test names extracted | -| **fallback** | everything else | today's capped plain text | - -All renderers are pure functions `(data, width) β†’ []string` with golden tests β€” no new dependencies (diff parsing from `diff` tool output and our own arg shapes; shell test-summary parsing is a small matcher like `progress.go`). - -**Answer emphasis.** Final answer prose is the only full-brightness block in a turn; steps and reasoning sit one step dimmer. Implemented as a per-role brightness rule in the theme, not new colors. - ---- - -## 7. The Spine β€” palette + drawer (kills D2, D6, D7) - -### 7.1 Command palette (`^K`) - -One fuzzy-searchable surface over: slash commands Β· sessions (resume) Β· models (switch) Β· drawer tabs Β· actions (cancel run, clear, export transcript, kick connection, shutdown server…). Hand-rolled scorer (~60 lines, no deps). Typing `/` inside it filters to commands (today's palette becomes a mode). Arrow keys + `⏎`; `esc` closes to exactly where you were. - -The palette is also the *teaching* surface: every entry shows its chord (`resume session ^R`) β€” users graduate from palette to chords naturally. - -### 7.2 The drawer β€” one frame, seven tabs - -``` -β•žβ• sessions Β· runs Β· events Β· memory Β· skills Β· tools Β· config β•β•β•β•β•β•ž -β”‚ /search (where supported) [1–7] tabs Β· esc βœ• β”‚ -β”‚ …tab content, windowed rows, per-tab footer hints… β”‚ -``` - -| Tab | Maps to (WEBUI.md) | Key capabilities | -|-----|--------------------|------------------| -| sessions | `/api/sessions` + CRUD/export | current panel upgraded: search, pin, rename, export, load-more, per-session tokens | -| runs | `/api/prompt`, `/api/runs/*` | start headless run, live status/tokens/elapsed (3s poll while visible), cancel, **inline approve/deny/trust** via the remote approval bridge | -| events | `/api/events` | `odek.event/v1` ring, filter by run/session; arg hashes & redacted fields only (by design) | -| memory | `/api/memory*` | facts by target with add/remove, pending-review episode queue with promote, consolidate | -| skills | `/api/skills` + promote | provenance badges (`needs review`, `untrusted`), usage counts, promote (force-gated) | -| tools | `/api/tools`, `/api/mcp` | registry with enabled state; MCP servers with limits (env withheld server-side) | -| config | `/api/config`, `/api/health`, `/api/usage`, `/api/connections` | sanitized config view, lifetime usage, live connections with kick, server shutdown (confirm-gated, typed) | - -Drawer keys are uniform: `1–7` jump, `]`/`[` cycle, `/` search, `⏎` act, `d` delete-with-confirm, `esc` close. Tab-specific keys live in each tab's footer only. - -### 7.3 Modality ladder (documented, enforced) - -``` -palette > drawer > approval queue > popups(@,/) > composer > transcript -``` -`esc` always steps down one rung. `^K` works from every rung. Quit (`^C`) confirms only when a run is live or the composer holds a draft. - ---- - -## 8. Approval queue (kills D4 β€” ship this first, even standalone) - -Replace the single `m.approval` pointer with a FIFO queue: - -- Panel shows **`1 of N`**, the current request (risk chip Β· plain-language description Β· verbatim command, `tab` to expand), and remaining count. -- Keys: `A` approve Β· `D` deny Β· `T` trust (when `allow_trust`) Β· `tab` expand Β· transcript scroll still works. WebUI keyboard parity. -- **Friction unchanged**: typed literal `approve`, trust withdrawn, count shown β€” our existing implementation is correct and stays. -- Batch requests from the server render as one card per command with per-command risk classes (the server already classifies every command in `parallel_shell`/`batch_patch`). -- Trust/friction state survives queue advancement; `approval_ack` advances the queue with a one-line confirmation. - ---- - -## 9. Composer & status bar - -**Composer**: existing textarea, plus β€” attachment chips row (`πŸ“Ž name βœ•`, drag-free terminal idiom for `/attach`), queue-depth indicator, placeholder that teaches (`ask… Β· @file Β· /command Β· ^K everything`), `βŒ₯⏎` = send-with-thinking toggle preview. Multi-line indicator when the draft spans rows. - -**Status bar**: one row, two jobs. Left: connection + run state + context-aware hints (the *only* place hints live; they change with focus β€” composer focused β†’ send keys; transcript scrolled β†’ jump keys; drawer open β†’ tab keys). Right: session tokens, RTT, `? help Β· ^K`. This absorbs today's footer and kills the double status line. - ---- - -## 10. Empty, first-run, and recovery states (kills D8) - -- **First run** (no sessions, fresh server): a welcome card β€” connection summary (from `server_info`: version, model, sandbox, stream), three suggested prompts as sendable rows, one-line "⌘K"-style tour hint. No animation, no wall of text. -- **Disconnected**: a recovery card, not a notice β€” retry (`r`), server log path (`o` to open hint), the attach URL, and "your draft is safe" reassurance. -- **Spawn failure** (`odek` not found): remediation panel β€” install hint, `--odek-bin` flag, `--url` attach alternative. Never a raw Go error string. -- **Empty drawer tabs** explain themselves ("no headless runs yet β€” start one with `r`") and teach the creating action. - ---- - -## 11. Visual language β€” EMBER Terminal (kills D1, D9, D10) - -**Token mapping** from the WebUI (`ui/style.css` β†’ `styles.go`), faithful to the brand: - -| WebUI token | Terminal value | Use | -|-------------|----------------|-----| -| `--amber #ffb224` | `#FFB224` | primary accent: brand, selection, answer emphasis | -| `--amber-hi #ffc95e` / `--amber-lo #ff8a3d` | same | gradient endpoints (banner), user turn bar | -| `--bg-2 #10131a` | *(n/a β€” transparent)* | TUIs sit in the user's terminal; surfaces are spacing + hairlines, not fills | -| `--line rgba(152,170,200,.15)` | `#2E3242` | hairlines: rules, borders, tree glyphs | -| `--text` / `--text-2` | `#E7E9EE` / `#9AA0AE` | body / muted | -| semantic green/yellow/red | `#34D399` `#FBBF24` `#F87171` | status, pass/fail, risk (unchanged) | - -- **Purple retires as primary**; it remains available in a `classic` theme (one config value, mechanical token swap) for existing users. Pink user-bars become amber-warm; assistant turns neutral-bright β€” the *answer* owns the accent. -- **Hierarchy by weight + brightness, not hue count.** Bold for heads, bright for answers, muted for machinery, faint for chrome. The eye gets exactly one anchor per region. -- **Themes**: `ember-dark` (default) Β· `ember-light` (parity with WebUI light tokens: `#F6F4EF` base, `#D98E00` amber) Β· `high-contrast` (pure B/W + amber) Β· `classic` (today's palette). 16-color terminals: lipgloss `colorprofile` degrades automatically (dep already present). -- **Motion budget**: spinner, gauge flash on threshold crossing, selection blink. `NO_MOTION=1` freezes all of it. Nothing else animates β€” ever. -- **Emoji discipline**: emoji stay only in `progress.go` narration (they're load-bearing charm); chrome and labels use monochrome glyphs. Today's mix of emoji+glyphs in chrome reads noisy at small sizes. - ---- - -## 12. Adaptive behavior summary - -| Breakpoint | Cockpit | Composer | Drawer | Stat lines | -|------------|---------|----------|--------|-----------| -| narrow < 80 | 1 row: model + gauge + dot | 2 rows, teaching placeholder | full overlay | essentials only (drop-priority exists) | -| standard 80–119 | 2 rows, full cluster | 3 rows + chips | full overlay | default | -| wide β‰₯ 120 | 2 rows | 3 rows + chips | **split pane** (opt-in `w`) | default + link detail | - ---- - -## 13. Implementation plan - -Grounded in the current code; each phase ships green (tests + lint) and independently useful. - -### Phase 0 β€” Foundations (~1 week) -- Theme tokenization: `styles.go` rebuilt as named EMBER tokens + `theme` variant struct; `classic` alias. No layout changes yet. -- **Approval queue** (D4) β€” correctness fix, shippable standalone: queue state, `1 of N` panel, A/D/T keys, friction unchanged. Tests: parallel `approval_request` ordering. -- Keymap router: one table (context β†’ keys β†’ action) driving both handling and hint rendering; ladder documented in `/help`. -- Width breakpoint helpers + relayout hardening; property test: no viewport overflow at 60…250 cols. - -### Phase 1 β€” The Stage (~1–1.5 weeks) -- Turn cards: head rows, collapse (`c`), jump (`[`/`]`), stat-line relocation; transcript prefix cache reworked accordingly. -- Reasoning accordion with the live auto-expand rule. -- Typed renderers (diff, file, tree, json, tests) as pure functions + golden tests; wired into `renderStep` expansion. -- Cockpit consolidation + popover (`h`); `/stats` becomes the popover's session tab. Status bar merge (absorb footer). - -### Phase 2 β€” The Spine (~1 week) -- Command palette (`^K`): entries registry (commands, sessions, models, actions), fuzzy scorer, chord teaching. -- Drawer frame + `sessions`/`runs`/`events` tabs; runs polling + remote approvals bridge; events filter. -- Split pane on wide terminals (opt-in). - -### Phase 3 β€” Management (~1 week) -- `memory`, `skills`, `tools`, `config` tabs (facts CRUD, episode promote, consolidate, skill promote, MCP listing, config view, connections kick, shutdown confirm). -- Health/usage data into the cockpit popover. - -### Phase 4 β€” Polish (~0.5–1 week) -- First-run, disconnect, and spawn-failure states. -- Light + high-contrast themes; `NO_MOTION`; 16-color pass. -- Golden layout snapshots at 3 breakpoints; README + `/help` refresh. - -**Testing strategy**: golden renders (plain-text) per breakpoint; property tests for layout math; interaction tests for queue/palette/drawer grammar; existing integration stand-ins extended with a queue + drawer scenario. No new dependencies. - ---- - -## 14. Risks & explicit trade-offs - -- **Split pane in Bubble Tea** (two viewports, one frame) is the highest-complexity item β†’ opt-in, Phase 2, cut-line to full-overlay if it fights the framework. -- **Renderer cost** β€” typed renderers run on expand only; step lines cache; the existing prefix-cache pattern absorbs the rest. -- **Theme churn** β€” token swap is mechanical, but muscle memory for colors is real β†’ `classic` ships on day one. -- **Palette vs chords** β€” palette-first can slow experts; mitigated by chord labels in every palette row (graduation path, not lock-in). -- **A11y in terminals is bounded** β€” we do: no color-only signals (glyphs always accompany state), reduced-motion, high-contrast, and screen-reader-friendly plain output via `--plain` (stretch: transcript-only stdout mode). - -## 15. Success criteria - -- Any surface ≀ `^K` + 3 keystrokes; every key chord discoverable in ≀ 1 look (`?`). -- Parallel approvals: zero dropped/overwritten requests (regression test as the gate). -- Long-turn scanability: answer visible within 1 screen of the last tool step via collapse/jump. -- One place to read connection/model/context/cost state (the cockpit), verified by a "state inventory" test that fails on duplication. -- Narrow-terminal usability: full core loop (prompt β†’ approve β†’ read answer) at 70Γ—24. diff --git a/internal/client/plan.go b/internal/client/plan.go index ebc6859..27376fa 100644 --- a/internal/client/plan.go +++ b/internal/client/plan.go @@ -11,7 +11,7 @@ import ( // // odek serve exposes the engine's plan state read-only: the newest parseable // "[Current plan:" system message, parsed with the same strict extractor the -// restart-resume path uses. GET-only by contract; see docs/PLANNING_MODE_UI.md. +// restart-resume path uses. GET-only by contract. // PlanStepStatus is one step's lifecycle state on the wire. type PlanStepStatus string diff --git a/internal/client/plan_test.go b/internal/client/plan_test.go index 5730086..32fe5de 100644 --- a/internal/client/plan_test.go +++ b/internal/client/plan_test.go @@ -8,8 +8,8 @@ import ( ws "golang.org/x/net/websocket" ) -// Tests for GET /api/sessions/{id}/plan (odek serve contract, docs/ -// PLANNING_MODE_UI.md Β§1): structured snapshot decoding, the found:false +// Tests for GET /api/sessions/{id}/plan (odek serve contract): +// structured snapshot decoding, the found:false // shape, HTTP error mapping, and malformed-body tolerance. The endpoint is // read-only by engine contract; the client never sends anything but GET. diff --git a/internal/tui/drawer.go b/internal/tui/drawer.go index 95e0826..f80a4eb 100644 --- a/internal/tui/drawer.go +++ b/internal/tui/drawer.go @@ -19,7 +19,7 @@ import ( // tabBar renders the drawer's tab strip after the panel title: the active // tab in accent, the rest muted, with the digit shortcuts taught inline. -// When seven tabs don't fit the given width, the strip collapses to the +// When the strip doesn't fit the given width, it collapses to the // active tab behind an ellipsis β€” cycling and digits still reach the rest. func (m *Model) tabBar(maxw int) string { var parts []string @@ -132,7 +132,7 @@ type drawerTab struct { // drawerTabs lists the drawer's tabs in display order. Every management // surface is a tab: the same ]/[ cycle, digit jump, strip, and r/⏎ refresh -// grammar governs all seven. +// grammar governs every tab. func drawerTabs() []drawerTab { return []drawerTab{ {"sessions", panelSessions, func(m *Model) tea.Cmd { return m.openSessions() }}, diff --git a/internal/tui/plan.go b/internal/tui/plan.go index 63e9266..56566a2 100644 --- a/internal/tui/plan.go +++ b/internal/tui/plan.go @@ -10,7 +10,7 @@ import ( "github.com/BackendStack21/bodek/internal/client" ) -// ── planning surface state (docs/PLANNING_MODE_UI.md Β§3.2) ───────────────── +// ── planning surface state ───────────────────────────────────────────────── // // bodek's WS carries triggers only: every engine plan mutation arrives as an // ordinary plan tool_call/tool_result pair. The structured truth lives behind diff --git a/internal/tui/plan_steps.go b/internal/tui/plan_steps.go index 9978e6f..e466335 100644 --- a/internal/tui/plan_steps.go +++ b/internal/tui/plan_steps.go @@ -6,7 +6,7 @@ import ( "strings" ) -// ── transcript specialization for plan steps (docs/PLANNING_MODE_UI.md Β§4A) ─ +// ── transcript specialization for plan steps ─────────────────────────────── // // Plan mutations are ordinary tool_call events, so without help each status // change renders as an opaque JSON preview row and long runs drown in them. diff --git a/internal/tui/plan_steps_test.go b/internal/tui/plan_steps_test.go index ede80aa..118a41d 100644 --- a/internal/tui/plan_steps_test.go +++ b/internal/tui/plan_steps_test.go @@ -7,8 +7,8 @@ import ( "github.com/BackendStack21/bodek/internal/client" ) -// Tests for plan tool_call transcript specialization (docs/PLANNING_MODE_UI.md -// Β§4A): semantic one-liners replace the JSON preview, hostile model-authored +// Tests for plan tool_call transcript specialization: semantic one-liners +// replace the JSON preview, hostile model-authored // text is sanitized/truncated, and anything unparseable falls back to the // generic argPreview path. diff --git a/internal/tui/plan_strip_test.go b/internal/tui/plan_strip_test.go index badf112..88ebc9c 100644 --- a/internal/tui/plan_strip_test.go +++ b/internal/tui/plan_strip_test.go @@ -10,7 +10,7 @@ import ( "github.com/BackendStack21/bodek/internal/client" ) -// Tests for the live plan strip (docs/PLANNING_MODE_UI.md Β§4B) and the /plan +// Tests for the live plan strip and the /plan // slash command. The strip's zero-footprint rule: idle, no-plan, collapsed, // or degraded engine β‡’ empty label, no layout cost. diff --git a/internal/tui/plan_tab_test.go b/internal/tui/plan_tab_test.go index df49da1..9dae1fd 100644 --- a/internal/tui/plan_tab_test.go +++ b/internal/tui/plan_tab_test.go @@ -10,8 +10,7 @@ import ( "github.com/BackendStack21/bodek/internal/client" ) -// Tests for the drawer Plan tab surface (docs/PLANNING_MODE_UI.md Β§4C): -// opener states, Telegram-parity rows + summary badge, detail submode per +// Tests for the drawer Plan tab surface: opener states, Telegram-parity rows + summary badge, detail submode per // house grammar (⏎ expand / esc fold), and the silent-degrade empty states. // Snapshots are injected through handlePlanMsg so no server is involved. diff --git a/internal/tui/plan_test.go b/internal/tui/plan_test.go index aca0ed0..266e084 100644 --- a/internal/tui/plan_test.go +++ b/internal/tui/plan_test.go @@ -7,8 +7,8 @@ import ( "github.com/BackendStack21/bodek/internal/client" ) -// Tests for the planning-surface state machine (docs/PLANNING_MODE_UI.md -// Β§3.2): WS-trigger scheduling + debounce sequencing, monotonic acceptance, +// Tests for the planning-surface state machine: WS-trigger +// scheduling + debounce sequencing, monotonic acceptance, // tab-visible poll lifecycle, and the session-switch reset hook. Assertions // ride synchronous observables (seq counters, flags, accepted state); fetch // closures are never executed β€” the wire contract lives in internal/client, diff --git a/internal/tui/view.go b/internal/tui/view.go index 7512baa..d05f959 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -303,7 +303,7 @@ func (m *Model) statusLine() string { if n := len(m.queue); n > 0 { q = th.acDetail.Render(fmt.Sprintf(" Β· %d queued", n)) } - // Live plan strip (docs/PLANNING_MODE_UI.md Β§4B): rides the same row, + // Live plan strip: rides the same row, // silent unless a run is active AND a plan exists β€” absence costs zero // pixels. Bounded to a short label so small terminals keep the row sane. strip := ""