Files
correx/docs/visual/tui-layout-plan.md

632 lines
27 KiB
Markdown
Raw Permalink 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 Layout Redesign — Soft Rounded + Blue Accent
This document has two halves. The **top half** describes what the user sees and experiences — the visual language, the three screens, microfeedback. The **bottom half** is the build plan — phases, files, checklists, code patterns.
---
# DESIGN SPEC
---
## 1. Visual Language
Everything shares one consistent look: a **soft rounded** dark theme with a **blue accent**.
### Surfaces
| Element | Looks like | Feels like |
|---------|------------|------------|
| Background | Deep charcoal `#14161a` | The ground — recedes |
| Panels | Slightly lighter `#181b21`, rounded borders `╭──╮` | Cards floating on the ground |
| Top bar | Darkest `#0f1115` | Anchors the screen |
| Footer bar | Same as top bar | Anchors the bottom |
| Active panel | Blue-tinted border glow | The thing you're working in |
### Light — five levels of contrast
```
#eef1f5 — bright white → headings, important values
#ced3da — light gray → body text, messages
#838b96 — muted gray → labels, metadata, timestamps
#434a55 — faint → borders, separators, placeholders
#262b33 — darkest gray → subtle panel borders
```
### Accent — blue (#4a9eff)
Used sparingly so it means something:
- **Selected item** indicator (`▶`)
- **Active panel** border highlight
- **Interactive hints** (keybind labels)
- **Brand mark** (the `x` in `correx`)
- **Inference events** in the stream
- **Prompt cursor** (`▌`)
Secondary accent is a purple-blue (`#7aa2f7`) used for lifecycle events.
---
## 2. The Three Screens
Every screen shares the same three-row grid. The middle row is a 1.7:1 horizontal split whose content changes by state. The input bar is always inside the left panel's footer.
```
┌──────────────────────────────────────────────────────┐
│ status bar │ ← always visible
├─────────────────────────────────┬─────────────────────┤
│ left panel (1.7) │ right panel (1.0) │ ← changes by state
│ │ │
│ ──────────────────── │ │
input bar (panel footer) │ │
├─────────────────────────────────┴─────────────────────┤
│ footer bar — keybind hints · mode │ ← always visible
└──────────────────────────────────────────────────────┘
```
### IDLE — choosing what to do
Left column shows the session list (+ workflow picker if no sessions). Right column is a welcome panel.
```
┌──────────────────────────────────────────────────────────────┐
│ ● connected │ localhost:8080 │ qwen_coder_14b (local) │
├──────────────────────────┬───────────────────────────────────┤
│ sessions │ Correx Agent Harness │
│ │ │
│ ▶ [01J9CA] ACTIVE │ Connected · 3 sessions │
│ cas-segment-store │ ───────────────────── │
│ [02KF8B] COMPLETED │ Select a session from the left │
│ approval-flow │ or type a name to start a new │
│ [03LD7C] FAILED │ one. │
│ refactor-router │ │
│ │ recent: │
│ ────────────────────── │ cas-segment-store 14:02 ▸ │
│ ▌ Session name… │ approval-flow 13:45 ✔ │
│ tab filter ctrl+n new │ refactor-router 12:30 ✘ │
└──────────────────────────┴───────────────────────────────────┘
│ ⌨i message ⌨p commands ⌨a approval ⌨e events ⌨F2 skin │ Soft · blue
└──────────────────────────────────────────────────────────────────────┘
```
### IN_SESSION — interacting with an agent
Left column is the conversation (messages from user, agent, kernel). Right column is the live event stream. Status bar expands to show session name, branch, model, context meter, state badge.
```
┌──────────────────────────────────────────────────────────────┐
│ ● connected · cas-segment-store · feat/cas-segments │
│ qwen_coder_14b (local) ctx ████████░░ 64% ▸ active │
├──────────────────────────┬───────────────────────────────────┤
│ router · conversation │ event stream ◌ live │
│ │ │
│ you 14:02 │ 14:11:02 [Lifecycle] stage.enter │
│ Implement CAS storage… │ 14:11:02 [Context] pack.synth │
│ │ 14:11:03 [Inference] infer.start │
│ correx 14:02 │ 14:11:19 [Tool] fs.write │
│ Routed → planning… │ 14:11:20 [Tool] fs.write │
│ │ 14:11:24 [Domain] artifact │
│ agent 14:05 │ 14:11:31 [Tool] shell.exec │
│ Plan: each segment is… │ 14:11:48 [Inference] infer.token │
│ │ 14:12:02 [Approval] approval │
│ correx 14:11 │ raised T3 network │
│ Transition planning→impl │ │
│ │ │
│ agent 14:12 │ │
│ SegmentStore.append()… │ │
│ │ │
│ ─────────────────────── │ │
│ ▌ Ask anything… │ │
│ feat/cas-segments · chat │ │
└──────────────────────────┴───────────────────────────────────┘
│ ⌨i message ⌨p commands ⌨a approval ⌨e events ⌨Tab panels ⌨F2 skin │ Soft · blue
└────────────────────────────────────────────────────────────────────────────────────┘
```
### APPROVAL — gate interrupted
The horizontal split collapses. The full main area shows the approval surface with tier badge, command preview, risk scan, steering input, and decision buttons.
```
┌──────────────────────────────────────────────────────────────┐
│ ● connected · cas-segment-store · feat/cas-segments │
│ qwen_coder_14b (local) ctx ████████░░ 64% ⏸ awaiting│
├──────────────────────────────────────────────────────────────┤
│ ┌─ approval ────────────────────────────────────────────┐ │
│ │ T3 Approval required · external / network │ │
│ │ Resolve a new dependency over the network │ │
│ │ paused │ │
│ │ │ │
│ │ $ ./gradlew :segment-store:dependencies │ │
│ │ │ │
│ │ Adds com.github.luben:zstd-jni:1.5.6-3 for cold- │ │
│ │ segment compression. Touches Maven Central. │ │
│ │ │ │
│ │ tool shell T3 │ │
│ │ reversible no │ │
│ │ policy stages.implementation.approvals.tool=req │ │
│ │ │ │
│ │ SEMANTIC RISK SCAN │ │
│ │ ▲ Network egress to repo1.maven.org │ │
│ │ ▲ Mutates gradle.lockfile │ │
│ │ ▲ New transitive: org.lz4? (none detected) │ │
│ │ │ │
│ │ ↳ ok, but lock the version_________________ │ │
│ │ │ │
│ │ [approve a] [auto-approve A] [steer s] [reject r] │ │
│ └───────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ ⌨a approve ⌨A auto-session ⌨s steer ⌨r reject ⌨esc later │ Soft · blue
└──────────────────────────────────────────────────────────────────────────┘
```
### Footer bar — contextual keybinds
| Screen | Hints shown |
|--------|-------------|
| IDLE | `i` message · `p` commands · `a` approval · `e` events · `F2` skin |
| IN_SESSION | above + `Tab` panels · `↑↓` history · `l` back |
| APPROVAL | `a` approve · `A` auto · `s` steer · `r` reject · `esc` later |
Right-aligned mode indicator always reads `Soft · blue`.
---
## 3. Animation & Microfeedback
- **Spinner** — `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` cycles at ~110ms. Seen next to "live" in event stream header, next to pending events, next to "paused" during approval.
- **Streaming cursor** — block `█` blinks at end of in-progress agent message. 50% opacity blink.
- **Toasts** — brief floating confirmations: "Approved", "Rejected", "Model switched". Appear near bottom center, auto-dismiss ~2.6s.
- **Panel transitions** — active panel border highlight shifts when switching focus with `Tab`.
---
# BUILD PLAN
---
## Phase 1 — Theming System
### File: NEW `components/Theme.kt`
Centralize all color and style constants into a single `object Theme` so every widget references the same palette. This eliminates the current pattern of scattered `Style.create().blue()` / `.cyan()` / `.gray()` calls.
```kotlin
object Theme {
// Surfaces
val bg: Color // xterm 233 (#14161a)
val bgDeep: Color // xterm 232 (#0f1115)
val panel: Color // xterm 234 (#181b21)
val panel2: Color // xterm 235 (#1f232b)
// Text
val fg: Color // xterm 252 (#ced3da)
val fgStrong: Color // xterm 255 (#eef1f5)
val dim: Color // xterm 243 (#838b96)
val faint: Color // xterm 238 (#434a55)
// Accent
val accent: Color // #4a9eff (blue)
val accent2: Color // #7aa2f7 (purple-blue)
// Semantic
val ok: Color // green
val warn: Color // amber
val bad: Color // red
// Border / selection
val border: Color // xterm 236 (#262b33)
val sel: Color // xterm 235+ (#21262e)
// Pre-built Style objects
val dimStyle: Style
val accentStyle: Style
val okStyle: Style
val warnStyle: Style
val badStyle: Style
val fgStrongStyle: Style
// Event category → color
fun categoryColor(category: String): Color
}
```
### Implementation Checklist
- [ ] Create `Theme.kt` in `components/`
- [ ] Define all Color constants (xterm or rgb depending on Mordant 2.6 API)
- [ ] Define pre-built Style objects for common patterns
- [ ] Define categoryColor() mapping (Lifecycle→accent2, Context→purple, Inference→accent, Tool→ok, Domain→amber, Approval→warn)
- [ ] Replace all hardcoded `.blue()`, `.cyan()`, `.gray()`, `.green()`, `.yellow()`, `.red()` calls across all existing components with Theme.* references
---
## Phase 2 — Layout: Three-Row Grid + Horizontal Split
### File modified: `TuiApp.kt`
**Current layout** (vertical stack for all display states):
```
StatusBar (height=1)
SessionList (height=min(count,7)+2)
[WorkflowList (height=6 if visible)]
[EventHistoryStrip (height=6 if visible)]
RouterPanel (fill)
InputBar (height=4)
```
**Target layout** — 3-row grid for all states:
```
grid rows: [status bar (length=1)] [main area (fill)] [footer bar (length=1)]
main area: horizontal split [left panel (ratio 1.7)] [right panel (ratio 1.0)]
```
Which widgets fill the panels depends on display state:
| State | Left panel (1.7) | Right panel (1.0) |
|-------|------------------|-------------------|
| IDLE | SessionList + WorkflowPicker | WelcomePanel |
| IN_SESSION | RouterPanel | EventStreamPanel |
| APPROVAL | ApprovalSurface (full width, no split) | — |
The input bar moves from being a top-level row **into the left panel's footer** (matching the reference's `Panel.footer` slot). This means each left-panel component is responsible for rendering its own footer containing the input bar.
### Layout code pattern
```kotlin
private fun renderInSessionLayout(frame: Frame, state: TuiState) {
val area = frame.area()
val vertRects = Layout.vertical()
.constraints(
Constraint.length(STATUS_HEIGHT),
Constraint.fill(), // main area
Constraint.length(FOOTER_HEIGHT),
)
.split(area)
val mainRects = Layout.horizontal()
.constraints(Constraint.ratio(1.7f), Constraint.ratio(1.0f))
.split(vertRects[1])
frame.renderWidget(statusBarWidget(state), vertRects[0])
frame.renderWidget(routerPanelWidget(state), mainRects[0]) // InputBar inside
frame.renderWidget(eventStreamPanelWidget(state), mainRects[1])
frame.renderWidget(footerBarWidget(state), vertRects[2])
}
```
### InputBar as panel footer
Each left-panel widget (RouterPanel, SessionListPanel) renders its own vertical stack:
- Top: [messages / session rows] (fill)
- Bottom: [InputBar] (fixed height)
This can be done by splitting the panel's allocated rect into two sub-rects, or combining both into a single Paragraph with a separator.
### Implementation Checklist
- [ ] Import `Layout.horizontal()` in `TuiApp.kt`
- [ ] IN_SESSION: 3-row grid with horizontal split (RouterPanel 1.7 + EventStreamPanel 1.0)
- [ ] IDLE: same grid but left = SessionList, right = WelcomePanel
- [ ] APPROVAL: full-width single column in main area
- [ ] Move InputBar from top-level widget to left panel footer
- [ ] Add footer bar rendering as grid row 3
---
## Phase 3 — Status Bar Polish
### File modified: `components/StatusBar.kt`
Upgrade from a simple connection+model line to the full reference `crx-top`:
```
● connected · cas-segment-store · feat/cas-segments · qwen_coder_14b (local) ctx ████████░░ 64% ▸ active
```
Segments (separated by `·`):
1. **Connection**`●` (green) / `○` (red) + connected/disconnected
2. **Session name** — bold (shown in IN_SESSION / APPROVAL)
3. **Branch / workflow ID** — dim
4. **Model** — accent color + provider label
5. **Context meter** — bar with accent fill on faint track + percentage
6. **State badge** — pill: `▸ active` (green) or `⏸ awaiting approval` (amber)
7. **Background badge**`+N updated` (dim yellow, shown when backgroundUpdateCount > 0)
### Context meter
Requires `contextUsed` and `contextBudget` in TuiState. Render as:
```
ctx [████████░░] 64%
```
Filled portion in accent color, empty portion in faint color.
### Implementation Checklist
- [ ] Add `contextUsed: Int?` and `contextBudget: Int?` to `TuiState`
- [ ] Replace current status bar spans with Theme references
- [ ] Add brand text with accented `x` (`corre` + `x`)
- [ ] Add session name + branch with `·` separators
- [ ] Add model + provider display
- [ ] Implement context meter bar (accent-fill on faint track)
- [ ] Implement state badge pill (warn for awaiting, ok for active)
- [ ] Keep background update badge, style with Theme.dim/yellow
---
## Phase 4 — Event Stream Panel
### File: NEW `components/EventStreamPanel.kt`
The right column in IN_SESSION state. Structured event list replacing the current raw-text `EventHistoryStrip`.
### Event row format
```
14:11:02 [Lifecycle] stage.enter implementation (role=coder)
14:11:19 [Tool] fs.write SegmentStore.kt (+212)
14:12:02 [Approval] approval.raised T3 · gradle network resolve ⠋[a]
```
Each row:
1. **Timestamp** — dim, fixed-width
2. **Category badge** — colored pill (see categoryColor mapping)
3. **Type** — bold/bright
4. **Detail** — normal text, truncated if long
5. **Pending indicator** — spinner + `[a]` keybind hint for pending approvals
Panel header shows:
```
event stream ◌ live
```
Or if session has pending approval:
```
event stream ⠋ paused
```
### Implementation Checklist
- [ ] Create `EventStreamPanel.kt` in `components/`
- [ ] Implement event row rendering with all 5 columns
- [ ] Wire categoryColor() into badge rendering
- [ ] Add pending event indicator (spinner + `[a]` hint)
- [ ] Add "live" / "paused" indicator in panel header
- [ ] Wire into TuiApp.kt as right column in IN_SESSION layout
---
## Phase 5 — Footer Keybind Bar
### File: NEW `components/FooterBar.kt`
Dedicated bottom row showing contextual keybind hints and current mode indicator. Currently these hints are embedded in InputBar row2 — they move here instead.
### Contextual hints
| State | Hints |
|-------|-------|
| IDLE | `i` message · `p` commands · `a` approval · `e` events · `F2` skin |
| IN_SESSION | above + `Tab` panels · `↑↓` history · `l` back |
| APPROVAL | `a` approve · `A` auto · `s` steer · `r` reject · `esc` later |
### Keybind chip pattern
```
i message p commands a approval
```
Key in white-on-blue (accent background, reversed), label in dim text.
### Mode indicator
Right-aligned: `Soft · blue`
### Implementation Checklist
- [ ] Create `FooterBar.kt` in `components/`
- [ ] Implement contextual hint logic based on display state / input mode / overlay
- [ ] Add mode indicator (right-aligned)
- [ ] Wire into TuiApp.kt as `Constraint.length(1)` in grid row 3
- [ ] Remove all key hint spans from InputBar row2 (keep only session name, chat mode)
---
## Phase 6 — Welcome Panel (IDLE Right Column)
### File: NEW `components/WelcomePanel.kt`
The right column in IDLE state. Shows contextual info when no session is active:
- Brand header (`Correx Agent Harness`) with accented `x`
- Brief contextual prompt ("select a session or type a name")
- Recent activity list (completed/active sessions with timestamps + status glyphs)
- Connection status hint if disconnected
### Implementation Checklist
- [ ] Create `WelcomePanel.kt` in `components/`
- [ ] Show brand header with accented `x`
- [ ] Show contextual prompt text
- [ ] Show recent sessions (if available) or empty-state message
- [ ] Use Theme colors throughout
---
## Phase 7 — Font Size 14-15
Terminal font size is emulator-controlled, not app-controlled. What we can do:
1. Review all MAX constants for readability at 14pt:
- `NAME_MAX = 15` → may need adjustment
- `DETAIL_MAX = 60` → review
- `DIFF_LINE_MAX_LENGTH = 100` → may need decrease
- `MAX_VISIBLE = 7` → may need decrease (fewer rows fit)
2. Add `Theme.fontSize: Int = 14` as documentation / scaling factor for layout constants.
3. If Mordant `Style.Builder` exposes size (needs verification), apply it.
### Implementation Checklist
- [ ] Add `Theme.fontSize` constant
- [ ] Review and adjust truncation/visibility constants
- [ ] Verify row estimates fit within target terminal sizes
---
## Phase 8 — Approval Surface Polish
### File modified: `components/ApprovalSurface.kt`
Already the most polished component, needs theme integration:
- Replace `Color.YELLOW` border with `Theme.warn`
- Replace hardcoded `.green()`, `.red()`, `.yellow()`, `.gray()` with Theme.*
- Tier badge → `Theme.pill()` pattern
- Risk scan section → `▲` prefix in amber (matching reference `.risk` class)
- Key hint bar → `Theme.dimStyle` with framed key labels
### Implementation Checklist
- [ ] Replace all hardcoded color calls with Theme references
- [ ] Replace `Color.YELLOW` border with `Theme.warn`
- [ ] Align risk section formatting with reference
---
## Phase 9 — Session List & Workflow List Polish
### Files: `SessionList.kt`, `WorkflowListPanel.kt`
Color substitution — these already use ROUNDED borders:
- `▶` prefix → `Theme.accent` (was `.blue()`)
- Names → `Theme.fgStrong` (was `.cyan()`)
- Status labels → `Theme.ok`, `Theme.warn`, `Theme.bad`
- Overflow / empty text → `Theme.dim`
- Borders → `Theme.border`
### Implementation Checklist
- [ ] Replace all hardcoded color calls with Theme references in SessionList.kt
- [ ] Replace all hardcoded color calls with Theme references in WorkflowListPanel.kt
---
# APPENDICES
---
## A. Color Reference
### Palette
| Token | Hex | ANSI | Use |
|-------|-----|------|-----|
| `bg` | `#14161a` | xterm 233 | Screen background |
| `bgDeep` | `#0f1115` | xterm 232 | Top/footer bar |
| `panel` | `#181b21` | xterm 234 | Panel surface |
| `panel2` | `#1f232b` | xterm 235 | Secondary surface |
| `fg` | `#ced3da` | xterm 252 | Body text |
| `fgStrong` | `#eef1f5` | xterm 255 | Headings, emphasis |
| `dim` | `#838b96` | xterm 243 | Muted text |
| `faint` | `#434a55` | xterm 238 | Borders, placeholders |
| `border` | `#262b33` | xterm 236 | Panel borders |
| `sel` | `#21262e` | xterm 235+ | Selection highlight |
| `accent` | `#4a9eff` | rgb(74,158,255) | Blue accent |
| `accent2` | `#7aa2f7` | rgb(122,162,247) | Purple-blue secondary |
| `ok` | `#7fd88f` | rgb(127,216,143) | Success |
| `warn` | `#e8c06a` | rgb(232,192,106) | Warning |
| `bad` | `#e87f7f` | rgb(232,127,127) | Error |
### Event category → color
| Category | Color | Hex |
|----------|-------|-----|
| Lifecycle | accent2 | `#7aa2f7` |
| Context | purple | `#c98fd9` |
| Inference | accent | `#4a9eff` |
| Tool | ok | `#7fd88f` |
| Domain | amber | `#e8b765` |
| Approval | warn | `#e8c06a` |
---
## B. Keybind Reference
| Key | Context | Action |
|-----|---------|--------|
| `↑↓` | Session list | Navigate sessions |
| `↑↓` | Input mode (IN_SESSION) | History navigation |
| `Enter` | Session list | Enter selected session |
| `Enter` | Input bar | Submit message / start session |
| `Tab` | Any | Toggle filter mode / focus panel |
| `i` | No overlay | Focus input bar |
| `p`, `/`, `:` | No overlay | Open command palette |
| `a` | No overlay | Open pending approval |
| `e` | No overlay | Open event inspector overlay |
| `c` | No overlay | Open context pack inspector |
| `g` | No overlay | Open stage graph |
| `m` | No overlay | Open model registry |
| `t` | No overlay | Open tool palette |
| `a` | Approval | Approve |
| `A` | Approval | Auto-approve session |
| `s` | Approval | Steer + approve |
| `r` | Approval | Reject |
| `ctrl+a` | Approval | Approve (alt) |
| `ctrl+r` | Approval | Reject (alt) |
| `ctrl+e` | Any | Toggle event strip |
| `ctrl+n` | IDLE | Start new session |
| `ctrl+l` | IN_SESSION | Return to session list |
| `ctrl+q` | Any | Quit |
| `ctrl+x` | Approval | Toggle diff expand |
| `F2` | Any | Cycle chrome direction |
| `F3` | Any | Cycle accent color |
| `Escape` | Overlay | Close overlay |
| `Escape` | Input focus | Blur input |
---
## C. State Changes
### TuiState additions
```kotlin
data class TuiState(
// ... existing fields ...
val contextUsed: Int? = null,
val contextBudget: Int? = null,
val eventStreamExpanded: Boolean = false,
)
```
`contextUsed` / `contextBudget` feed the context meter. Populated from `ServerMessage` snapshot data during snapshot phase.
---
## D. Implementation Order
| Phase | Files | Dependencies | Effort |
|-------|-------|-------------|--------|
| **1. Theme** | `Theme.kt` (new) | None | Medium |
| **2. Layout** | `TuiApp.kt`, left panel components | Phase 1 | Large |
| **3. Status bar** | `StatusBar.kt`, `TuiState.kt` | Phase 1 | Medium |
| **4. Event stream** | `EventStreamPanel.kt` (new) | Phase 1, Phase 2 | Medium |
| **5. Footer bar** | `FooterBar.kt` (new), `InputBar.kt` | Phase 1, Phase 2 | Small |
| **6. Welcome panel** | `WelcomePanel.kt` (new) | Phase 1, Phase 2 | Small |
| **7. Font** | `Theme.kt` (amend), constants review | Phase 1 | Small |
| **8. Approval** | `ApprovalSurface.kt` | Phase 1 | Small |
| **9. List polish** | `SessionList.kt`, `WorkflowListPanel.kt` | Phase 1 | Small |
---
## E. Constraints & Risks
1. **Mordant Color API**: `Color.rgb(r, g, b)` depends on Mordant 2.6.0. If unavailable, fall back to `Color.xterm(n)` for closest ANSI color. Verify during Phase 1.
2. **Horizontal split**: `Layout.horizontal()` is part of Tambouille 0.3.0 (currently used in build.gradle). If unavailable, simulate with two-column Paragraph or verify API existence before Phase 2.
3. **Context meter**: Requires upstream data (context used/budget) that may not be in current server protocol. May need to stub until data arrives.
4. **Interactive event clicks**: Tambouille supports widget click callbacks. Event row selection can be added if interactivity is needed.