refactor(tui): migrate renderer from Mosaic to tamboui

Replaces the Mosaic 0.18 string-frame renderer with tamboui 0.2.1-SNAPSHOT
to unlock a cell buffer, widget catalog, and real layout engine. Domain
core (state/reducer/ws/input) is unchanged; only the renderer and the key
mapper are rewritten. Fixes a latent Mosaic-mapper bug where q/n/c/a/r/s//
were swallowed inside input modes by making the new mapper mode-aware.
This commit is contained in:
2026-05-17 13:40:51 +04:00
parent 7d46f46f01
commit b267982005
15 changed files with 1123 additions and 281 deletions
+18 -19
View File
@@ -51,11 +51,7 @@
- **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
### Task 2.3 — EffectDispatcher + wire RootReducer into TuiApp ✅
- 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>`.
@@ -63,20 +59,32 @@
- 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`
### 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`
### 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
### 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.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.
---
## Pending — resume here
### Task 4.1 — Scrollable session output log
- Extend `SessionsState`: `eventLog: Map<SessionId, ArrayDeque<LogEntry>>`, `scrollOffsetById: Map<SessionId, Int>`.
- New `LogEntry(timestampMs, kind, text)`.
@@ -84,15 +92,6 @@
- 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.
@@ -104,8 +103,8 @@
## 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
[done] 1.1 → 1.2 → 1.3 → 1.4 → 2.1 → 2.2 → 2.3 → 3.1 → 3.2 → 3.3 → {4.2, 4.3}
[next] 4.1 → 5.1 → 5.2
```
## Renderer evaluation — Mosaic is the floor