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.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user