- 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".
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.
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.
- 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
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).