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

7.9 KiB
Raw Blame History

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/reconnectingstate.connection.*
    • state.sessions (list) → state.sessions.sessions; state.selectedSessionIdstate.sessions.selectedId
    • state.activeApprovalstate.approval.active
    • state.inputText/inputModestate.input.text/mode
    • state.providerId/providerStatusstate.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.reduceSnapshot.withMutableSnapshot { state = next } → trySend each Effect to a Channel<Effect>.
  • Launch coroutines: ws.connect(), collect ws.messagesdispatch(ServerEventReceived(it)), collect ws.connectiondispatch(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.ktSessionOutputLog.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).