27 KiB
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
xincorrex) - 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.
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.ktincomponents/ - 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
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()inTuiApp.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 ·):
- Connection —
●(green) /○(red) + connected/disconnected - Session name — bold (shown in IN_SESSION / APPROVAL)
- Branch / workflow ID — dim
- Model — accent color + provider label
- Context meter — bar with accent fill on faint track + percentage
- State badge — pill:
▸ active(green) or⏸ awaiting approval(amber) - 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?andcontextBudget: Int?toTuiState - 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:
- Timestamp — dim, fixed-width
- Category badge — colored pill (see categoryColor mapping)
- Type — bold/bright
- Detail — normal text, truncated if long
- 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.ktincomponents/ - 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.ktincomponents/ - 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 accentedx - 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.ktincomponents/ - 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:
-
Review all MAX constants for readability at 14pt:
NAME_MAX = 15→ may need adjustmentDETAIL_MAX = 60→ reviewDIFF_LINE_MAX_LENGTH = 100→ may need decreaseMAX_VISIBLE = 7→ may need decrease (fewer rows fit)
-
Add
Theme.fontSize: Int = 14as documentation / scaling factor for layout constants. -
If Mordant
Style.Builderexposes size (needs verification), apply it.
Implementation Checklist
- Add
Theme.fontSizeconstant - 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.YELLOWborder withTheme.warn - Replace hardcoded
.green(),.red(),.yellow(),.gray()with Theme.* - Tier badge →
Theme.pill()pattern - Risk scan section →
▲prefix in amber (matching reference.riskclass) - Key hint bar →
Theme.dimStylewith framed key labels
Implementation Checklist
- Replace all hardcoded color calls with Theme references
- Replace
Color.YELLOWborder withTheme.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
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
- Mordant Color API:
Color.rgb(r, g, b)depends on Mordant 2.6.0. If unavailable, fall back toColor.xterm(n)for closest ANSI color. Verify during Phase 1. - 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. - Context meter: Requires upstream data (context used/budget) that may not be in current server protocol. May need to stub until data arrives.
- Interactive event clicks: Tambouille supports widget click callbacks. Event row selection can be added if interactivity is needed.