Skip to content

bodek: a beautiful Bubble Tea TUI for the odek agent - #1

Merged
jkyberneees merged 5 commits into
mainfrom
claude/trusting-cerf-cswat2
Jun 18, 2026
Merged

jkyberneees merged 5 commits into
mainfrom
claude/trusting-cerf-cswat2

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

bodek is a beautiful Bubble Tea terminal interface for the odek agent.

It is a pure front-end: it launches (or attaches to) an odek serve instance and renders the agent's live stream over odek's WebSocket + REST protocol. All agent behaviour — tools, the danger approval engine, the Docker sandbox, skills, memory, sessions — is provided by odek itself; bodek never re-implements any of it.

┌──────────────┐   WebSocket (RFC 6455, JSON) + REST   ┌──────────────────┐
│    bodek     │ ◄───────────────────────────────────► │   odek serve      │
│ (Bubble Tea) │   tokens · tools · approvals · @refs   │  (ReAct engine)   │
└──────────────┘                                        └──────────────────┘

What it does

  • Streaming chat — token-by-token answers rendered as Markdown (glamour).
  • Live tool activity — every tool_call/tool_result with a per-tool glyph, spinner, argument preview, and one-line result.
  • Security approvals — odek's danger prompts surface as an inline panel; answer a/d/t.
  • @ file/session attachments — live autocomplete via /api/resources; references resolve server-side through odek's untrusted-content boundary.
  • Session browser (^R) — resume, replay, and delete past conversations (/api/sessions).
  • Model switcher (^O) — /api/models.
  • Cancellation (Esc) — abort a running turn via /api/cancel.
  • Sandbox aware — header shows 🛡 sandboxed / ⚠ host access; --sandbox opts into Docker isolation.
  • Context-aware progress — 🧪 running tests, 📖 reading client.go, 🚀 pushing, with a live elapsed timer.
  • Fluent UX — gradient wordmark/hairline, smooth braille spinner, smart autoscroll, scroll indicator.

Per-session auth tokens are persisted in ~/.bodek/sessions.json so resume/cancel/delete work across runs (mirroring the Web UI's localStorage).

Project layout

Path Responsibility
cmd/bodek CLI entry: flags, lifecycle, wiring
internal/server Launch/attach to odek serve, resolve the auth token
internal/client odek serve WebSocket + REST protocol
internal/tokens Local persistence of per-session auth tokens
internal/tui Bubble Tea model, update loop, panels, view

Quality

  • ~99% internal-package statement coverage (client 100%, tui 99%, tokens 98%, server 95% — the remainder is unreachable OS-error handling), via unit + integration tests against an in-process odek serve stand-in.
  • golangci-lint v2 clean, go vet clean, gofmt clean.
  • CI (build, vet, race tests + coverage, lint) and a GoReleaser release workflow on version tags.

Verification

End-to-end validated against a locally built odek serve: token auth, WebSocket dial, prompt/event streaming, and the resource/session/model/cancel REST endpoints all work. The interactive TUI itself was not run on a TTY in CI and a full LLM turn was not exercised (no API key / network egress in the build env).

🤖 Generated with Claude Code

https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf


Generated by Claude Code

jkyberneees and others added 4 commits June 18, 2026 08:50
bodek is a beautiful terminal front-end for odek. Rather than
re-implementing any agent logic, it launches (or attaches to) an
`odek serve` instance and renders its streaming WebSocket protocol —
reusing odek's tools, danger approval engine, sandbox, skills, memory,
and sessions verbatim.

- internal/server: spawn/attach to `odek serve`, resolve the CSRF token
  via GET / Set-Cookie, supervise the subprocess lifecycle
- internal/client: odek serve WebSocket protocol (x/net/websocket) —
  transport plus decoding of every event (token, thinking, tool_call,
  tool_result, done, error, approval_request, skill/memory/agent events)
- internal/tui: Bubble Tea model with live token streaming, per-tool
  activity, inline danger approvals (a/d/t), glamour-rendered Markdown,
  gradient wordmark, and session token/latency telemetry
- cmd/bodek: CLI entry, flags (--url, --odek-bin, --sandbox), lifecycle

Verified end-to-end against a locally built odek serve: connect, token
auth, WebSocket dial, prompt, and event decoding all work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf
Make bodek feel premium and fluent, and add file/session attachments
by reusing odek serve's resource index — no new agent logic.

Fluency & polish:
- smooth braille spinner; live elapsed timer and cycling status verbs
  while the agent reasons
- per-tool glyphs in the activity feed (shell, read, search, browser…)
- gradient hairline under the header (cached per width)
- smart autoscroll: follows the stream while busy, never yanks you when
  scrolled up reading history; scroll-position indicator in the footer

File attachments (@-references):
- internal/client: Resources() queries odek's /api/resources endpoint
- internal/tui: live @-completion popup (↑/↓ select, ⏎/⇥ insert, esc
  cancel) for files and sessions; dynamic relayout reserves its space
- references resolve server-side via odek's resource registry, going
  through the same untrusted-content boundary as any external input

Tests: resource-event decode, ref parsing (activeRef/refStart), tool
glyphs, and a no-TTY smoke test driving a full streaming turn plus the
approval and autocomplete panels at full and narrow widths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf
…lease

Surface the rest of odek serve's API in the TUI and harden the project.

TUI / endpoints (all odek serve APIs now used):
- /api/sessions + /api/sessions/{id}: session browser (^R) to resume,
  replay transcript, and delete past conversations
- /api/models: model switcher (^O)
- /api/cancel: Esc aborts the running turn
- per-session auth tokens persisted in ~/.bodek/sessions.json so resume/
  cancel/delete work across runs (internal/tokens)
- sandbox status shown in the header (🛡 sandboxed / ⚠ host access)
- context-aware progress messages derived from the running tool/command
  (🧪 running tests, 📖 reading X, 🚀 pushing) with live elapsed timer

Tests & quality:
- comprehensive unit + integration tests against an in-process odek serve
  stand-in; internal-package coverage ~99% (client 100, tui 99, tokens 98,
  server 95 — remainder is unreachable OS-error handling)
- golangci-lint v2 config (clean), Makefile test/cover/lint targets
- GitHub Actions: CI (build, vet, race tests + coverage, lint) and a
  GoReleaser release workflow on version tags
- README updated with new features, key bindings, badges, and dev docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf
Verification-protocol finding (Axis 2.8 Adversarial Surface / 2.3 Security):
agent-influenced output — streamed tokens, tool args/results, reasoning,
engine notices, and resumed transcripts — was written to the terminal
without stripping ANSI/control escape sequences, a terminal-injection risk
(cursor/screen manipulation, OSC 52 clipboard exfiltration).

- add sanitize()/isControl(): strip C0 control bytes + DEL (incl. ESC),
  keeping newlines/tabs; applied to all untrusted display paths (collapse,
  streamed token + thinking ingestion, addNote, resumed transcripts)
- tests for the sanitizer and end-to-end defanging

CI: golangci-lint v2 requires golangci-lint-action v7+ (v6 rejects v2.x) —
bump the action so the Lint job runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf

Copy link
Copy Markdown
Contributor Author

🔎 Verification Certificate

Applied The AI Verification Protocol v5.2.7 to this PR. This was a single-agent pass (no multi-provider B/C/D/E pipeline, no replay-sandbox/fuzz infra), so η is reported qualitatively with coverage as a proxy rather than a full mutation/oracle derivation — unverified gaps are listed explicitly per §9.

Classification: GeneratedCode (AI-authored) · Verdict: HumanReviewRecommended

Axes (✅ pass · ⚠️ note · 🔴 fail→fixed)

Axis Status Finding / Resolution
2.1 Semantic Correctness ✅ Logic exercised by unit + integration tests against an in-process odek serve stand-in.
2.2 Behavioral Contract ✅ Event/REST field mapping verified against cmd/odek/serve.go (the spec source), not inferred from bodek.
2.3 Security Surface 🔴→✅ Fixed: untrusted agent/tool output rendered to the terminal without stripping ANSI/control escapes.
2.4 Structural Integrity ✅ Clean client/server/tokens/tui separation; no cycles.
2.5 Behavioral Exploration ⚠️ Disconnect/approval/cancel/empty-size paths covered by tests; a live LLM turn was not exercised (no key/egress).
2.6 Dependency Integrity ✅ All deps pinned in go.mod (no */floating ranges).
2.7 Generator Provenance ✅ N/A (verifier side); commits attributed.
2.8 Adversarial Surface 🔴→✅ Fixed: terminal-injection via ESC/OSC 52 (clipboard exfil) / cursor & screen manipulation in streamed tokens, tool args/results, reasoning, notices, and resumed transcripts.
2.9 Documentation Coverage ✅ README updated with every new feature, key bindings, badges, dev/CI docs.

Remediation applied (auto-repairable, additive — §7.1)

fee8bbe — sanitize()/isControl() strip C0 control bytes + DEL (incl. ESC) while keeping newlines/tabs, applied to all untrusted display paths. Added unit + end-to-end defang tests. This is purely defensive (no behavior change to legitimate content), so it was auto-applied rather than queued for human-only review.

Also fixed: CI Lint job (golangci-lint-action v6 → v7, required for golangci-lint v2).

Quality signals (η proxy)

  • Internal-package statement coverage 98.7% (1122/1137) — client 100%, tui 99%, tokens 98%, server 95% (residual is unreachable OS-error handling).
  • go vet, gofmt, golangci-lint v2 all clean; race-enabled tests pass.

Unverified gaps (§9 — honest disclosure)

  • No end-to-end LLM turn (no API key / network egress in the build env) — streaming render verified by construction + simulated events, not a real provider round-trip.
  • No TTY run of the interactive Bubble Tea screen.
  • η signals m (mutation), f (fuzz), and the independent-oracle o were not computed — out of scope for a single-agent pass.

Rationale: no residual 🔴 after remediation; verdict is HumanReviewRecommended (not AutoApprove) because this is AI-generated code verified by a single agent, and the live behavioral layer (2.5) could not be exercised here — that's the layer the protocol says AI cannot self-evaluate.

🤖 Generated with Claude Code


Generated by Claude Code

Add standard slash-command support and a completion palette that mirrors
the @ popup:

- type "/" for a command palette (↑/↓ select, ⇥ complete, ⏎ run, esc):
  /help, /clear, /sessions, /model [name], /thinking [on|off], /cancel, /quit
- a fully-typed "/cmd args" + ⏎ runs directly; commands work mid-turn
  (e.g. /cancel) and never get sent to the agent
- /help renders a command + keybinding card

Scope @ to attachments only: the completion now suggests files (sessions
are reached via /sessions or ^R), with a relabeled popup and a command
glyph. README, welcome tips, and key-binding docs updated.

Fully covered by tests (commands.go 100%); suite + lint green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rWqu7pktbd2ejfNw3a7Vf
@jkyberneees
jkyberneees merged commit 49d4b09 into main Jun 18, 2026
2 checks passed
@jkyberneees
jkyberneees deleted the claude/trusting-cerf-cswat2 branch June 18, 2026 09:38
jkyberneees added a commit that referenced this pull request Aug 29, 2026
…pt find (#39)

* feat(tui): two-step confirm for ^L and /clear

The whole transcript — plus every turn stat, tool count, and context
counter — was one accidental ^L away from gone, while deleting a single
session or fact in the panels already required the d→y two-step gate.
Arm the same confirmClear gate instead: y fires the wipe, any other key
disarms, the composer footer shows the danger hint. /clear gains the
same idle-only rule ^L always had, so a mid-turn wipe can no longer
drop the view out from under a streaming turn.

* feat(tui): copy the last reply to the clipboard via OSC 52

The #1 post-turn action — pasting the answer into a PR, issue, or
editor — had no keyboard path: selecting long markdown inside a
full-screen alt-buffer TUI is clunky, especially without --mouse.
^Y and /copy now copy the latest finalized assistant reply via OSC 52.
The sequence is emitted through tea.Exec so it reaches the terminal
verbatim (tea.Println output is dropped on the alt-screen), the payload
is consumed by the local terminal emulator only, and oversized replies
are refused rather than silently truncated. Terminals without OSC 52
support ignore the sequence; the note says so. Promotes
charmbracelet/x/ansi from indirect to direct — no new dependency.

* feat(tui): transcript find bar on alt+f

Long agent sessions had no way to search content: turn jumps and
folding navigate by position only. alt+f opens a one-row find strip
above the composer — typed runes filter matches live across message
text, reasoning/reply segments, and tool steps (names, args, results,
sub-agent logs); enter jumps forward, N back, esc closes. The bar
captures the keyboard while open, and the ^L clear-confirm chord stays
live through it. Matches map to message blocks through a per-message
line index built alongside the existing turn/step indexes, so jumps
land with a line of context above the target.

* docs(readme): sync key bindings and commands with the tui

alt+f find bar, ^Y OSC 52 copy, and the two-step confirm on ^L and
/clear.

* test(tui): cover find-bar routing, match kinds, and osc52 runner

Every uncovered block in the UX round's new code now has a test:
find-bar key routing (ctrl+c quit, backspace incl. the empty-query
no-op, busy ctrl+l guard), per-kind matching across reasoning/reply
items and step name/arg/result/logs, stale-match jumps, close
idempotence, find-bar render states (hint, count, truncation), and
the clipboardWrite runner (nil-writer guard, verbatim write, error
propagation).

All functions in find.go and clipboard.go are at 100% statement
coverage; SetStdin/SetStderr stay empty-bodied (zero instrumentable
statements) and are exercised by TestClipboardWriteRunner.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant