Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/BackendStack21/bodek.svg)](https://pkg.go.dev/github.com/BackendStack21/bodek)
[![Go Report Card](https://goreportcard.com/badge/github.com/BackendStack21/bodek)](https://goreportcard.com/report/github.com/BackendStack21/bodek)

**A beautiful [Bubble Tea](https://github.com/charmbracelet/bubbletea) terminal interface for the [odek](https://github.com/BackendStack21/odek) agent.**
**A [Bubble Tea](https://github.com/charmbracelet/bubbletea) terminal interface for the [odek](https://github.com/BackendStack21/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
approvals, skills, and memory — in a full-featured TUI. Every bit of agent
behaviour (tools, danger gating, sandbox, skills, memory, sessions) comes from
**odek itself**; bodek never re-implements any of it.

Expand Down Expand Up @@ -439,7 +439,7 @@ prompt can start with `?`, `[`, or any other character.
Click a tool header or reasoning block to inspect it; while inspecting, `↑`/
`↓` move between items and Enter expands the focused one. Tool responses
display at most eight body rows plus a paging indicator, with fewer
rows in short terminals. Use `PgUp`/`PgDn` to page, `Alt+Y` to copy the
rows in short terminals. Use `PgUp`/`PgDn` to page, `alt+y` to copy the
retained response, and Escape to return to typing. The global `^E`
details toggle uses the same page limits.

Expand Down
2 changes: 1 addition & 1 deletion cmd/bodek/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestBuildProgramOptionsDefault(t *testing.T) {
if len(opts) != want {
t.Fatalf("expected %d default program options (filter, assembler, alt-screen, mouse), got %d", want, len(opts))
}
// Sanity check: the option is callable like a real tea.ProgramOption.
// The option is callable like a real tea.ProgramOption.
var p tea.Program
_ = p
_ = opts[0]
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ silently ignored.
| `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). |
| `verbosity` | string | `normal` | `--verbosity` | Noise dial: `quiet` (info notes hidden), `normal`, `detailed` (`^E` expand-all view). `/verbosity` switches live **and persists it here**. An explicit `--verbosity` still wins for that launch. |
| `thinking` | string | (inherit) | `--thinking` | Reasoning depth: `disabled` · `low` · `medium` · `high`. `^T` cycles; `/thinking` opens a picker (or `/thinking <level>` sets directly) **and persist it here**. `/thinking inherit` (or `default`) clears the key so the next launch seeds from odek again. Empty inherits the odek serve default until you set a level. Requires odek ≥ v2.5.0 for string `/api/config.thinking` and inherit-on-omit. |
| `thinking` | string | (inherit) | `--thinking` | Reasoning depth: `disabled` · `low` · `medium` · `high`. `^T` cycles; `/thinking` opens a picker (or `/thinking <level>` sets directly) **and persists it here**. `/thinking inherit` (or `default`) clears the key so the next launch seeds from odek again. Empty inherits the odek serve default until you set a level. Requires odek ≥ v2.5.0 for string `/api/config.thinking` and inherit-on-omit. |
| `resume` | bool | `false` | `--resume` | Resume this directory's last session on start (transcript replay). Off by default: a launch starts fresh. `--new` always wins. |

Unset keys fall back to their defaults — the file only ever stores choices
Expand Down
2 changes: 1 addition & 1 deletion internal/client/turn_started_decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestTurnStartedDecode(t *testing.T) {
}
}

// R3: streamed frames carry turn_id while a turn is live.
// Streamed frames carry turn_id while a turn is live.
func TestTurnIDOnStreamedFrames(t *testing.T) {
var ev Event
if err := json.Unmarshal([]byte(`{"type":"tool_call","name":"shell","data":"{}","turn_id":"t_0123abcd"}`), &ev); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/tokens/tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestPersistWriteAndRenameErrors(t *testing.T) {
}

func TestConcurrentAccess(t *testing.T) {
// Smoke test for the Store's locking: run with -race to catch data races.
// Exercises the Store's locking; run with -race to catch data races.
s := &Store{m: map[string]string{}}
done := make(chan struct{})
for i := 0; i < 4; i++ {
Expand Down
File renamed without changes.
File renamed without changes.
12 changes: 6 additions & 6 deletions internal/tui/bindings_hardening_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

// ── adversarial-review hardening (R1/R2/R3 findings) ────────────────────────

// R1-F2/R1-F4: shift+digit and shift+punctuation chords must NOT decode to
// Shift+digit and shift+punctuation chords must NOT decode to
// the unshifted glyph (keyboard-layout-dependent) — they surface as the
// unmapped sentinel; unmodified CSI-u printables pass through as typed text.
func TestShiftDigitBecomesSentinelNotGlyph(t *testing.T) {
Expand All @@ -29,7 +29,7 @@ func TestShiftDigitBecomesSentinelNotGlyph(t *testing.T) {
}
}

// R1-F6: kitty key-release reports (event types 2/3) must never decode as a
// Kitty key-release reports (event types 2/3) must never decode as a
// fresh keypress — a release of Enter must not submit twice.
func TestKittyReleaseEventsIgnored(t *testing.T) {
for _, seq := range []string{"\x1b[13;1;2u", "\x1b[13;1;3u", "\x1b[13;5;3u"} {
Expand All @@ -45,7 +45,7 @@ func TestKittyReleaseEventsIgnored(t *testing.T) {
}
}

// R2-F1: the sentinel must never type into any capture surface. It is
// The sentinel must never type into any capture surface. It is
// intercepted at the top of handleKey — verify for the AC popup path.
func TestUnmappedChordNeverTypesAnywhere(t *testing.T) {
m := newTestModel()
Expand Down Expand Up @@ -73,7 +73,7 @@ func TestUnmappedChordNeverTypesAnywhere(t *testing.T) {
}
}

// R2-F2: a newline chord on the AC popup must insert a newline but NOT
// A newline chord on the AC popup must insert a newline but NOT
// auto-accept the completion (no expanded completion text splices in) —
// Enter alone accepts. The popup may close naturally when the newline
// ends the completion token, same as typing a space.
Expand All @@ -89,7 +89,7 @@ func TestACNewlineChordDoesNotAccept(t *testing.T) {
}
}

// R3-G5: the ^K gate must also hold while a clarify card is head.
// The ^K gate must also hold while a clarify card is head.
func TestCtrlKGatedDuringClarify(t *testing.T) {
m := newTestModel()
m.handleEvent(client.Event{Type: "clarify_request", ID: "clr", Question: "which one?"})
Expand All @@ -107,7 +107,7 @@ func TestCtrlKGatedDuringClarify(t *testing.T) {
}
}

// R3-G4: bare s/x must not answer a suggestion while the queue strip holds
// Bare s/x must not answer a suggestion while the queue strip holds
// focus — the strip owns the keyboard until esc/⏎/^Q.
func TestQFocusBlocksPlainSuggestKeys(t *testing.T) {
m := newTestModel()
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/jobs_tab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func jobsMux(t *testing.T, jobsJSON string, extra map[string]http.HandlerFunc) (
return m, &seen
}

// ── M2: jobs drawer tab ──────────────────────────────────────────────────────
// ── jobs drawer tab ──────────────────────────────────────────────────────────

func TestJobsTabRowsAndSelection(t *testing.T) {
m := newJobsTestModel(t, nil)
Expand Down Expand Up @@ -243,7 +243,7 @@ func TestJobsSlashCommandAndPalette(t *testing.T) {
}
}

// ── M3: lifecycle watcher ────────────────────────────────────────────────────
// ── lifecycle watcher ────────────────────────────────────────────────────────

func TestJobsWatcherBaselineAndDiffNotes(t *testing.T) {
m := newJobsTestModel(t, nil)
Expand Down
7 changes: 3 additions & 4 deletions internal/tui/keybindings_chords_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import (
"github.com/BackendStack21/bodek/internal/client"
)

// ── enhanced-key chord fixes (plan: .plans/KEY_BINDINGS_FIX_PLAN.md) ────────
// RED-first: ctrl+enter must be distinguishable from plain enter, shifted
// CSI letter chords must reach the composer, and the shift+enter sentinel
// must not leak into the friction confirmation buffer.
// Enhanced-key chord contract: ctrl+enter must be distinguishable from plain
// enter, shifted CSI letter chords must reach the composer, and the
// shift+enter sentinel must not leak into the friction confirmation buffer.

// TestFilterRewritesCtrlEnterCSI pins the decode of Ctrl+Enter from kitty
// CSI-u and xterm modifyOtherKeys encodings: today the ctrl modifier is
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ type Model struct {
usageSnap *client.Usage

sessCtxTok int
subCosts map[string]float64 // finished sub-agent final costs by task id (wire v2 P6)
subCosts map[string]float64 // finished sub-agent final costs by task id
sessOutTok int
winCtxTok int // live context-window fill: last parent prompt (windowTokens)
runCtxCum int // pre-v2.3: last cumulative contextTokens (fill = delta)
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/subagent_cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
)

// Sub-agent LLM calls cost real money, and odek reports each finished
// task's final spend (cost_usd on the finished subagent_state frame —
// wire v2 P6). The session-cost surfaces — header, /stats, and the
// task's final spend (cost_usd on the finished subagent_state frame).
// The session-cost surfaces — header, /stats, and the
// cockpit cap row — must add that spend on top of the main-loop token
// estimate, summed once per task id so replayed frames never double-count.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package tui

// R1–R5 transcript-signal refinements, written RED-first. Each test pins one
// refinement: generalized verdict chips (build/vet/race), collapsed dot
// Transcript-signal refinements. Each test pins one
// behavior: generalized verdict chips (build/vet/race), collapsed dot
// tallies, per-child agent state glyphs, chip-style receipts, and the
// last-event age stamp on the streaming head.

Expand All @@ -15,7 +15,7 @@ import (
"github.com/BackendStack21/bodek/internal/client"
)

// ── R1: verdict chips for build / vet / lint / race ─────────────────────────
// ── verdict chips for build / vet / lint / race ─────────────────────────────

func TestBuildVerdictChips(t *testing.T) {
th := newTheme()
Expand Down Expand Up @@ -77,7 +77,7 @@ func TestLintChipVocabulary(t *testing.T) {
}
}

// ── R2: collapsed dot tallies ───────────────────────────────────────────────
// ── collapsed dot tallies ───────────────────────────────────────────────────

func TestCollapseTallyDots(t *testing.T) {
m := newTestModel()
Expand All @@ -93,7 +93,7 @@ func TestCollapseTallyDots(t *testing.T) {
if !strings.Contains(got, "3 tool steps · ··✗") {
t.Errorf("collapsed summary missing dot tally: %q", got)
}
// Sanity: all-fine turn shows no ✗.
// All-fine turn shows no ✗.
ok := message{role: roleAsst, collapsed: true,
steps: []step{{name: "read_file", arg: "a.go", done: true, result: "ok"}}}
if !strings.Contains(m.collapseSummary(ok), "1 tool step · ·") {
Expand All @@ -113,7 +113,7 @@ func TestCollapseTallyDots(t *testing.T) {
}
}

// ── R3: per-child agent state glyphs ────────────────────────────────────────
// ── per-child agent state glyphs ────────────────────────────────────────────

func TestAgentStateGlyphVocabulary(t *testing.T) {
cases := []struct {
Expand Down Expand Up @@ -144,7 +144,7 @@ func TestAgentStateGlyphVocabulary(t *testing.T) {
}
}

// ── R4: chip-style turn receipt ─────────────────────────────────────────────
// ── chip-style turn receipt ─────────────────────────────────────────────────

func TestReceiptChips(t *testing.T) {
r := receipt{files: 2, adds: 3, dels: 1, hasDiff: true, tests: "✓"}
Expand All @@ -157,7 +157,7 @@ func TestReceiptChips(t *testing.T) {
}
}

// ── R5: last-event age on the streaming head ────────────────────────────────
// ── last-event age on the streaming head ────────────────────────────────

func staleFixture() (*Model, func(time.Time)) {
m := newTestModel()
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/turn_started_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestTurnStartedOperatorOpensPlainRemoteCard(t *testing.T) {
}
}

// R2 idempotency: a replayed turn_started must not stack a second card.
// A replayed turn_started must not stack a second card.
func TestTurnStartedIdempotent(t *testing.T) {
m := newTestModel()
m.handleEvent(client.Event{Type: "turn_started", TurnID: "t_ab12", Initiated: "system"})
Expand Down
Loading