Commit Graph

46 Commits

Author SHA1 Message Date
kami d6155691c8 feat(tui): token metrics aggregation, plan overlay, tool reasoning, clar dismissal
#295 — include narration_llm tokens in changes-panel aggregate count
#296 — execution plan overlay (Ctrl+P in-session)
#298 — surface reasoning/CoT on tool-call turns in output view
#265 — dismiss clarification modal when answered externally (session.resumed)
2026-07-21 11:52:00 +04:00
kami 15248cae8a feat(recovery,context): tier-2 intent-holder arbiter + remaining-delta pinning + surfaced signal events
- recovery: two-tier repair ladder — owner then arbiter (recovery stage recast
  as intent-holder, holds initial intent, reconciles cross-file contract disputes)
  with independent INTENT_ROUTE_BUDGET; RecoveryRoutingTest coverage
- context: remaining-delta checklist pinning (RemainingDelta{Pinning,Entry}Test)
- surface write-only events (session name, quality signals) into stats/browse
- parseToolArguments lenient-Json fallback for bare-key drift
- tools: ChildProcess seam
2026-07-11 23:56:52 +04:00
kami 3d5e05c1fb feat(context): reasoning-thread continuity, L3 doc filtering, and gate hardening
Threads reasoning_content across inference calls and journal renders, filters
markdown noise out of L3 repo-knowledge retrieval, adds PlanLinter H3 checks,
and tightens filesystem tool output/dir-listing behavior surfaced by prior
live QA (see project_readloop_campaign memory).
2026-07-10 11:13:43 +04:00
kami f51a8dada4 feat(recovery): route failed write-less stages to recovery instead of futile retry
Adds the failure-ticket + recovery-routing mechanism: a deterministic
gate->capability table gates whether a stage has agency to fix its own
failure, opens a FailureTicketOpenedEvent when it doesn't, and routes to
a metadata role=recovery stage (per-stage budget, cap 2, not reset by
TransitionExecuted) instead of retrying in place. Extends salvage
decisions with a RECOVER option so the review-gate judge can also route
to recovery, unifies deterministic-gate and review-gate routing through
routeToRecovery(), and has ExecutionPlanCompiler synthesize a
write-capable recovery stage + edge for freestyle plans. Read-only tools
(file_read, list_dir) are now always available on any tool-granting
stage so a recovery stage can inspect the write-less stage's failure
without flooding context via shell ls -R.
2026-07-08 10:28:53 +04:00
kami 41ed6414c6 feat(guardrails): steering channel + shell-in-file rule + capability-gap detector
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).
2026-07-07 13:27:59 +04:00
kami 527490e5df fix(tui): surface late clarifications and stop bg bleed on input + action rows
- 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.
2026-07-03 01:00:55 +04:00
kami 5deb2f6425 fix(tui): fill the frame background so the terminal backdrop can't bleed through
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.
2026-07-02 13:26:42 +04:00
kami ccfa4b4740 feat(tui): composer soft-wrap, alt+backspace, bracketed paste, borderless input
- 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
2026-06-28 20:42:50 +04:00
kami 85af2c67b7 tui: real terminal cursor in composer + window title (v2)
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>
2026-06-22 16:28:22 +00:00
kami d247b19608 tui: migrate Bubble Tea v1 → v2 (charm.land), enable Shift+Enter
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>
2026-06-22 15:59:55 +00:00
kami 6c792b83e2 feat(tui-go): in-session right panel cycles events / changes / off (d)
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>
2026-06-22 14:47:23 +00:00
kami cde0c33031 feat(tui-go): multi-line input — Ctrl+J / Alt+Enter newline, growing box (Phase 2)
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>
2026-06-22 12:03:06 +00:00
kami f2be46a743 refactor(tui-go): move session dates to the R overlay; drop dead idle panels
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>
2026-06-22 11:57:41 +00:00
kami 9d00612742 feat(tui-go): idle launcher — centered input + status/keys rail (Phase 1)
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>
2026-06-22 11:33:19 +00:00
kami 81280c5bd5 feat(tui-go): welcome "recent" list uses the local date, matching the session list
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>
2026-06-22 10:28:28 +00:00
kami 4e4d7c186a fix(tui-go): make the write/edit output-panel number meaningful
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>
2026-06-22 10:09:56 +00:00
kami 454f24fc3a feat(tui-go): show last-activity date in the starting-view session list
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>
2026-06-22 10:04:41 +00:00
kami 85121cec9d fix(tui-go): event-stream badge tracks session status, not just the socket
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>
2026-06-22 09:52:37 +00:00
kami 531910d38a feat(tui-go): transcript free-scroll + help overlay + event filter
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>
2026-06-22 05:41:38 +00:00
kami 6a06f2ead5 feat(tui-go): inline action rows in the OUTPUT transcript
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>
2026-06-21 23:00:18 +00:00
kami 54f94a549f feat(tui-go): command-palette keybinds + configurable status bar
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>
2026-06-21 10:50:28 +00:00
kami 04d7e26482 feat(tui-go): @ file-reference picker
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>
2026-06-21 10:24:57 +00:00
kami ba7ac06c16 feat(tui-go): transcript message-jump + OSC52 copy
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>
2026-06-21 09:59:29 +00:00
kami 26621567ac feat(tui-go): transparent modal backdrops
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>
2026-06-21 09:40:52 +00:00
kami 85bfddf1c4 feat(tui-go): slash command menu in the chat input
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>
2026-06-21 09:35:34 +00:00
kami 1f7a5d96a7 feat(tui-go): session list / resume view (E)
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>
2026-06-20 16:22:27 +00:00
kami da72ee5b80 feat(tui-go): render markdown in chat turns (E)
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>
2026-06-20 16:12:16 +00:00
kami b563d9841d feat(tui-go): workflow-propose choice panel
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.
2026-06-14 13:29:34 +04:00
kami 218a98034e feat(tui-go): interactive clarification question 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.
2026-06-14 13:00:59 +04:00
kami b4ca17c85f fix(tui): responsive layout for slim terminals
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.
2026-06-13 11:01:16 +04:00
kami 8b896b519a feat(tui): truthful-state §2 — last-event clock + unknown-event raw fallback
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.
2026-06-13 10:46:06 +04:00
kami 4f6bfa85f7 feat(server,tui): session stats pane — metrics over the WS bus + Bubble Tea overlay
Surfaces the observability metrics (observability-spec §3-tier-2) in the Go TUI:
a session-summary pane showing duration, token throughput per provider, tool
time, approval latency per tier, failure counts, and the session wall-time
accounting split. Press `S` (or palette → "session stats").

Mirrors the artifacts/config fetch pattern exactly — a WS request/response, not a
new transport. Server-side reuses the already-tested MetricsInspectionService
(one MetricsReport definition, two wires: REST `correx stats` + WS).

Server:
- ClientMessage.GetSessionStats + ServerMessage.SessionStats(@SerialName
  session.stats), reusing MetricsReport as the nested payload.
- StreamQueries.sessionStats replays via MetricsInspectionService (pure read,
  replay-neutral); routed in GlobalStreamHandler.
- ServerMessageSerializationTest golden pins the session.stats wire format.

TUI (Go/Bubble Tea):
- protocol: TypeSessionStats + StatsDto/nested structs + GetSessionStats encoder
  + golden decode test (cross-language contract).
- OverlayStats pane (statsModal), `S` key + palette entry + footer hint,
  loading/cache-by-session state, bounded breakdown rows.
- demo `stats` preview case for serverless visual verification.
2026-06-13 10:39:52 +04:00
kami 8c9ca9db00 fix(tui): F-006 flatten multi-line paste in input render
A multi-line paste put raw newlines into the single-line input slot, overflowing
the fixed-height box and leaving a smeared/stale region. flattenForDisplay collapses
newlines (↵) and tabs for rendering only; the inputBuffer keeps real characters for
submission.
2026-06-10 11:13:45 +04:00
kami ee24b7786a fix(kernel,server,tui,inference): generic resource gauge + F-002/F-003/F-004
- generic SystemResourceProbe: owner-agnostic /proc/meminfo system RAM
  overlaid on the vendor GPU probe, wired on both managed and static paths
  so the VRAM/GPU/RAM gauge renders with an external llama-server (was dormant)
- F-002: classify provider 4xx (e.g. llama.cpp 400) as terminal, not retryable
  (except 408/429); stops retrying deterministic request failures to exhaustion
- F-003: FileSystemPromptLoader expands leading ~ / ~/ to user home
- F-004: map InferenceFailedEvent + RetryAttemptedEvent to new
  ServerMessage.InferenceFailed / RetryAttempted, decoded+rendered in tui-go;
  ArtifactContentStoredEvent explicitly mapped to null (internal, no warn)
- tests: tilde expansion, SystemResourceProbe (static/managed/fail-soft)
2026-06-10 11:07:08 +04:00
kami 638f57709d feat(tui): workflow-start intent input box wired to StartSession.input 2026-06-04 02:24:41 +04:00
kami 2bef4b96a5 feat(tui): Task 3.5 — decode + render router.narration as bright narration_llm lines
Add TypeRouterNarration constant, mark it event-bearing, decode golden test,
applyServer case appending RouterEntry{Role:"narration_llm"} with metrics, and
routerRows render branch showing bright ◆ prefix with FgStrong content + metrics suffix.
2026-06-03 19:09:28 +04:00
kami 80a72d370e feat(tui): show router inference metrics beside router lines 2026-06-03 15:41:51 +04:00
kami e9a87febc4 feat(tui): render workflow progress as dim router-feed narration lines 2026-06-03 15:07:31 +04:00
kami 46c567c835 feat(tui): render stage lifecycle frames as dim lines in the router feed
Stage started/completed/failed events are now injected into the router
transcript (role "stage") and rendered in Faint colour, giving the
operator a chronological in-feed view of workflow progress alongside
user/router/tool turns.
2026-06-03 14:54:21 +04:00
kami 5721ed21bb feat(tui): show session workspace (cwd) in the status bar
SessionWorkspaceBoundEvent already recorded the bound workspace root but
it was never surfaced. Map it to a new session.workspace_bound frame
(sealed ServerMessage variant, auto-registered; re-emitted on reconnect
via the snapshot replay), carry it on Session.WorkspaceRoot, and show it
home-abbreviated in the status bar (⌂ ~/Programs/correx). Golden test
pins the decode.
2026-06-03 01:56:46 +04:00
kami f7fc10ddf5 feat(tui): full event history, approval-decision events, status bar
- Events were hard-capped to the last 7 per session in addEvent, so the
  e-inspector and EVENTS panel could never show more. Raise to 1000
  (effectively all, bounded); the EVENTS panel now shows the latest that
  fit (tail), and the inspector scrolls a window around the selection
  with a position indicator.
- The TUI had no approval.resolved handling at all — the decision frame
  was ignored and pending only cleared on session.resumed. Add the
  constant + case: clear the gate and record an ApprovalResolved event
  (APPROVED/REJECTED/AUTO_APPROVED + reason) into the stream.
- Status bar now shows the current stage and session status alongside the
  name. Relabel the cryptic "N bg" badge to "N elsewhere".
2026-06-03 01:24:52 +04:00
kami b56f0e88ca fix(tui): approval nav lockups, shell non-diff preview, hint dedup
Three QA-found issues in the approval gate:

- displayState gated the in-session/approval surfaces on hasApproval
  before sessionEntered, so moving the list cursor onto a session with a
  pending gate auto-opened it, and `l` back-to-list couldn't escape
  (the gate re-popped). Require sessionEntered first.
- The band ran every preview through the two-column diff renderer, so a
  shell tool's argv JSON rendered as an identical-column "diff". Detect
  real unified diffs (isUnifiedDiff); render other previews as plain
  text, and drop the ^x fullscreen hint when there's nothing to expand.
- The footer duplicated the band's approve/reject/steer/diff keys. It now
  shows navigation (l back / e events / q quit) instead.
2026-06-03 01:16:04 +04:00
kami 03ccac76c7 feat(tui): dock approval gate + two-column diff viewer
Replace the floating approval modal (which blanked the whole UI) with an
opencode-style band that takes the input bar's slot while a gate is
pending: tool/tier/risk header above a side-by-side old|new diff, with
the session output and event stream still visible above it. The band
height adapts to the diff length.

Add a unified-diff parser that aligns removals/additions into split rows
(blank left cell for a create, blank right for a delete) and a
two-column renderer shared by the band preview and the ^x fullscreen
view. Wire plain a/r to approve/reject to match the advertised keys.
2026-06-03 00:17:08 +04:00
kami da3f6c84a3 feat(tui): QA fixes + approval Ctrl keys, workflow focus, session UUID
- gate session-list nav on idle so in-session arrows/jk don't move the list (5a)
- approval dismiss on esc + in-session 'a' reopens a pending approval (5b)
- auto-focus a newly started workflow session (was running invisibly)
- approval actions on Ctrl chords (^a/^r/^x) matching the modal; bare/Alt letters no longer approve; diff closes on ^x/esc
- steer hint in the approval modal; session UUID in the input bar
- env-gated debug logging (CORREX_TUI_LOG) with k.Alt in the key trace
2026-06-01 23:23:29 +04:00
kami 55a18b4b3a feat: correx-managed model lifecycle slice 5 — Go TUI model swap + telemetry
Go TUI decodes model.changed / model.list / resource.status and renders a
status-bar VRAM/GPU%/RAM gauge plus a models overlay (m key / palette 'models'):
lists configured models with the resident one marked, enter swaps (SwapModel),
c clears the pin (ClearModelPin). All three new server messages are
non-event-bearing control/gauge frames, applied immediately like
provider.status_changed.

Server addition required for the picker: ServerMessage.ModelList (model.list,
NonEventMessage) sent once in the initial snapshot from
ManagedInferenceRouter.availableModelIds()/currentModelId() — the TUI otherwise
cannot enumerate swap targets.

Completes the 5-slice model-lifecycle feature. ./gradlew check green; go build/vet clean.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 5 of 5).
2026-06-01 13:36:01 +04:00
kami f922b855eb feat: add Go/Bubble Tea TUI rewrite (apps/tui-go)
Reimplement the TUI in Go using Bubble Tea, replacing the Kotlin/Tamboui
app. Same WebSocket protocol, soft-rounded layout with blue accent theme.
2026-05-30 00:00:35 +04:00