Files
correx/docs/plans/2026-05-17-tui-refactor.progress.md
T
kami 0c1876a549 feat(tui): session filter + reconnect countdown
Tasks 4.2–4.3 of docs/plans/2026-05-17-tui-refactor.md:

- SessionList filters by workflowId (case-insensitive) using
  state.sessions.filter, populated by SubmitInput while in
  InputMode.Filter and cleared on CancelInput.
- TuiWsClient tracks reconnect attempt count and emits
  ConnectionEvent.RetryScheduled(attempt, nextRetryAtMs) before each
  backoff delay. Attempt resets on successful connect.
- StatusBar renders "reconnecting (attempt N, retry in Xs)" with
  recompose-driven countdown.

Also captures a renderer-evaluation note in the progress doc: Mosaic's
ceiling vs Textual, candidate JVM alternatives (tamboui, Kotter,
Lanterna, Mordant), and confirmation that the reducer/Effect core is
renderer-agnostic if a swap is ever pursued.

Pending: 4.1 (scroll log, deferred), 5.1 (coverage gate), 5.2 (cleanup).
2026-05-17 03:21:26 +04:00

132 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TUI Refactor — Progress Log
**Plan:** `docs/plans/2026-05-17-tui-refactor.md`
**Last update:** 2026-05-17
**Mode:** running with `/implement` (spec + quality review skipped per user instruction)
**Base SHA at start:** `58670a6` (no commits made yet — all changes still in working tree)
---
## Completed
### Task 1.1 — Upgrade Mosaic to 0.18.0 ✅
- `apps/tui/build.gradle`: mosaic-runtime 0.13.0 → 0.18.0
- **Scope creep:** Mosaic 0.18 requires Kotlin 2.2.10, so root `build.gradle` was bumped 2.0.21 → 2.2.10 globally.
- Side-effect: 9 new `MaxLineLength` detekt failures in `core/kernel/orchestration/{SessionOrchestrator,DefaultSessionOrchestrator}.kt` — fixed by wrapping long `System.err.println` calls. Added `LongMethod` to existing `@SuppressWarnings` on `SessionOrchestrator`.
- `./gradlew check` green.
### Task 1.2 — Action sealed type + KeyResolver ✅
- Created `apps/tui/.../input/Action.kt` (14 variants), `input/KeyResolver.kt`, `test/.../input/KeyResolverTest.kt` (18 tests).
- Added `Filter` to `KeyEvent.kt`.
- Added JUnit 5 + kotlin-test deps + `useJUnitPlatform()` to `apps/tui/build.gradle`.
### Task 1.3 — Replace stdin reader with `Modifier.onKeyEvent` ✅
- Deleted `keyChannel`, `readKeys`, `charToKeyEvent`, `handleKeys`, `handleInputModeKey`.
- Created `input/MosaicKeyMapper.kt` mapping `com.jakewharton.mosaic.layout.KeyEvent { key, alt, ctrl, shift }` → domain `KeyEvent`.
- `Column` now has `Modifier.onKeyEvent { ... }` at root; dispatch path = MosaicKeyMapper → KeyResolver → `applyAction`.
- `applyAction(action, state, wsClient, scope): TuiState` is pure on state; ws sends launched via `scope.launch` from within (transitional — Task 2.3 replaces it).
- `TuiNavigation.kt` shrunk: kept `navigateUp`/`navigateDown`, dropped `parseEscapeSequence` + `handleCancel`.
### Task 1.4 — Steering note inside InputMode ✅
- `InputMode` final shape: `None, WorkflowId, SteeringNote, Filter`.
- `Action.OpenSteeringPrompt` → switches to `SteeringNote` only if `state.approval.active != null`.
- `Action.SubmitInput` with mode=SteeringNote → sends `ApprovalResponse(decision=STEER, steeringNote=text)`, clears approval.
- `InputBar` labels updated. `handleSteer` deleted. No `System.console` anywhere in `apps/tui`.
### Task 2.1 — Split TuiState into slices ✅
- New: `state/{ConnectionState,SessionsState,InputState,ApprovalState,ProviderState}.kt`.
- `TuiState` is now composition of the five slices with defaults.
- Field mapping applied across `TuiApp.kt`, `TuiNavigation.kt`, `StateReducer.kt`, `components/StatusBar.kt`:
- `state.connected/reconnecting``state.connection.*`
- `state.sessions` (list) → `state.sessions.sessions`; `state.selectedSessionId``state.sessions.selectedId`
- `state.activeApproval``state.approval.active`
- `state.inputText/inputMode``state.input.text/mode`
- `state.providerId/providerStatus``state.provider.id/status`
### Task 2.2 — Pure RootReducer + Effect ✅
- New: `reducer/{Effect,InputReducer,SessionsReducer,ApprovalReducer,ConnectionReducer,ProviderReducer,ServerMessageReducer,RootReducer}.kt`.
- `Action` extended: `ServerEventReceived(ServerMessage)`, `Connected`, `Disconnected`, `RetryScheduled(attempt, nextRetryAtMs)`.
- All slice reducers return `Pair<SliceState, List<Effect>>`. Default arm = `state to emptyList()`.
- 39 new tests added (55 total in module).
- **NOT YET WIRED** into `TuiApp.kt` — that is Task 2.3. The old `applyAction`/`applyServerMessage` paths still run.
- Decisions deviating from plan: dropped unused `selectedSessionId` param on `InputReducer.reduce`; dropped unused `clock` param on `ConnectionReducer.reduce`. Old `StateReducer.kt` left in place alongside new `ServerMessageReducer.kt` (no callers of `ServerMessageReducer` yet).
---
## Pending — resume here
### Task 2.3 — EffectDispatcher + wire RootReducer into TuiApp 🚧 NEXT
- Create `reducer/EffectDispatcher.kt`.
- Rewrite `ws/TuiWsClient.kt`: expose `messages: SharedFlow<ServerMessage>` and `connection: SharedFlow<ConnectionEvent>`; remove suspend-callback constructor params. Add `sealed interface ConnectionEvent { Connected, Disconnected, RetryScheduled(attempt, nextRetryAtMs) }`.
- In `TuiApp`: single `dispatch(action)``RootReducer.reduce``Snapshot.withMutableSnapshot { state = next }` → trySend each `Effect` to a `Channel<Effect>`.
- Launch coroutines: `ws.connect()`, collect `ws.messages``dispatch(ServerEventReceived(it))`, collect `ws.connection``dispatch(it.toAction())`, drain effects channel via `EffectDispatcher`.
- Delete old `applyAction`/`applyServerMessage` (and `StateReducer.kt` if unused after the cut).
- Smoke-test: `n` → workflow id prompt → submit → session starts; approval flow; steering note.
### Task 3.1 — `Panel` composable + `TerminalSize`
- Create `components/Panel.kt` (`@Composable fun Panel(title: String?, content: @Composable () -> Unit)`).
- Create `components/TerminalSize.kt` (data class + `staticCompositionLocalOf { TerminalSize(80, 24) }`).
- Do not wire yet.
### Task 3.2 — Migrate components to `Panel`
- Wrap each section in `TuiApp` with `Panel(title=…) { … }`.
- Drop every `BOX_WIDTH` constant and hand-padding from `StatusBar`, `SessionList`, `ActiveSession`, `ApprovalPanel`, `InputBar`.
- `grep -r "BOX_WIDTH" apps/tui` must return nothing.
### Task 3.3 — Responsive terminal width
- Add `readTerminalSize()` (stty fallback, default 80×24) in `TerminalSize.kt`.
- 500ms polling loop in `TuiApp.runTuiApp`; wrap content in `CompositionLocalProvider(LocalTerminalSize provides …)`.
### Task 4.1 — Scrollable session output log
- Extend `SessionsState`: `eventLog: Map<SessionId, ArrayDeque<LogEntry>>`, `scrollOffsetById: Map<SessionId, Int>`.
- New `LogEntry(timestampMs, kind, text)`.
- `SessionsReducer`: on `ServerEventReceived` for InferenceCompleted/ToolCompleted/StageStarted/StageCompleted/StageFailed, append entry (cap 200).
- Rename `ActiveSession.kt``SessionOutputLog.kt`; render last N entries with scroll offset.
- Add `Action.ScrollLogUp/Down`; introduce `FocusState { SessionList, OutputLog }`, Tab toggles. Map `NavUp/NavDown` to scroll when focus is on log.
### Task 4.2 — Session filter via `InputMode.Filter`
- `/` enters Filter mode (already wired in resolver). On `SubmitInput` mode=Filter, copy text into `sessions.filter`. On `CancelInput` mode=Filter, clear filter.
- `SessionList` renders only sessions whose `workflowId` contains `filter` (case-insensitive).
### Task 4.3 — Reconnect feedback
- In `TuiWsClient.connect`, before backoff `delay`, emit `ConnectionEvent.RetryScheduled(attempt = ++count, nextRetryAtMs = clock() + delayMs)`.
- `ConnectionReducer.RetryScheduled` already handles state; `Connected` resets attempt to 0.
- `StatusBar` formats `nextRetryAtMs - clock()` as countdown.
### Task 5.1 — Coverage gate
- Add kover verify rule to `apps/tui/build.gradle`: minBound 70 on `com.correx.apps.tui.reducer.*` + `com.correx.apps.tui.input.*`. Confirm syntax against current kover version.
### Task 5.2 — Final cleanup pass
- Audit every `@Suppress` under `apps/tui/src/main`; either fix or document.
- Update `docs/refactor.md` 2026-05-17 entries to `[done]`.
---
## Sequencing
```
[done] 1.1 → 1.2 → 1.3 → 1.4 → 2.1 → 2.2
[next] 2.3 → 3.1 → 3.2 → 3.3 → {4.1, 4.2, 4.3} → 5.1 → 5.2
```
## Renderer evaluation — Mosaic is the floor
Mosaic (jake-wharton/mosaic 0.18) is intentionally minimalist: re-emits plain text lines per frame, no screen-cell buffer, no widget catalog, no built-in borders/modals/scroll regions/mouse, no focus management. We hand-draw `─├┤` in `components/Panel.kt`. That is the entire framing system.
This is fine for a reducer/effect prototype but it caps how polished the TUI can ever look. Compared to Python's Textual we lack: cell-buffer rendering, layout engine, widget library (DataTable / Tree / Input / Modal), focus + mouse, scroll regions, transitions.
**Candidate replacements to evaluate (renderer swap; reducer/Action/Effect core stays):**
- **tamboui** — user-suggested: https://github.com/tamboui/tamboui (not investigated yet — no web access this session).
- **Kotter** — Compose-style DSL on JVM, more capable than Mosaic (real text-attribute model, animations, structured input). Closest semantic match to current code.
- **Lanterna** — Java TUI lib with real `TerminalScreen` buffer, GUI widgets (`Panel`, `Window`, `Table`, `TextBox`), borders, layouts, mouse. Imperative/Swing-style; bigger rewrite of `TuiApp` + `components/` but reducers untouched.
- **Mordant** — already transitive; only buys nicer ANSI paint, same ceiling as Mosaic.
**Migration cost:** ~all of `apps/tui/src/main/.../TuiApp.kt` and `components/*`. Unchanged: `input/`, `reducer/`, `state/`, `ws/` — the renderer-agnostic core. That's the architectural payoff of Task 2.2/2.3.
**Decision:** deferred. Capture as standalone spec before swapping.
## Notes for the next session
- Working tree currently has no commits since `58670a6`; everything is staged-or-modified. Consider committing per task on resume.
- Kotlin is now 2.2.10 repo-wide; expect occasional `experimental context receivers` warnings (`-Xcontext-receivers`) — future Kotlin will error on this.
- Mosaic 0.18 `KeyEvent` lives at `com.jakewharton.mosaic.layout.KeyEvent` with fields `key: String, alt, ctrl, shift`. `onKeyEvent` is `com.jakewharton.mosaic.layout.onKeyEvent`.
- All `EventPayload` subclasses must be registered in `core/events/.../serialization/Serialization.kt` — relevant for Task 4.1 if any new event types are added (none currently planned).