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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,19 @@ modifier routes to the composer), and `Alt+A`/`Alt+D`/`Alt+T`
Wrap invocation lines by display cells before paging. Escape control and
invisible characters visibly, and label any argument retention limit.
Replay must offer the same inspection path as live ingestion.
- Deliberate step inspection shows one expanded tool at a time and borrows the
composer rows on short terminals without losing the draft. Keep the selected
tool identifiable and the active detail page visible while paging; sub-agent
children remain chips in the transcript. The pager names whether it shows
invocation or result. Global `^E` still expands all details after leaving
inspection. At 40 columns, a one-time hint must fit completely and the
inspect footer must teach `Alt+I`.
- Approval cards put the exact display-safe command or operation resource first, visibly escape
controls and invisible characters, and state when odek supplied no command
or working directory. Explain that class trust lasts until the current
connection ends; keep allow-once and deny visible at narrow widths. Default
turn footers right-align outcome, elapsed time, tool count, and known cost; full
telemetry remains under `^E` and `/stats`.
- Keep normalized `step.result` for copying/error compatibility and bounded
`step.detailResult` for structured display. Preserve sanitized command/path
identity through live and history ingestion; never infer item boundaries from
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,13 @@ own front-end settings are separate; see [Configuration](#configuration).
and the first-run hints wrap by display cells (including wide characters).
- **The palette (`^K`)** — every command, session, model, and drawer tab one
fuzzy search away; every row teaches its chord.
- **Turn cards** — telemetry sits under the assistant reply, a coding receipt
- **Turn cards** — a short outcome/elapsed/tool/cost line sits under the assistant reply, a coding receipt
(`touched 4 · +82 −19 · tests ✓`) scans what the turn changed, `^F`
folds noisy turns to that receipt, `alt+↑`/`alt+↓` jump turn-to-turn,
and the calm default keeps the transcript still: reasoning previews and
tool responses stay hidden until `^E` (details) reveals the intent rail
(with its `beat N/M` labels) and every step's full output. While the
tool responses stay hidden until deliberate inspection or `^E` (details)
reveals the intent rail (with its `beat N/M` labels) and every step's
full output and turn telemetry. While the
turn streams, its head line carries the run's elapsed counter at the
right edge — the one live clock in the transcript.
- **Typed tool renderers** — diffs tint with a `+N −M` chip, file reads get
Expand All @@ -178,7 +179,9 @@ own front-end settings are separate; see [Configuration](#configuration).
click so a finished step reads as one calm line. An opened step shows its
invocation before its result, including while the tool is running. Tool
arguments are retained up to 256 KiB, with an explicit omission marker if
larger. The
larger. Selecting one step opens a focused inspection path that gives the
transcript the composer rows on short terminals; the draft returns unchanged
on Escape. The
status line is the only spinner. Running steps speak the same progress
copy as the status line (`🧪 running tests`) and tick their own elapsed
clock — and the sealed duration stays on the head after the call
Expand Down Expand Up @@ -327,11 +330,10 @@ own front-end settings are separate; see [Configuration](#configuration).
count. `▶ N jobs` / `✗ job` instruments ride the same bar when a
background job is active; plan progress rides the busy line while a
turn runs and the `/plan` tab otherwise.
- **Per-turn footers & `/stats`** — token counts and latency ride every turn
head (`⚡` latency, `⌂` context, `↳` output tokens, `↗` tok/s, `⚒` tools);
`/stats` opens a sheet that rolls up the session (speed, TTFT, LLM time,
cost, cache, context). The `⎇` glyph is reserved for git commits in the
transcript.
- **Per-turn footers & `/stats`** — a right-aligned row under the reply shows turn outcome, elapsed time,
tool count, and cost when prices are known. `^E` reveals full per-turn
telemetry; `/stats` rolls up the session (speed, TTFT, LLM time, cost,
cache, context). The `⎇` glyph is reserved for git commits in the transcript.
- **Generation speed** — live `↗ tok/s` in the cockpit stats sheet
(`/server`) from `usage` frames
(prefers `generationTokensPerSecond` when the stream measured TTFT;
Expand Down Expand Up @@ -445,6 +447,8 @@ Click a tool header or reasoning block to inspect it; while inspecting, `↑`/
appears first, even while a tool is running; the result follows when it
arrives. Long invocation lines wrap by display cells. Details display at
most eight rows plus a paging indicator, with fewer rows in short terminals.
The pager names the visible section, and a compact inspection line keeps the
selected tool identifiable when a long sub-agent chip strip scrolls above it.
Use `PgUp`/`PgDn` to page, `alt+i` to copy the displayed invocation,
`alt+y` to copy the retained response, and Escape to return to typing.
The global `^E` details toggle uses the same page limits. Control and
Expand Down Expand Up @@ -584,9 +588,9 @@ can encode them:

| Key | Action |
|-----|--------|
| `a` / `Alt+A` | Approve (plain key: empty composer only); in friction mode, first open the confirmation editor |
| `a` / `Alt+A` | Allow once (plain key: empty composer only); in friction mode, first open the confirmation editor |
| `d` / `Alt+D` | Deny (plain key: empty composer only) |
| `t` / `Alt+T` | Trust the class, only when the server offers it and friction is off (plain key: empty composer only) |
| `t` / `Alt+T` | Trust this risk class until the current connection ends, only when the server offers it and friction is off (plain key: empty composer only) |
| `Tab` | Expand/collapse command and description details |
| `Alt+PgUp` / `Alt+PgDn` | Page expanded approval details |
| `Esc` | Return from confirmation editing, or fold details; otherwise arm turn cancellation when busy |
Expand All @@ -607,6 +611,12 @@ focus to the latest transcript message (a still-queued successor keeps
scrollback), so a stale form can never collect an approval for a prompt
the engine already abandoned.

The card leads with the exact command (or operation resource) or an explicit
note if odek did not supply one. Expanded details visibly escape control and invisible characters,
show the engine's action class and reason, and say when odek did not supply a
working directory. Trust's duration is tied to the current WebSocket
connection; reconnecting resets it.

---

## Configuration
Expand Down
23 changes: 21 additions & 2 deletions internal/tui/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tui

import (
"fmt"
"strings"
"time"

tea "github.com/charmbracelet/bubbletea"
Expand All @@ -20,15 +21,33 @@ type approvalOption struct {
// class-trust shortcut (mirrors the TTY approver policy).
func (m *Model) approvalOptions() []approvalOption {
opts := []approvalOption{
{"approve", "approve"},
{"allow once", "approve"},
{"deny", "deny"},
}
if a := m.curApproval(); a != nil && a.AllowTrust && !a.Friction {
opts = append(opts, approvalOption{"always allow", "trust"})
opts = append(opts, approvalOption{"allow class until disconnect", "trust"})
}
return opts
}

func approvalRiskLabel(risk string) string {
switch risk {
case "shell_exec":
return "shell commands"
case "network_egress":
return "network access"
case "local_write":
return "local writes"
case "low", "medium", "high":
return risk + "-risk tool action"
case "":
return "this tool action"
default:
label := visibleInvocation(strings.ReplaceAll(risk, "_", " "))
return strings.ReplaceAll(label, "\n", `\n`)
}
}

// handleApprovalKey keeps an approval card from hijacking the composer. Only
// explicit Alt chords decide; ordinary text, paste, cursor movement, and
// Enter continue to operate on the draft underneath the card.
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/approval_plainkeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func TestApprovalFooterShowsPlainHints(t *testing.T) {
busyTurn(m)
m.handleEvent(client.Event{Type: "approval_request", ID: "apr", AllowTrust: true})
foot := plain(m.footer())
for _, want := range []string{"a approve", "d deny", "t trust"} {
for _, want := range []string{"a allow once", "d deny", "t trust class"} {
if !strings.Contains(foot, want) {
t.Errorf("approval footer missing %q: %q", want, foot)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/calm_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func TestTurnHeadElapsedCounter(t *testing.T) {
if strings.Contains(head, "⚡") {
t.Errorf("finalized head must not carry sealed telemetry:\n%s", head)
}
if !strings.Contains(lines[len(lines)-1], "⚡") {
t.Errorf("finalized turn missing telemetry foot:\n%s", plainOut)
if !strings.Contains(lines[len(lines)-1], "3.0s") {
t.Errorf("finalized turn missing concise elapsed foot:\n%s", plainOut)
}
}
4 changes: 2 additions & 2 deletions internal/tui/cost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestTurnFooterShowsCost(t *testing.T) {
SessionContextTokens: 10_000, SessionOutputTokens: 2_000,
})
out := plain(drive.View())
if !strings.Contains(out, "$ $0.016") {
if !strings.Contains(out, "3.0s · 1 tool · $0.016") {
t.Errorf("footer missing turn cost in:\n%s", out)
}
}
Expand All @@ -112,7 +112,7 @@ func TestTurnFooterUsesModelOverride(t *testing.T) {
})
out := plain(drive.View())
// 10k input @ $2/M + 2k output @ $3/M = $0.02 + $0.006.
if !strings.Contains(out, "$ $0.026") {
if !strings.Contains(out, "3.0s · 1 tool · $0.026") {
t.Errorf("footer missing override cost in:\n%s", out)
}
}
Expand Down
Loading
Loading