Files
correx/docs/plans/2026-05-17-tui-refactor.progress.md
T
kami b95135eb3b refactor(tui): event-loop foundation — actions, reducers, effects, Panel
Tasks 1.1–3.3 of docs/plans/2026-05-17-tui-refactor.md:

- Mosaic 0.13 → 0.18; Kotlin 2.0.21 → 2.2.10 (required by Mosaic).
  Fix MaxLineLength regressions in core/kernel orchestration logs.
- Input: drop raw System.in reader and ANSI escape parser. Wire
  Modifier.onKeyEvent at root; MosaicKeyMapper → KeyResolver → Action.
- State: split TuiState into Connection/Sessions/Input/Approval/Provider
  slices. Add InputMode.SteeringNote; delete handleSteer/System.console.
- Reducers: pure RootReducer composes slice reducers returning
  (state, List<Effect>). Effect is sealed (SendWs, Quit). Single
  dispatch(action) point in TuiApp; one coroutine drains effects.
- TuiWsClient now exposes messages/connection SharedFlows; suspend
  callbacks gone. ConnectionEvent sealed type added.
- Rendering: Panel(title){…} composable with LocalTerminalSize
  composition local; drop BOX_WIDTH and hand-padding from every
  component. Poll stty size every 500ms.
- Tests: KeyResolver (18) + 5 reducer suites (37). 55 total in :apps:tui.

Pending tasks 4.1–5.2 tracked in
docs/plans/2026-05-17-tui-refactor.progress.md.
2026-05-17 03:21:26 +04:00

116 lines
7.9 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
```
## 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).