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

27 KiB
Raw Permalink Blame History

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
└──────────────────────────────────────────────────────────────────────────┘
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.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

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])
}

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

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

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.