A beautiful Bubble Tea terminal interface for the odek agent.
██████ ██████ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██ ██ ██ ██ █████ █████
██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ███████ ██ ██
bodek is a pure front-end. It launches (or attaches to) an odek serve
instance and renders the agent's live stream — reasoning, tokens, tool calls,
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.
# 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=<your-key>
# 3 · Chat
bodekPrefer a compiled binary? Grab one from the releases page — see Install.
Contents: What & why · Install · Usage · Features · Using bodek · Configuration · Security model · Troubleshooting · Development
odek already ships a streaming WebSocket protocol (the one its Web UI speaks). bodek reuses that exact protocol from the terminal, which means:
- Zero duplicated logic — tools, the
dangerapproval engine, the Docker sandbox, skills, and memory all run inside odek, unchanged. - Full fidelity — token streaming, per-tool activity, and security prompts appear in the TUI exactly as the engine emits them.
- One source of truth — upgrade odek and bodek gets the new behaviour for free.
- Minimal core footprint — odek stays deliberately dependency-light, while the rich terminal UI lives here with its Bubble Tea, glamour, and other front-end dependencies.
┌──────────────┐ WebSocket (RFC 6455, JSON) ┌──────────────────┐
│ bodek │ ◄────────────────────────────► │ odek serve │
│ (Bubble Tea) │ tokens · tools · approvals │ (ReAct engine, │
│ TUI client │ │ tools, sandbox) │
└──────────────┘ └──────────────────┘
Prerequisite: bodek is only the front-end — you also need the odek
engine. See odek's install instructions
(any OpenAI-compatible provider key via ODEK_API_KEY).
Download the latest compiled binary from the
releases page — archives
are published for Linux, macOS, and Windows (amd64 & arm64), with
checksums.txt for verification.
One-liner for Linux / macOS (resolves the latest asset for your platform and
installs into ~/.local/bin):
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m); [ "$ARCH" = "x86_64" ] && ARCH=amd64
URL=$(curl -fsSL https://api.github.com/repos/BackendStack21/bodek/releases/latest \
| grep browser_download_url | grep "${OS}_${ARCH}" | cut -d '"' -f 4)
curl -fsSL "$URL" | tar -xz bodek && install -m 755 bodek ~/.local/bin/On Windows, download the windows_amd64 (or arm64) .zip from the
releases page and put bodek.exe on your PATH.
# Install odek (the engine) and bodek (the TUI)
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=<your-key>
bodekbodek looks for odek on your PATH. To point at a specific binary use
--odek-bin, or skip spawning entirely with --url.
bodek # launch odek serve and start chatting
bodek --sandbox # run tool calls inside odek's Docker sandbox
bodek --url 'http://127.0.0.1:8080/?token=…' # attach with the token URL odek serve printed
bodek --url http://127.0.0.1:8080 --token d3adb33f # attach with an explicit token
bodek --odek-bin ./odek # use a specific odek binary
bodek --mouse # enable mouse wheel scrolling (blocks text selection)
bodek --bel=false # mute the attention bell (title still updates)
bodek --notify # desktop notifications (OSC 9) on turn/approval events
bodek --theme ember-light # start with a theme (/theme switches live)
bodek --plain # linear mode: transcript to scrollback (a11y, pipes)
bodek -- --prompt-caching # pass extra flags through to `odek serve`
bodek version # print the bodek version
bodek upgrade # download and install the latest releaseodek serve protects its WebSocket and REST APIs with a per-instance token it
prints to stderr at startup (odek serve ⚡ http://…/?token=…). When bodek
spawns the server it picks the token up automatically; when you attach to one
that's already running, paste the token URL into --url or pass the token
itself via --token. Older odek versions without token enforcement are
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. bodek's
own front-end settings are separate; see Configuration.
- 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/themeand 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,
^Ffolds noisy turns,alt+↑/alt+↓jump turn-to-turn, reasoning accordions auto-expand live and collapse on the next turn, and^Eexpands every tool step's details. - Typed tool renderers — diffs tint with a
+N −Mchip, file reads get line numbers, JSON pretty-prints, and step lines earn typed chips from structured output only: test verdicts (✓ 5 passed · 2 skipped, go coverage), git commits/pushes (⎇ a1b2c3d,↑ main), lint results, compiler warning counts, HTTP statuses, and search hit counts. Prose like "Build passed" never goes green. - Streaming answers rendered as Markdown (glamour).
- Tool activity — every
tool_call/tool_resultshown 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 linesfooter, 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 upgradeinstalls it; dev builds are never nagged).
- 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_logactivity nests beneath the delegating call, so a sub-agent's progress reads as its own branch of the step tree. Each card carries its task's goal (parsed from thedelegate_tasksargument; tasks the wire hasn't confirmed yet show aspending), live step/tool/iterations/tokens telemetry fromsubagent_stateframes (odek v1.30+), a client-side elapsed timer between frame bursts, and terminal status glyphs (✓success,◐partial,✗error,⊘cancelled,⏱timeout). The collapsed rollup counts failures —2/3 · 1 ✗ · 8.1k tok— a terminal failure sticks to the notice strip until the turn ends, and every delegating turn closes with aswarm: 5 ✓ · 1 ✗ — SA4 errorverdict. A disconnect retires in-flight cards (× lost on disconnect) instead of leaving ghost spinners.ctrl+s(or/stop <SA#>, two-step confirmed) stops one running sub-agent of the current turn; the/agentstab'screaches any live task through the instance registry. - 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 (emarkdown,EJSON), and search server-side (/);nloads the next page. Resuming sends asession_switchso 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.
- Inline approvals — odek's
dangerengine prompts surface where the context is; decisions go straight back over the socket. See 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,
/stopand^Lare two-step, and the server shutdown requires typing the literal wordshutdown. See the Security model.
- 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;/statsrolls 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
/statsadds themax_cost_usdcap 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);
/serveropens the full cockpit card.
- 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
--plainthe accessible surface for screen readers — and the natural one for pipes:bodek --plain < task > run.log. NO_COLORdegrades the entire EMBER palette to plain text in every mode;NO_MOTION=1swaps animated spinners for static frames.
- Spawn or attach — bodek launches a private
odek serveby 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
disconnectedbadge (empty input⏎retries manually).
- Attention when backgrounded — turn completion and pending approvals
set the terminal window title (
✓ done — <model>/⚠ approval needed — <model>) and ring the bell (--bel=falsemutes);--notifyadds OSC 9 desktop notifications. Fires only on terminal states — never per token. - Sandbox aware — the header shows
🛡 sandboxedor⚠ host access; pass--sandboxto run tool calls inside odek's Docker isolation.
| Key | Action |
|---|---|
⏎ |
Send the prompt (queues it while a turn is running) |
^K |
The palette — everything: commands, sessions, models, drawer tabs |
/ |
Open the command palette (see below) |
@ |
Attach a file (see below) |
alt+↑ / alt+↓ |
Jump to the previous / next turn |
alt+y |
Copy the focused turn's reply — the one you last jumped to (falls back to the latest reply) |
alt+r |
Re-send the last prompt (/retry) |
alt+f |
Search the transcript (⏎ next match · N previous) |
^F |
Fold/unfold the most recent turn card (click any turn head with --mouse) |
tab |
Open/close the latest reasoning block (live turns auto-expand) |
^R |
Browse & resume saved sessions |
^O |
Switch the model |
^Q |
Focus the queue strip (↑↓/jk select · ←→/hl move · d delete · esc/⏎ back to the input) |
^T |
Toggle extended thinking for the next turn |
^J |
Insert a newline in the input |
^L |
Clear the conversation (two-step confirm: y clears, any other key cancels) |
^E |
Toggle tool details — every step expands to its full output/logs |
^Y |
Copy the last reply to the clipboard (OSC 52 — needs a supporting terminal) |
Esc |
Cancel the running turn (two-step confirm: y cancels, any other key keeps running; queued prompts return to the input) |
↑ / ↓ / PgUp / PgDn / ^U / ^D |
Scroll the transcript (arrows at the input's edge lines) |
^P / ^N |
Recall previous prompts (prompt history) |
^G / End (empty input) |
Jump to the latest output |
F1 |
Show the help card |
wheel (with --mouse) |
Scroll the transcript · click tool rows, turn heads, and the cockpit |
⏎ (disconnected, empty input) |
Retry the connection |
^C |
Quit |
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.
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.
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 ⏎.
| Command | Action |
|---|---|
/help |
Show available commands and key bindings |
/clear |
Clear the conversation (two-step confirm; idle only) |
/copy |
Copy the last reply to the clipboard (OSC 52) |
/retry |
Re-send the last prompt (queues it if a turn is running) |
/theme [name] |
Switch the color theme at runtime and persist it (ember-dark · ember-light · high-contrast · classic) |
/stats |
Session metrics card (cost, cache, context gauge) |
/server |
Cockpit — server, link, budget & session in one card (or click the header) |
/sessions |
Browse, search, pin, rename, export & resume sessions |
/runs |
Headless REST runs — live status, remote approvals, cancel |
/run <prompt> |
Start a headless run (fresh session) and watch it in the runs tab |
/events |
The odek.event/v1 runtime feed |
/plan |
Structured task plan of this session (live status) |
/memory |
Facts by target, pending-episode promote, consolidate |
/skills |
Skill provenance badges & promote |
/tools |
Tool registry with enabled state & MCP servers |
/config |
Sanitized config, lifetime usage, connections (kick) |
/model [name] |
Switch model (opens a picker with no argument) |
/thinking [on|off] |
Toggle extended thinking for the next turn |
/cancel |
Cancel the running turn |
/stop <SA#> |
Stop one running sub-agent (bare /stop lists them) |
/agents |
Sub-agent registry — live 3s poll, c stop (two-step), o jump to transcript |
/attach <path> |
Stage a file to send with the next prompt (5 MB each, 10 MB total) |
/unattach [name] |
Drop staged files (all when no name given) |
/quit |
Exit bodek |
/sessions, /runs, /agents, /events, /plan, /memory, /skills,
/tools, and /config all open tabs of one drawer with a shared grammar:
]/[cycle tabs ·1–9jump ·rrefresh ·esccloses.- 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 config values.↑/↓scroll it,esc/qfolds back (selection kept), andppromotes straight from the detail — no more promoting blind. - Sessions —
/search (server-side),ppin,rrename,e/Eexport md/json,ddelete (yconfirms — deletes are always two-step),⏎resume. - Runs — live 3s poll,
A/D/Tremote approvals,ccancel,prefresh pending approvals,edrill into the run's event trail. - Agents — the serve instance's sub-agent registry, live-polled every 3s;
cstop the highlighted row (two-step, same gate as/stop),ojump to the delegating transcript step,⏎the full registry record. - Events — the
odek.event/v1ring:ffilter to this session,xclear filters (a runs-tab drill-in scopes it to one run). - Plan — the engine's structured task plan (Telegram-parity renderer):
summary badge (
v7 · 2/4 done · 1 blocked) plus one row per step;⏎expands the selected step's full title/note. Read-only — the plan is steered by the model; while a run is active and a plan exists, a live▸ plan 2/4 · <active step>summary rides the busy line. - Memory —
a/Aadd user/env facts,ddelete fact (yconfirms),ppromote a pending episode,c/Econsolidate. - Skills — provenance badges plus a dim description line;
ppromote (also from the detail view),Pforce-promote tainted. - Tools/Config — registry + MCP servers; config values flatten one
level (
sandbox.enabled), nested values show raw JSON in the detail; lifetime usage,dkick a connection,Styped shutdown death-gate.
Type @ to attach a file. bodek searches the working tree and shows a
completion popup; ↑/↓ to choose, ⏎ or ⇥ to insert, esc to dismiss.
> summarize @internal/client/client.go and explain the protocol
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 @.)
When the agent requests approval for a dangerous operation, pick an outcome from the panel and confirm — typing never answers by accident:
| Key | Action |
|---|---|
↑ / ↓ (or ← / →) |
Move the highlight (Approve / Deny / Trust class when offered) |
⏎ |
Confirm the highlighted option |
Esc |
Deny (abort) |
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.
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.
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. odek's server-side
configuration is unaffected and stays in ~/.odek/config.json.
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.
- 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 typingshutdownletter 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.
bodekcan't findodek— the engine must be on yourPATH, or pass--odek-bin /path/to/odek.- Auth errors when attaching — copy the full token URL
odek serveprinted (--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
--mousetrade-off (terminals can't have both). Launch without it when you need to copy. - Colors look wrong — try
/theme classic, checkTERM;NO_COLOR=1forces 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 withBODEK_CONFIG). See docs/CONFIGURATION.md. - Windows — use Windows Terminal or another ANSI/OSC-capable emulator; desktop notifications depend on OSC 9 support.
make build # → bin/bodek
make run # build and launch
make test # go test -race ./...
make cover # coverage report for internal packages
make lint # golangci-lint (if installed)
make vet
make tidyContinuous integration runs build, go vet, golangci-lint, and the
race-enabled test suite on every push (see
.github/workflows). Tagged releases (vX.Y.Z) are
built and published automatically by
GoReleaser.
Project layout:
| Path | Responsibility |
|---|---|
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 |
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).
- AGENTS.md — the contributor contract: commands, the mandatory pre-commit checklist, testing expectations, and code conventions.
- docs/INTEGRATIONS.md — the audit matrix mapping
every
odek serveREST endpoint and WebSocket message to its client method and UI surface. Update it when either side changes. - docs/CONFIGURATION.md — the front-end settings and environment variable reference.
MIT — see LICENSE.