# 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>`. 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` and `connection: SharedFlow`; 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`. - 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>`, `scrollOffsetById: Map`. - 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).