Bundles three operator-reliability guardrails (Vikunja #28/#29/#30) plus the
in-flight branch WIP they were built on top of (reasoning_content capture,
operator/project profile editor, write-jail workspaceRoot fix) — the tree is
interdependent (SessionOrchestrator references reasoningArtifactId from the WIP)
and does not compile as separable subsets, so it lands as one commit.
Guardrails:
- #28 mid-stage steering: ClientMessage.SteerSession -> GlobalStreamHandler ->
orchestrator.submitSteering, reusing SteeringNoteAddedEvent + existing context
fold (advisory, non-authoritative; invariants #3/#7). Closes the gap where
steering typed off an approval gate was silently dropped.
- #29 shell-in-file guardrail: ShellInFileContentRule (core:toolintent) blocks a
file_write whose content is a bare shell command (e.g. "mkdir -p ..."); FileWriteTool
description now advertises auto-mkdir of parent dirs. Basename-allowlist so the
extensionless case is caught; scripts/Makefiles/multiline exempt.
- #30 pt1 capability-gap detector: deterministic CapabilityGapDetector maps stage
intent -> implied ToolCapability, compares to granted tools, emits advisory
CapabilityGapDetectedEvent in FreestyleDriver.lockAndRun. Recorded, never fails
the gate and never auto-grants (invariants #3/#4/#5). Reflection rung is pt2.
Verified: ./gradlew check green (whole tree).
- clarDismissed was model-global but only reset when the clarification's
session was selected on arrival, so a clarification for an unselected
session stayed silently hidden. Reset on every arrival.
- input bar (in-session + launcher) only truncated lines, never padded to
width, so the terminal backdrop bled through the right. Pad with padTo.
- action/narration/user rows prefixed plain (unstyled) spacers around their
icons, leaving transparent gaps near the symbols. Use bg-styled spacers.
Approval-card and preview JSON artifacts (the freestyle execution_plan, the analyst's analysis) rendered as a raw JSON dump. Add a JSON-aware preview: a scannable goal + numbered stage list for execution_plan, indented key/value lines for other objects, with light highlighting. Non-JSON previews fall back to the raw line view unchanged.
The main render path never applied the Screen backdrop, so any sub-view that left a line short or a row empty showed the transparent terminal background. Pad every frame to full width×height with the theme backdrop before display; content stays top-left anchored so caret coords are unaffected.
Root Child DOX Index assembled, plus a per-module AGENTS.md across the tree
(core/*, infrastructure/*, apps/*, testing/*, and docs/examples/frontend/etc),
each following the DOX section shape: Purpose, Ownership, Local Contracts,
Work Guidance, Verification, Child DOX Index.
- editorLines now soft-wraps long logical lines at the box width (caret tracked
across wrapped rows) instead of overflowing/truncating
- alt+backspace deletes the word before the cursor (deleteWordBack)
- handle tea.PasteMsg so bracketed paste (Ctrl+V / Ctrl+Shift+V / right-click)
drops clipboard text into the focused composer; was silently dropped
- render the composer (in-session + launcher) borderless: top rule + flush text,
no side borders or trailing padding, so a terminal drag-copy yields just the
typed text instead of box chrome and whitespace
file_read on a directory/file returns newline-bearing summaries; clip kept the
newlines, so the action row rendered multi-line with a background-filled stripe
and the listing leaking underneath. Flatten the summary to one line first.
Add "tasks" and "task-detail" kinds to PreviewFrame, backed by a
sampleTaskBoard work graph that exercises every readiness glyph and
status color (epic blocked on children, a ready child, in-flight, done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each task row gets a 2-col glyph — ● ready (workable now) / ○ waiting (blocked) /
blank — and the detail pane shows "ready to work" or "blocked — waiting on <ids>",
fed by the new GET /tasks ready/blockedBy fields. Makes a decomposed graph legible
at a glance; a dependency-blocked task is status TODO (not the red BLOCKED lifecycle
state), so the glyph is what distinguishes them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ranked exact/substring search: TaskSearch (all terms AND, ranked title >
key > goal > criteria > notes) over one project or the whole board via
TaskService.search. Surfaced as GET /tasks?q=, a read-only task_search tool
for agents, and a `/` filter in the TUI board.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a cross-project task board to the TUI (T / palette): a roster fetched
from GET /tasks with vim-style nav, refresh, and an enter-to-open detail
pane (goal, criteria, paths, links, notes) built from the list payload — no
extra fetch. Server: TaskService.listAll() and a project-optional GET /tasks
(scoped with ?project=, whole board without; recent-first).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two v2-unlocked polish items now that the TUI is on bubbletea v2:
Native cursor — the composer caret was a frame-animated "▏" that forced the
redraw loop to keep ticking in insert mode. v2's View.Cursor lets the terminal
draw and blink a real cursor, so:
- editorLines/launcherInput drop a width-1 marker rune (U+E123, same cell width
as "▏") at the caret; View() locates it in the fully-composited frame via
splitCursor → (X,Y), swaps it for a space, and sets a blinking CursorBar there.
- render() (preview/golden tests) resolves the marker to the static "▏" glyph, so
screenshots are unchanged and the marker never leaks into output.
- nil cursor in normal mode hides it (vim-correct); gated on overlay==None so an
open palette/files modal (which keeps editMode==Insert) doesn't draw a cursor
behind it.
- animating() no longer forces a redraw in insert mode — the terminal blinks the
cursor itself, so an idle insert screen holds still (native text selection
survives) and burns no frames.
Window title — View.WindowTitle names the terminal tab after the active session,
falling back to the model name then "correx".
Tests: new cursor_test.go (coord extraction, mode/overlay gating, no marker leak);
updated the animating-gate test for the new insert-mode contract. Build, vet,
gofmt, full suite green; compose preview shows the static caret with no marker leak.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the Go TUI from github.com/charmbracelet/{bubbletea,lipgloss} to the v2
ecosystem at charm.land/{bubbletea,lipgloss}/v2 (Go 1.25). v2's kitty keyboard
disambiguation is on by default, so Shift+Enter now reliably inserts a newline
in the composer (Ctrl+J / Alt+Enter kept as fallbacks for non-kitty terminals).
Approach: rather than rewrite ~190 key-match sites to v2's (Code, Mod) idiom, a
small shim (internal/app/key.go) converts a v2 KeyPressMsg into the v1-shaped
keyMsg the handlers already expect, at the single Update boundary. The rest is
mechanical:
- key constants tea.Key* → shim consts; tea.KeyMsg → keyMsg.
- lipgloss.Color is now a func returning color.Color, not a type → fields/params
retyped to image/color.Color (theme/diff/overlays/view).
- v2 View() returns tea.View: render() builds the string, View() wraps it and
carries AltScreen (alt-screen is a per-frame View field now, not a program opt).
- WithWhitespaceBackground/Foreground → WithWhitespaceStyle(Style).
- preview: drop lipgloss.SetColorProfile (v2 renders truecolor by default).
Build, vet, gofmt, and the full test suite are green; preview renders truecolor
across kinds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Press d (or the "panel" palette command) in-session to cycle the right panel:
- events — the live event stream (default, unchanged)
- changes — a token-usage line (tokens + turns, summed from the transcript's
TurnMetrics) over a git-status-style list of files the session has
written, each with summed +/− from its diff. ^x still opens the full
diff.
- off — hide the panel; the output transcript takes the full width.
changesRows derives everything from data already in the model (router-turn
metrics + the tool entries' unified diffs), so no new protocol. Footer + ? help
updated; "changes" preview kind added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enter still submits; Ctrl+J (always) and Alt+Enter (most terminals) insert a
newline at the cursor. True Shift+Enter isn't distinguishable from Enter in
bubbletea v1.3.10 (terminals send the same byte, no kitty-protocol parsing), so
these are the working stand-ins — noted in the input hint.
- editorLines() renders the buffer as styled rows with the caret at the cursor,
splitting on \n and capping at maxInputLines (last rows kept). Both the idle
launcher input and the in-session bottom bar use it and grow to fit; View()
sizes the bottom bar via inputBarHeight() instead of the fixed inputH.
- Footer gains a "^J newline" hint in insert mode (non-filter); the launcher
sub-line shows "⌥↵/^J newline". New "compose" preview kind + editor_lines_test
(split + height cap).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The launcher removed the idle session list, so the per-row date moves to the R
resume overlay — now the sole session list. sessionListRow shows an absolute
local date (absDateISO → shortDateTime) alongside the relative age:
"Jun 22 14:30 · 5m ago", with the year shown for older-than-this-year rows.
Delete the now-unused idle-panel renderers (sessionRows / welcomeRows /
workflowRows) and renderMainNarrow's dead idle branch (renderLauncher owns idle,
narrow included). Add a "resume" preview kind to screenshot the R overlay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the idle two-panel layout (session list + welcome, where the welcome
panel duplicated the list) with an opencode-style launcher: a compact, centered
input whose lower-right shows the launch target and model (chat by default), a
hideable right rail of status + quick keys, and no session list — it's a keypress
away via R.
- Tab (or w) cycles the launch target: chat → workflows → chat (launcherWf),
shown at the input. Enter on chat starts a chat; on a workflow, StartSession
with the typed text as the brief.
- Right rail (connection, session/active counts, i/Tab/R/?/p keys) hides via the
new "rail" palette command. Drops automatically on narrow terminals.
- View() suppresses the bottom input bar on idle (the input is the launcher);
footer + ? help updated to the launcher keys (help-coverage test extended).
The old idle-panel renderers (sessionRows/welcomeRows/workflowRows) are now
unused but kept in the tree pending Phase 2 + a decision on where the session
dates should live now that the idle list is gone. Multi-line input (Ctrl+J /
Alt+Enter newline) is Phase 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Was the UTC-then-local time-of-day; now the same "Jan 02 15:04" / "Jan 02 2006"
shortDateTime as the session list, so a previous-day session isn't shown as if
it were today.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
formatTime forced .UTC(), so every event-log timestamp and the welcome "recent"
times read hours off the operator's wall clock — and inconsistent with the local
date just added to the session list. time.UnixMilli is already local, so the
explicit .UTC() was the bug; drop it. No test asserts a clock string (the render
matrix only checks event type/detail), so this is display-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The collapsed tool row showed "tool output (N chars)" where N was len() of the
raw unified diff — i.e. the diff blob's *byte* length (headers, @@, +/-/context
all counted), labelled "chars". That number corresponded to nothing the operator
cares about, and len() is bytes not characters.
- Summarise a write/edit by the diff's row count instead — "· diff (7 rows) —
^x to view" — matching exactly what the ^x modal reports. Non-diff output (the
defensive fallback) now counts runes, not bytes, so "N chars" is truthful.
Retire itoaLen (the byte-count-as-"len" footgun).
- Harden the (+a −b) line counts in diffSummary: guard the file header with the
trailing space ("+++ "/"--- "), so a changed line whose content starts with
"++"/"--" is counted instead of mistaken for a header. Apply the same guard in
parseUnifiedDiff so such a line is rendered (and counted) rather than dropped.
tool_summary_test.go covers the diff-row summary, the rune (not byte) count, and
the ++/-- content-line edge case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited handleNormalKey / handleApprovalKey / the OverlayDiff handler against the
help cheat-sheet and found gaps: enter, ↑↓/jk, /, s, c, a, w, q and ? itself were
bound but undocumented. Reorganise helpBody into navigate / session / overlays /
approval band / diff-viewer groups covering all of them. help_complete_test.go
asserts a row exists for every binding so the help can't silently drift from the
handlers. The longer sheet scrolls fine via the modal-scroll support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The idle "sessions" panel listed id/status/name/stage but no time at all. Add a
right-aligned last-activity timestamp (Session.LastEventAt) per row — "Jan 02
15:04" within the current year, "Jan 02 2006" for older — so old sessions are
distinguishable at a glance. The name is truncated first so the box's width
clamp never eats the date column. (The R resume overlay already shows a relative
age; this is the live list on the starting screen.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The EVENTS panel showed "● live" off m.connected alone, so a completed/failed/
paused session kept reading "live" as long as the websocket was up — stale, since
a terminal session emits no more events. Derive the badge from the selected
session's Status (the same vocabulary the status bar uses): ✓ done, ✗ failed,
‖ paused, ● live while active, ○ idle when disconnected. event_badge_test.go
covers all five.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ? help and S stats modals rendered at fixed height; compositeOverlay
drops any rows past the screen, so on a 24-30 row terminal their bottom
sections — including the close hint — vanished with no way to reach them
(and the backdrop border corrupted). Recent help additions worsened it.
- Add a shared scrollable-modal renderer: helpBody()/statsBody() produce
discrete lines, renderScrollModal() windows them by m.modalScroll (clamped
to the viewport via modalBodyRows/scrollableModalMax) and pins the hint, so
the box never exceeds the screen. A "(off-end/total)" indicator shows in the
title when scrolled. Keys: ↑↓/jk line, PgUp/PgDn page, ^u/^d half, g/G ends;
any other key still dismisses help. modal_scroll_test.go (2): clamp + no-op.
- Page the artifacts viewer (v) too: PgUp/PgDn were one line at a time — now a
full body, with ^u/^d half and g/G ends via scrollArtifact().
Verified via cmd/preview: help/stats close hints now survive at h=20-28 (were
clipped below ~32/40) and show the scroll indicator; full at tall heights.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ^x fullscreen viewer (OverlayDiff) backs every tool preview — split diff
for write/edit, the command card for shell, plain lines otherwise, plus any
transcript tool-output — but only scrolled one line per keypress, making a
long diff tedious to read.
- Add paging to the viewer, matching the OUTPUT free-scroll contract: ↑↓/jk
by line, PgUp/PgDn by a full body, ^u/^d by half, g/G to the ends. New
scrollDiff() clamps to [0, diffMaxScroll] so paging past either edge lands
exactly (no dead presses). diff_scroll_test.go (2): clamp + no-op-when-fits.
- Update the modal hint to advertise paging (line / page / ends / close) and
add a "diff / preview viewer (^x)" section to the ? help overlay. Also note
the T3+ confirm-again behaviour in the approval-band help.
Approval ergonomics re-audit (BACKLOG §E): the spec'd band still holds —
single-key y/n/e for T0-T2, mandatory second-key confirm for T3+ (HighTier),
navigable queue with an i/n indicator, docked band never modal-stacked; all
covered green by approval_test.go. The scroll granularity above was the only
gap, now closed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backlog hygiene per the file's own maintenance rule — resolved items get
moved out, not left ticked-in-place.
- Cut resolved body bullets from BACKLOG §A/B/C/D/E/G/I (health probes,
static-first infra, CritiqueFinding, architect contradiction-check, A1/A2
gates, batch fetch-approval, source/egress events, render matrix, markdown,
session list, idea promotion, turnId/probe/narration, .cs symbols, scene
validator). All already archived in RETRO's burndown table.
- Narrow partially-done items to their live follow-up rather than delete:
B5 -> static_check stage emitter only; B6/C-A3 -> CritiqueOutcomeCorrelated
producer only; C-A1 -> planner-prompt activation only.
- Drop the stale "§E UNBLOCKED" index line (plan-comparison is BLOCKED, no
PlanCandidate event; idea-promotion shipped f107ff5).
- Archive the operator-requested TUI wave (palette, @ picker, CLAUDE.md L0
injection, inline action rows, free-scroll/help/filter) in RETRO 2026-06-22
— never BACKLOG items, would otherwise be orphaned in neither file.
- gitignore apps/tui-go/preview (cmd/preview build artifact).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three TUI-polish items.
- OUTPUT free-scroll: PgUp/PgDn (and ^u/^d half-page) scroll the transcript
back through history; esc snaps to tail-follow. outputScroll is clamped
against outputViewport(), which mirrors View/renderMain geometry, so the
edges land exactly (no dead presses unwinding an overshoot). routerRows
split into buildTranscriptRows (full render) + windowing so the handler
measures the same content. Tests cover the clamp + the fits-in-panel no-op.
- Help overlay: `?` (or the "help" palette command) opens a keybinding
cheat-sheet grouped by context (session / overlays / approval band) — the
non-palette keys especially. Any key dismisses it.
- Event-inspector filter: `/` filters the event list by a case-insensitive
substring of type/detail (currentEvents applies it); `c` clears, esc stops
editing then closes. Fresh per open. EVENTS side panel stays unfiltered.
go build / vet / test green; rendered help + filtered inspector via
cmd/preview (help, events-filter kinds).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ⏵ tool-start row doubled up with the ✓/✎ result row in OUTPUT. Drop it
inline — tool starts still show in the tool list + EVENTS panel. The inline
transcript now carries only outcomes: ✎ writes, ✓/✗ tool results, ⌘/✕
approvals, ⊞/⊟ grants.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface side-effecting ("external feedback") events in the conversation
flow, opencode-style, instead of only in the EVENTS side log.
- New RouterEntry role "action" (glyph + text) appended at arrival so it
interleaves with chat turns by order. Rendered dim with an accent gutter
glyph; the EVENTS panel still carries the full stream.
- Surfaced: tool start (⏵), tool done (✓ / ✎ wrote <path> (+a −b) for a
diff), tool failed (✗), rejected (✕ blocked), approval resolved
(⌘ approved / ✕ rejected, noting "· via grant" on a grant auto-approve),
and grant create/revoke (⊞ / ⊟ · scope). ApprovalResolved names no tool
on the wire, so the tool is recovered from the pending queue before the
gate is dropped.
- Toggle: "inline actions" palette command flips actionsHidden (rows are
always recorded, gated at render, so toggling reveals history); persisted
in tui-prefs.json. prefs.go refactored to load/mutate/save so the new
field and statusbarHidden no longer clobber each other.
- demo.go: "actions" preview kind.
go build / vet / test green; rendered the flow via cmd/preview.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the new cross-session grant scopes in the TUI.
- Approve-always (A) now opens a scope picker instead of immediately
creating a SESSION grant: choose this session / this project / everywhere.
SESSION stays the default (A then enter/s = old behaviour); p and g
create PROJECT / GLOBAL grants. The grant + approval are sent on confirm
(esc cancels, leaving the call pending).
- New "grants" palette command + G shortcut open a standing-grants viewer
(OverlayGrants) listing the active PROJECT/GLOBAL grants — scope, tool,
permitted tiers, project path — with x/enter to revoke. The server's
RevokeGrant reply (a fresh grant.list) refreshes it in place.
- protocol.go: TypeGrantList + GrantDto; RevokeGrant/ListGrants encoders;
CreateGrant now documents the wider scopes. server.go decodes grant.list
into m.grants and treats it as a non-session global reply.
- render-matrix coverage: grant.list classified as a non-rendering query
reply (populates the overlay, not the transcript).
- demo.go: grants + grant-scope preview kinds.
go build / vet / test green; rendered both overlays via cmd/preview.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Palette: each command now shows its bare-key shortcut in a key column (and the filter
matches on it), so the palette teaches the shortcuts instead of hiding them.
Status bar: a new "status bar" palette command opens a toggle overlay to show/hide the
non-essential segments (current stage, session status, workspace path, model, last-event
clock, resource gauge); correx/connection/session-name/spinner stay. Choices persist
TUI-local in tui-prefs.json (JSON in the config dir — display state, kept out of the
shared/replayed server config) and reload on launch.
Also trims the in-session footer (drops stats/model — both reachable via `p cmds`) so it
no longer overflows + truncates `q quit` at ~100 cols after the transcript-nav additions.
Tests cover toggle→render gating and prefs persistence; verified via cmd/preview renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing `@` at a word boundary in the chat input (mid-word `@` stays literal, e.g. emails)
opens a workspace file picker: it requests file.list for the session (cached per session),
filters as you type, and enter inserts `@path ` at the cursor — back to typing. Renders as
a transparent overlay, windowed around the selection. file.list is classified non-rendering
in the render-matrix guard. Tests cover token-boundary detection, ref insertion, and filtering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ctrl+↑/↓ jumps between your sent (user) messages in the transcript: ctrl+↑ from the
bottom selects the most recent, steps to older ones (clamping); ctrl+↓ steps back and
drops to tail-follow past the last. The selected message is highlighted and scrolled
into view (routerRows tracks per-message row offsets and windows to the selection).
`y` yanks the selected message — or the latest turn when none is selected — to the
system clipboard over OSC52 (go-osc52, tmux-wrapped under $TMUX), which is SSH/Termius-
safe unlike a mouse drag. A brief "✓ copied" footer note animates out (and self-stops
the tick). esc drops the selection; switching sessions clears it. Tests cover the
user-message jump/clamp/tail and the copy-text selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Modals now composite over a dimmed copy of the screen behind them instead of an opaque
scrim, so the transcript stays faintly visible around a modal (opencode-style). center()
stashes the frame's base (View sets m.lastBase), strips+dims it to a faint scrim, and
splices the modal box over it (ANSI-aware via ansi.Truncate/TruncateLeft so the side
margins keep the dimmed content). An idempotence guard passes an already-full-screen
modal through unchanged, so the existing double-center builders don't get re-dimmed.
Geometry tests assert the composite still fills the screen exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing `/` as the first char of a chat line opens the command palette inline
(opencode-style), instead of typing a literal slash; mid-line `/` stays literal.
Factors openPalette() (shared with the `p` key) and advertises `/ cmds` in the
insert-mode footer when the line is empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Input history is now a single global ring (recordInput) shared by every input surface —
free-chat, the workflow-intent line, and in-session chat — so ↑/↓ recalls anything you've
sent, including outside a session (previously per-session, so idle/intent had none).
Copy fix: the 120ms animation tick now only runs while something actually animates
(animating(): active session spinner, blinking caret, reconnect). Init no longer starts it
unconditionally; tickKick (re)starts it on demand and tickMsg stops it when idle. An idle
screen no longer auto-redraws, so a native terminal text selection survives instead of being
wiped every frame. Tests cover the ring (dedup/cap/cross-context walk) and the idle gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pending approvals are a navigable queue (PendingQueue/PendingIdx; ↑↓/jk, count
shown), no modal stacking; resolve removes the specific gate by request id
- y/n/e keys (approve/reject/steer); T0–T2 act on one key, T3+ requires arm+confirm
("press y again"), any other key disarms; reject/steer/auto never gated
- snapshot restore now rehydrates the full pending list, not just the first
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Table-driven render assertions (37 cases over 42 protocol Type* constants), driven
through applyServer and asserted ANSI-stripped on stable defining text. Coverage
guard parses every Type* from protocol.go and fails if a kind is neither covered
nor explicitly classified non-rendering. Pins width/theme/timestamp; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
R opens a navigable overlay of recent sessions (GET /sessions: short id, status,
workflow/stage, relative age); enter resumes via POST /sessions/{id}/resume, which
replays onto the existing global /stream (no WS re-dial). Fetch/resume errors surface
in the overlay, never panic. stdlib-only (ws.Client.HTTPBase derives the base URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderMarkdown wraps charmbracelet/glamour (dark style, per-width memoized) and is
hooked into the router chat-turn render path; falls back to plain content on any
glamour error so the TUI never crashes or loses text. Other roles/UI untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`go 1.26` is not a fetchable/released toolchain, so released Go cannot build the
module ("toolchain not available"). 1.24.2 is the minimum the deps require
(charmbracelet/x/ansi needs >= 1.24.2) and is a real toolchain; the directive is a
floor, so newer Go still works. Bump if a specific newer release is intended.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the health subsystem (llama/event-store/disk probes) in the TUI,
mirroring the session-stats pane: ClientMessage.GetHealthChecks ->
ServerMessage.HealthChecks (health.checks, reuses HealthReport) ->
StreamQueries.healthChecks via HealthInspectionService; Go OverlayHealth on
key H + palette 'health checks', pull-based fetch, per-subject status with
degraded loud. Empty/disabled -> visible fallback (no blank/lie). Go+Kotlin
golden tests pin the wire contract field-for-field. Observability spec §4/§3.
OverlayIdeas (opened with I, global/cross-session) lists the idea board in the
artifact-viewer shell: ↑↓ to move, x/d to remove (sends DiscardIdea, optimistic
drop), esc to close. Pull-based via ListIdeas -> idea.list.
Render a workflow.proposed frame as a single-select picker with a manual-answer
slot: ↑↓/jk to choose, enter to launch, e for custom, esc to peek away. Picking
a candidate sends StartSession (and focuses the launched session); the custom
slot continues the conversation via ChatInput. Reuses the modal helpers from the
clarification view.
Render stage clarification questions as an AskUserQuestion-style form:
header chip, prompt, selectable option chips, and a free-text custom slot.
Arrow/hjkl nav, space to select, e for custom, enter to submit. Submit
guards on all-answered and sends ClarificationResponse; the parked stage
re-runs with the answers in context.
Layer 1 of tui-requirements §5: a no-LLM analyzer that shell-lexes a
proposed command, classifies binaries against a known-command table, and
mechanically flags the constructs that widen blast radius — sudo/doas,
recursive delete, pipe-to-shell (skipping wrappers so `| sudo sh` counts),
redirects outside the workspace, network binaries, base64/eval, and
unparseable input (itself a red flag). Pure + replay-stable: same preview
+ workspaceRoot → same analysis, no environment reads.
The approval band renders command approvals as a card — worst-first flag
chips over the reconstructed command line, each token colored by severity
(T0–T4 ramp). ^x opens a scrollable fullscreen view so a long command is
never truncated. Diff and plain-preview paths are unchanged.
Server: computeToolPreview now renders the shell tool's argv as a full,
shell-quoted command line instead of the 200-char JSON-args fallback,
honouring §3's "full untruncated command". The TUI parser accepts both
the rendered line and the older `{"argv":[...]}` shape for replay.
Layer 2 (model annotation) deliberately deferred per the spec.
The TUI overflowed and became unreadable on narrow terminals: the status bar and
footer spilled far past the edge (justify never truncated), the two-column main
split shrank the events panel to ~20 cols, and the stats overlay wrapped mid-word.
- justify is now width-safe: shrinks the left segment first (the right holds the
high-signal connection clock / active spinner), clamps the right only as a last
resort, ANSI-aware so it never slices escape codes. No line can exceed the width.
- Main area collapses below narrowWidth (96): single full-width session list when
idle; output stacked over the live event strip in-session (the strip is too
valuable to drop — the constraint is width, not height).
- Compact chrome when slim: status drops workspace/gauge/provider-suffix and uses a
short clock ("◷ 3s") + bare spinner; footer uses a reduced hint set and drops the
"Soft·blue" badge; approval action row tightens its gaps so decision keys stay
visible.
- Overlays get near-full-width on slim terminals; stats pane uses compact formats +
per-line clipping so it never wraps.
Tests: no rendered line exceeds the terminal width at 40–160 cols (incl. the stats
overlay); narrow in-session main stacks output+events.
Implements the load-bearing TUI-requirements §2 hard requirements:
- Last-event clock: "last event Ns ago" always visible in the status bar while
in-session, updating every frame tick. Turns warn-colored when an active session
has gone quiet past the stale threshold — the "thinking vs hung" tell. The
WebSocket connection indicator (●/◌/○) already covers immediate drop visibility.
- Unknown-event raw fallback: applyServer now surfaces any unrecognized,
session-scoped event type as a raw row in the event stream instead of silently
dropping it (the frontend must not lie by omission). Recognized-but-unrendered
types (router.response, protocol_error) get an explicit no-op so the default
branch means genuinely unknown.
Tests: agoLabel formatting, unknown-event surfaces-as-row, known-ignored
no-spurious-row. Preview "session" frame exercises the clock.
Deferred: the full Go↔Kotlin per-event-type render matrix (§2 last bullet) — a
larger fixture effort than the core behaviors landed here.