diff --git a/docs/TUI.md b/docs/TUI.md
new file mode 100644
index 0000000..f8d8d60
--- /dev/null
+++ b/docs/TUI.md
@@ -0,0 +1,999 @@
+# TUI: Terminal Interface Blueprint
+
+*A design document for the new madz terminal interface. Grounded in the existing implementation (Ink
++ `ink-scroll-view` + structured logger), inspired by the best patterns of bitchx IRC client. The
+core functionality stays the same — the new TUI renders it better.*
+
+---
+
+## 1. Philosophy
+
+The interface is a terminal. Text flows in from the system, text flows out from the user. No panels,
+no tabs, no switching. One scrollable output area, one input line.
+
+The IRC layout is borrowed for its elegance: messages accumulate above, input sits at the bottom,
+scrolling is natural. But the content is code, output, system responses — not conversation.
+
+### Core Tenets
+
+1. **Input is primary.** The user lives at the input line. The output area is secondary — read when
+needed, scroll when needed.
+2. **Output is a log.** System output, code output, agent responses — all flow into the same stream.
+3. **Silence is the default.** The interface should feel like a quiet terminal — alive with output,
+not noise.
+4. **Batteries included, not scripts required.** Runtime customizations (toggles, formats, filters)
+ship built-in. No config file editing needed for common changes.
+
+---
+
+## 2. Architecture Overview
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ [14:23] Mads: Hello, Jason. │
+│ [14:24] System: Build complete │
+│ [14:25] Mads: Here's the diff... │
+│ ... (scrollable conversation) ... │
+├─────────────────────────────────────────────────────────────┤
+│ [●] Ready | [⚡12] [💬42] [◣ 1.2k] │
+├─────────────────────────────────────────────────────────────┤
+│ > _ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### Component Hierarchy
+
+```
+App (src/tui/app.js)
+├── Banner / OnboardingPanel — First-run experience
+├── ConversationPanel — ScrollView-based message display
+│ └── ScrollView (ink-scroll-view)
+│ └── MessageBubble[] — Role-colored, markdown-rendered
+├── StatusBar — Status indicator, skill/message/context counts
+└── InputPanel — Text input with cursor display
+```
+
+No panels, no tabs, no switching. The interface is a single scrollable output area with one input
+line.
+
+### Key Dependencies
+
+| Dependency | Role |
+|------------|------|
+| `ink` | TUI framework (Box, Text, useInput, useStdout, useWindowSize) |
+| `ink-scroll-view` | Scrollable viewport (ScrollView, scrollToBottom, scrollBy, remeasure) |
+| `pino` | Structured logger (dual-file: madz.log + madz_error.log) |
+
+---
+
+## 3. Scrolling & Viewport
+
+The conversation area uses `ink-scroll-view`'s `ScrollView` component. This handles all viewport
+management — no custom virtual scroll logic needed.
+
+### How It Works
+
+```jsx
+// ConversationPanel wraps ScrollView around message bubbles
+
+ {messages.map(msg => )}
+
+```
+
+### Auto-Scroll Behavior
+
+```
+1. New message arrives → scrollToBottom() (deferred via setTimeout 0ms to allow React to commit)
+2. Streaming content grows → scrollToBottom() (React re-render triggers scroll)
+3. User scrolls up → stays where they are (no forced scroll)
+4. Terminal resize → remeasure() called via stdout.on("resize")
+```
+
+### Scroll API (via ref)
+
+| Method | Purpose |
+|--------|---------|
+| `scrollToBottom()` | Scroll to the end of content |
+| `scrollBy(delta)` | Scroll by N rows (positive = down, negative = up) |
+| `scrollTo(offset)` | Scroll to absolute position |
+| `scrollToTop()` | Scroll to offset 0 |
+| `remeasure()` | Re-measure viewport dimensions (call on terminal resize) |
+| `remeasureItem(index)` | Force re-measure of a specific child (useful for dynamic content) |
+| `getViewportHeight()` | Get visible row count |
+| `getScrollOffset()` | Get current scroll position |
+| `getContentHeight()` | Get total content height |
+| `getBottomOffset()` | Get scroll offset when at bottom |
+| `getItemHeight(index)` | Get measured height of a specific item |
+| `getItemPosition(index)` | Get position and height of a specific item |
+
+### Keyboard Scrolling (when input is unfocused)
+
+| Key | Action |
+|-----|--------|
+| Up arrow | `scrollBy(-1)` |
+| Down arrow | `scrollBy(1)` |
+| PageUp | `scrollBy(-viewportHeight)` |
+| PageDown | `scrollBy(viewportHeight)` |
+
+### Controlled Mode
+
+For advanced use cases (e.g., synchronizing multiple views), `ink-scroll-view` provides
+`ControlledScrollView` which accepts a `scrollOffset` prop instead of managing state internally.
+
+---
+
+## 4. The Cursor
+
+The cursor is managed by ink's `useCursor` hook, which provides `setCursorPosition` for
+controlling cursor visibility and position. Passing `undefined` hides the cursor; passing a
+`{x, y}` object shows it at the specified position.
+
+### Configuration
+
+```jsx
+import { useCursor } from 'ink';
+
+const { setCursorPosition } = useCursor();
+
+// Hide cursor when input is unfocused
+setCursorPosition(undefined);
+
+// Show cursor at input position when focused
+setCursorPosition({ x: stringWidth(prompt + text), y: 1 });
+```
+
+### Behavior
+
+- **Input focused** — cursor shown at input position via `setCursorPosition({x, y})`
+- **Input unfocused** — cursor hidden via `setCursorPosition(undefined)`
+- **Blinking** — handled by the terminal emulator, not by Ink
+- **Character** — sourced from `config.tui.cursorChar` (set via terminal escape sequences)
+
+The TUI manages cursor visibility toggling based on input focus state. `useCursor` provides the
+positioning primitive; the TUI decides when to show or hide.
+
+---
+
+## 5. Message Display
+
+Messages are rendered as role-colored bubbles with markdown support, tool call display, and
+reasoning content.
+
+### Message Structure
+
+```typescript
+interface Message {
+ role: "user" | "assistant" | "system";
+ content: string;
+ time: string; // HH:MM format
+ streaming?: boolean; // True while content is being streamed
+ reasoningContent?: string; // Agent thinking (shown inline, truncated at 200 chars)
+ activeToolCall?: { name: string }; // Currently running tool
+ toolCallDisplay?: string; // Completed tool calls (multi-line)
+ toolCalls?: { name: string }[]; // Tool call history
+ id?: string; // Stable identifier for memoization
+}
+```
+
+### Role-Based Styling
+
+| Role | Label Color | Bubble Border | Alignment |
+|------|------------|---------------|-----------|
+| **user** | Green | Green | Right |
+| **system** | Yellow | Yellow | Left |
+| **assistant** | Cyan | Cyan | Left |
+
+### Message Bubble Rendering
+
+```
+┌───────────────────────────────────────────┐
+│ [14:23] Mads: Here's the code: │
+│ │
+│ ```js │
+│ const x = 42; │
+│ ``` │
+│ │
+│ - Tool: readFile (src/logger.js) │
+│ - Tool: patch (src/tui/app.js) │
+│ │
+│ (thinking) Analyzing the request... │
+└───────────────────────────────────────────┘
+```
+
+### Memoization
+
+`MessageBubble` uses `React.memo` with a custom `areEqual` function that compares display-relevant
+fields (role, content, time, reasoningContent, streaming, toolCallDisplay, activeToolCall, id). This
+prevents unnecessary re-renders of unchanged messages during streaming.
+
+### Markdown Rendering
+
+Assistant messages are rendered through `marked` + `marked-terminal`, which converts markdown to
+ANSI terminal text. A module-level parse cache (`Map`) avoids reparsing identical content across
+renders. The streaming cursor character (`█`) is stripped before parsing to avoid parser errors.
+
+---
+
+## 6. Runtime Configuration (Bitchx-Inspired)
+
+Bitchx's `/toggle` command was legendary because it made common customizations instant — no config
+file editing required. The TUI should ship with sensible defaults and built-in features for runtime
+control. All TUI configuration lives in the `tui` section of `config.yaml`.
+
+### TUI Configuration (config.yaml)
+
+```yaml
+tui:
+ name: madz
+ cursorChar: "█"
+ autoScroll: true
+ timestamps: true
+ commandEcho: true
+ cursorBreathe: true
+ debugOutput: false
+```
+
+### Proposed: Toggle Commands
+
+Toggle commands allow runtime overrides of the `config.yaml` defaults:
+
+| Toggle | Default | Description |
+|--------|---------|-------------|
+| `autoScroll` | `true` | Auto-scroll to bottom on new messages |
+| `timestamps` | `true` | Show timestamps on messages |
+| `commandEcho` | `true` | Echo user commands to output |
+| `cursorBreathe` | `true` | Enable breathing cursor model |
+| `debugOutput` | `false` | Show debug-level messages |
+
+Usage:
+```
+/toggle timestamps → turns timestamps off
+/toggle timestamps → turns timestamps on (toggle)
+/toggle → shows all toggles and their states
+```
+
+### Proposed: Format Customization
+
+Bitchx's `/fset` allowed users to customize how every message type rendered. The TUI could adopt a
+similar pattern:
+
+```
+/format system "[%T] %BSystem%n: %I%t%n"
+/format error "[%T] %RError%n: %t"
+/format agent "[%T] %CMads%n: %t"
+```
+
+Format specifiers:
+- `%T` — timestamp (respects `timestamps` toggle)
+- `%t` — text body
+- `%B` — bold, `%n` — null color (reset)
+- `%I` — italic, `%R` — red, `%C` — cyan, `%M` — magenta
+
+> **YAGNI:** These format specifiers are speculative. Implement only if there is a clear, demonstrated need. Do not build a full format customization system without evidence that users will use it.
+
+### Proposed: Message Filtering
+
+Bitchx had a sophisticated message-level system where you could filter what appeared in each window.
+The TUI could adopt a similar pattern:
+
+```
+/level debug → toggle debug messages on/off
+/level → show active levels
+/level all → show all levels and their states
+```
+
+Available levels:
+| Level | Description |
+|-------|-------------|
+| `user` | User messages |
+| `assistant` | Agent responses |
+| `system` | System notifications |
+| `debug` | Debug/internal messages (hidden by default) |
+
+> **YAGNI:** Message-level filtering adds significant complexity (parsing, persistence, UI indicators). Implement only if there is a clear, demonstrated need.
+
+### Persistence
+
+Runtime toggle overrides are stored in memory only. The `config.yaml` `tui` section is the source of
+truth — changes to it take effect on restart. No separate `tui-config.json` file is needed.
+
+---
+
+## 7. Command Parser
+
+Commands are parsed from input when Enter is pressed. The `CommandParser` class handles a dispatch
+table of registered commands, with fallback to skill execution.
+
+### Registered Commands
+
+| Command | Behavior |
+|---------|----------|
+| `/quit` | Disconnect and exit |
+| `/clear` | Clear conversation |
+| `/new` | Start a new session |
+| `/help` | Show available commands |
+| `/config set ` | Set a config value |
+| `/provider set ` | Switch AI provider |
+| `/schedule list` | List scheduled tasks |
+| `/schedule pause ` | Pause a scheduled task |
+| `/schedule resume ` | Resume a scheduled task |
+| `/schedule run-now ` | Run a scheduled task immediately |
+| `/gc` | Trigger V8 garbage collection |
+| `/gc status` | Show GC status |
+
+### Skill Execution
+
+Unrecognized `/command` patterns that match a registered skill name are executed as skills:
+```
+/skillName [args]
+```
+
+The skill body (from `SKILL.md`) is loaded and streamed to the agent as a prompt, allowing the agent
+to interpret and execute the skill instructions.
+
+### Unknown Commands
+
+```
+/unknownCommand
+→ "Unknown command: /unknownCommand. Type /help for available commands."
+```
+
+---
+
+## 8. Interaction Model
+
+### Keyboard Shortcuts
+
+| Key | Action | Cursor |
+|-----|--------|--------|
+| Any character | Append to input | Visible |
+| Enter | Submit command | Visible → Hidden (after submit) |
+| Escape | Interrupt streaming / quit | Hidden |
+| Tab | Toggle input focus | N/A |
+| Up arrow (focused) | Scroll through command history | Visible |
+| Down arrow (focused) | Scroll forward through history | Visible |
+| Up arrow (unfocused) | Scroll output up | Hidden |
+| Down arrow (unfocused) | Scroll output down | Hidden |
+| PageUp (unfocused) | Scroll output up 1 page | Hidden |
+| PageDown (unfocused) | Scroll output down 1 page | Hidden |
+
+### Command History
+
+The up/down arrows scroll through command history when the user is at the bottom of the output (not
+scrolling the output itself). This is a terminal convention:
+
+```
+1. User presses Enter → command executes, output appears, auto-scroll active
+2. User presses Up → scrolls through previous commands (not output)
+3. User presses Down → scrolls forward through command history
+4. User presses Up while scrolled up in output → scrolls output
+```
+
+**Note:** History is in-memory (`chatHistory` array). The structured logger (`src/logger.js`)
+handles persistent logging of all interactions.
+
+### Input Lifecycle
+
+```
+1. User presses key → cursor appears, input focused
+2. User types → cursor follows text end
+3. User presses Enter → command executed, output appended, input cleared, cursor fades
+4. 2 seconds idle → cursor hidden (color transition to dark gray)
+5. User presses key again → cycle repeats
+```
+
+---
+
+## 9. Status Bar
+
+The status bar displays connection status, system metrics, and contextual information.
+
+### Current Implementation
+
+```
+[●] Ready | [⚡12] [💬42] [◣ 1.2k]
+```
+
+| Element | Content |
+|---------|---------|
+| Status indicator | `●` green (ready), `▶` yellow (streaming), `✖` red (error) |
+| Status message | Current state ("Ready", "Streaming...", "Compacting context...") |
+| Skill count | Number of registered skills |
+| Message count | Total messages in conversation |
+| Context size | Current conversation token count (human-readable: "1.2k", "15k") |
+
+### Proposed Enhancement
+
+Add toggle/filter indicators to the status bar:
+```
+[●] Ready | [⚡12] [💬42] [◣ 1.2k] [ts:1 scroll:1]
+```
+
+This gives the user a quick glance at which runtime features are active.
+
+---
+
+## 10. Edge Cases & Resilience
+
+### Terminal Resize
+
+```
+1. Detect resize via stdout.on("resize")
+2. Call scrollRef.current.remeasure() to update viewport dimensions
+3. ink-scroll-view handles re-layout automatically
+```
+
+### Streaming Overflow
+
+```
+1. New message arrives during streaming
+2. Content hash (messageCount + streamingContentLength) triggers re-evaluation
+3. scrollToBottom() is deferred 0ms to allow ink-scroll-view's useLayoutEffect to complete
+4. Previous content hash is tracked to avoid redundant scrolls
+```
+
+### Connection Loss
+
+```
+1. Error in dispatchProvider → catch block handles it
+2. System message displayed: "I couldn't connect right now - {error}. Try sending your message
+again?"
+3. Streaming message is cleared from UI
+4. Session is saved (onSaveSession callback)
+```
+
+### Model Stuck in Thinking Loop
+
+```
+1. Auto-continue circuit breaker tracks consecutive empty responses
+2. After config.agent.autoContinueLimit (default 1000) empty responses:
+ - Show error message
+ - Reset counter
+ - User must rephrase or start new session
+```
+
+### Output Retention
+
+The conversation is managed by `sessionState` (not the TUI). The TUI renders whatever messages are
+in its state array. Memory management (compaction, trimming) is handled by the session layer, not
+the TUI.
+
+---
+
+## 11. Implementation Notes
+
+### Ink-Specific Patterns
+
+1. **`useStdout`** — For terminal resize events (call `remeasure()` on the scroll ref)
+2. **`useInput`** — For keyboard handling (single handler in App component)
+3. **`ScrollView` (ink-scroll-view)** — For scrollable conversation area
+4. **`React.memo`** — For MessageBubble optimization
+
+### Key Patterns
+
+```jsx
+// Scroll to bottom with deferred timing
+const scrollHandle = () => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollToBottom();
+ }
+};
+const timer = setTimeout(scrollHandle, 0);
+
+// Terminal resize handling
+useEffect(() => {
+ const resizeHandler = () => {
+ if (scrollRef.current && stdout.isTTY && !process.env.CI) {
+ scrollRef.current.remeasure();
+ }
+ };
+ stdout.on("resize", resizeHandler);
+ return () => stdout.off("resize", resizeHandler);
+}, [stdout, scrollRef]);
+
+// Abort controller for streaming interruption
+abortControllerRef.current = new AbortController();
+// ...
+abortControllerRef.current.abort();
+```
+
+### State Management
+
+The current app uses React's built-in state (`useState`, `useRef`) rather than `useReducer`. This is
+sufficient for the current scale. See Section 16 for proposed consolidation.
+
+---
+
+## 12. Streaming Architecture
+
+This section describes how the TUI wires into the AI agent's streaming pipeline.
+
+### Data Flow
+
+```
+User Input (Enter)
+ → App.handleSubmit()
+ → App.handleChat() or App.handleCommand()
+ → dispatchProvider(message, provider, streamingCallback, signal)
+ → callProvider()
+ → callReactAgent(agent, message, sessionConfig, callPrompt, streamingCallback, options)
+ → LangGraph ReactAgent stream
+ → streamingCallback(event) for each event type:
+ - "text" → append to committedContent, update message.content + █ cursor
+ - "reasoning" → append to committedReasoning, update message.reasoningContent
+ - "tool_start" → set message.activeToolCall
+ - "tool_end" → append to message.toolCallDisplay
+ - "tool_error" → append error to message.toolCallDisplay
+ - "compaction_start" / "compaction_end" → toggle isCompacting
+```
+
+### Streaming Callback
+
+The `streamingCallback` is set up in `handleChat()` / `handleCommand()` and passed to
+`dispatchProvider`. Each event type triggers a `setMessages()` call that clones the messages array
+and mutates the last (streaming) message in place.
+
+### Auto-Continue Circuit Breaker
+
+If the agent returns zero text output, the TUI automatically sends a "Please continue." signal. This
+repeats up to `config.agent.autoContinueLimit` (default 1000) times before triggering a circuit
+breaker error. The counter resets as soon as any text output arrives. An `isAutoContinuingRef` flag
+tracks whether the TUI is in auto-continue mode.
+
+### Abort / Interrupt
+
+The `Escape` key triggers `handleInterrupt()`, which:
+1. Calls `abortControllerRef.current.abort()`
+2. Sets `isStreamingRef.current = false`
+3. Clears the streaming cursor from the last message
+4. Awaits the `dispatchPromise` (which throws `AbortError` and is caught by the try/catch)
+5. If interrupted during chat: pops the user message from sessionState, clears the partial assistant
+message, deletes the checkpointer thread
+6. If interrupted during skill: pops the user message, deletes the checkpointer thread
+
+### Todo Queue Integration
+
+The todo tool queue emits status events (`todo_status`) that flow through the LangGraph stream. The
+TUI wires into this via `setTodoStreamingCallback()`, which updates `message.toolCallDisplay` with
+todo status lines alongside tool call results.
+
+---
+
+## 13. Session & Persistence
+
+### Session Lifecycle
+
+```
+1. index.js creates session via createSession()
+2. SessionStateManager wraps the session state
+3. App receives sessionState as a prop
+4. On user message: sessionState.addExchange({ role: "user", content })
+5. On agent response: sessionState.addExchange({ role: "assistant", content })
+6. onSaveSession callback persists to memory/sessions/
+```
+
+### Context Token Calculation
+
+The TUI calculates conversation tokens using `tiktoken` (with a character-count fallback). Both the
+conversation and the system prompt are counted. The result is displayed in the status bar as a
+human-readable size (e.g., "1.2k").
+
+### GC Integration
+
+When GC is enabled (`config.memory.gc.enabled !== false`), a `gcManager` is initialized in
+`index.js`. The TUI receives `gcManager.onActivity` as a prop, which is called after each message
+exchange to trigger idle GC. The `/gc` command allows manual triggering and status inspection.
+
+---
+
+## 14. Onboarding & Banner
+
+### Banner
+
+A BBS-style ASCII art banner displays on first launch. It shows command help grouped by category
+(Chat, Command). Dismisses on any key press (Escape exits the app).
+
+### Onboarding Panel
+
+When no user profile exists, the onboarding flow activates:
+1. `OnboardingPanel` renders with prompts from the onboarding instance
+2. User responses are processed via `onboarding.processResponse()`
+3. Progress is shown as `(current/total)`
+4. On completion, the banner displays and normal conversation begins
+
+---
+
+## 15. Summary
+
+This blueprint describes the madz terminal interface, grounded in the existing implementation. The
+design is defined by four principles:
+
+1. **Input is primary.** The user lives at the input line. Output is secondary.
+2. **Silence is the default.** The interface should feel like a quiet terminal — alive with output,
+not noise.
+3. **Batteries included.** Runtime customizations (toggles, formats, filters) should ship built-in —
+no config file editing required.
+4. **The terminal is the window.** `ink-scroll-view` handles scrolling, Ink handles rendering, the
+structured logger handles persistence.
+
+The result is an interface that feels like a quiet terminal — present, alive, and ready for work.
+Not a dashboard. Not a tool. A workspace.
+
+---
+
+## 16. Architectural Debt & Proposed Improvements
+
+The current implementation works, but several structural decisions compound as the TUI grows. This
+section documents known debt and proposed improvements for future refactoring.
+
+### 16.1 State Management — `useReducer` over `useState`
+
+The current `app.js` has eight independent `useState` calls with no coordination:
+
+```typescript
+const [messages, setMessages] = useState([]);
+const [statusMessage, setStatusMessage] = useState("Ready");
+const [chatHistory, setChatHistory] = useState([]);
+const [historyIndex, setHistoryIndex] = useState(-1);
+const [inputText, setInputText] = useState("");
+const [inputFocused, setInputFocused] = useState(true);
+const [contextSize, setContextSize] = useState(0);
+const [isCompacting, setIsCompacting] = useState(false);
+```
+
+When a message arrives, you're updating `messages`, `statusMessage`, `contextSize`, and
+`chatHistory` — all separate calls, all separate renders. Consolidate into a single `useReducer`
+with a `TUIState` interface. One state tree, one render cycle per meaningful change.
+
+### 16.2 Streaming Logic — Extract to Its Own Hook
+
+The streaming callback is set up inline in `handleChat()` / `handleCommand()` and passed through
+multiple layers. Extract into a `useStreaming()` hook that:
+
+- Manages the `AbortController` lifecycle
+- Translates stream events into state transitions
+- Handles the auto-continue circuit breaker
+- Exposes a clean `streamingState` object to the UI
+
+Separates *how we stream* from *what we stream*.
+
+### 16.3 File Structure — Group by Concern, Not by Component
+
+The current flat 17-file layout works for now but doesn't scale. Proposed structure:
+
+```
+src/tui/
+├── app.js # Root component, providers
+├── state/
+│ ├── reducer.js # TUIState + all action types
+│ ├── types.js # Action type constants
+│ └── selectors.js # Derived state (contextSize, statusMessage, etc.)
+├── hooks/
+│ ├── useStreaming.js # AbortController, event transformation, auto-continue
+│ ├── useScroll.js # ScrollView ref, resize handling, keyboard scroll
+│ ├── useInput.js # Keyboard routing (scroll vs history vs input)
+│ └── useCommand.js # Command parsing + dispatch
+├── components/
+│ ├── ConversationPanel.js
+│ ├── InputPanel.js
+│ ├── StatusBar.js
+│ ├── MessageBubble.js
+│ └── Banner.js
+├── panels/
+│ ├── OnboardingPanel.js
+│ └── (future panels, if any)
+├── utils/
+│ ├── commandParser.js
+│ ├── contextTokens.js
+│ ├── markdownText.js
+│ └── format.js # Format specifiers, toggle logic
+└── index.js
+```
+
+Not dogma — predictability. When you're looking for streaming logic, you know exactly where to look.
+
+### 16.4 Remove the Panel System Entirely
+
+The `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js` files contradict the
+blueprint's philosophy ("No panels, no tabs, no switching"). If Jason needs to inspect skills or
+memory, those are commands (`/skills`, `/memory`) that produce output in the conversation stream —
+not separate UI surfaces. The TUI should be one thing: a terminal.
+
+### 16.5 Command Parser — Event-Driven, Not Switch-Driven
+
+The current `commandParser.js` is a dispatch table. Make it more extensible — a command registry
+where commands are registered as objects with `validate`, `execute`, and `help` properties. Adding a
+new command is a registration, not a switch case edit.
+
+### 16.6 What to Keep
+
+- `ink-scroll-view` for scrolling — works well
+- `React.memo` on MessageBubble — correct optimization
+- The structured logger — dual-file pino is solid
+- The `tiktoken` context token calculation — accurate
+- The overall component hierarchy (App → ConversationPanel + StatusBar + InputPanel) — that part is right
+
+### 16.7 What to Keep the Same
+
+The *philosophy* — input is primary, output is a log, silence is the default. That's the right
+mental model. It's the implementation scaffolding around it that should be reorganized.
+
+---
+
+## 17. Implementation Spec
+
+This section provides the concrete types, interfaces, and patterns needed to implement the TUI.
+
+### 17.1 State Shape
+
+```typescript
+interface TUIState {
+ // Messages
+ messages: Message[];
+ chatHistory: string[];
+ historyIndex: number;
+
+ // Input
+ inputText: string;
+ inputFocused: boolean;
+
+ // Status
+ statusMessage: string;
+ contextSize: number;
+ isCompacting: boolean;
+
+ // Streaming
+ isStreaming: boolean;
+ isAutoContinuing: boolean;
+ autoContinueCount: number;
+
+ // Scroll
+ scrollOffset: number;
+ viewportHeight: number;
+
+ // Config overrides (runtime toggles)
+ toggles: {
+ autoScroll: boolean;
+ timestamps: boolean;
+ commandEcho: boolean;
+ cursorBreathe: boolean;
+ debugOutput: boolean;
+ };
+}
+
+const initialState: TUIState = {
+ messages: [],
+ chatHistory: [],
+ historyIndex: -1,
+ inputText: '',
+ inputFocused: true,
+ statusMessage: 'Ready',
+ contextSize: 0,
+ isCompacting: false,
+ isStreaming: false,
+ isAutoContinuing: false,
+ autoContinueCount: 0,
+ scrollOffset: 0,
+ viewportHeight: 0,
+ toggles: {
+ autoScroll: true,
+ timestamps: true,
+ commandEcho: true,
+ cursorBreathe: true,
+ debugOutput: false,
+ },
+};
+```
+
+### 17.2 Action Types
+
+```typescript
+type TUIAction =
+ // Messages
+ | { type: 'ADD_MESSAGE'; message: Message }
+ | { type: 'UPDATE_MESSAGE'; id: string; updates: Partial }
+ | { type: 'CLEAR_MESSAGES' }
+ | { type: 'ADD_HISTORY'; text: string }
+ | { type: 'SET_HISTORY_INDEX'; index: number }
+
+ // Input
+ | { type: 'SET_INPUT_TEXT'; text: string }
+ | { type: 'SUBMIT_INPUT' }
+ | { type: 'SET_INPUT_FOCUSED'; focused: boolean }
+
+ // Status
+ | { type: 'SET_STATUS'; message: string }
+ | { type: 'SET_CONTEXT_SIZE'; size: number }
+ | { type: 'SET_COMPACTING'; compacting: boolean }
+
+ // Streaming
+ | { type: 'SET_STREAMING'; streaming: boolean }
+ | { type: 'SET_AUTO_CONTINUING'; autoContinuing: boolean }
+ | { type: 'INCREMENT_AUTO_CONTINUE' }
+ | { type: 'RESET_AUTO_CONTINUE' }
+
+ // Scroll
+ | { type: 'SET_SCROLL_OFFSET'; offset: number }
+ | { type: 'SET_VIEWPORT_HEIGHT'; height: number }
+
+ // Config
+ | { type: 'TOGGLE_CONFIG'; key: keyof TUIState['toggles'] }
+ | { type: 'SET_CONFIG'; updates: Partial };
+```
+
+### 17.3 Command Registry Schema
+
+```typescript
+interface Command {
+ name: string;
+ description: string;
+ usage: string;
+ validate: (args: string[]) => boolean | string; // returns error message on failure
+ execute: (
+ args: string[],
+ state: TUIState,
+ dispatch: React.Dispatch,
+ helpers: CommandHelpers
+ ) => Promise | void;
+}
+
+interface CommandHelpers {
+ dispatchProvider: (message: string, provider: string, streamingCallback: StreamingCallback, signal: AbortSignal) => Promise;
+ sessionState: SessionStateManager;
+ config: Config;
+ scrollRef: React.RefObject;
+}
+
+// Registration
+const commands: Record = {
+ quit: {
+ name: 'quit',
+ description: 'Disconnect and exit',
+ usage: '/quit',
+ validate: () => true,
+ execute: async () => { /* ... */ },
+ },
+ // ... more commands
+};
+```
+
+### 17.4 Streaming Callback Transformation
+
+```typescript
+type StreamEventType =
+ | 'text'
+ | 'reasoning'
+ | 'tool_start'
+ | 'tool_end'
+ | 'tool_error'
+ | 'compaction_start'
+ | 'compaction_end'
+ | 'todo_status';
+
+interface StreamingCallback {
+ (event: StreamEvent): void;
+}
+
+interface StreamEvent {
+ type: StreamEventType;
+ content?: string; // for 'text', 'reasoning'
+ toolName?: string; // for 'tool_start', 'tool_end', 'tool_error'
+ toolArgs?: string; // for 'tool_end'
+ error?: string; // for 'tool_error'
+ status?: string; // for 'todo_status'
+}
+
+// Transformation logic (inside useStreaming hook)
+function handleStreamEvent(event: StreamEvent, state: TUIState): Partial {
+ switch (event.type) {
+ case 'text':
+ return {
+ isStreaming: true,
+ messages: updateLastMessage(state.messages, {
+ content: event.content,
+ streaming: true,
+ }),
+ };
+ case 'reasoning':
+ return {
+ messages: updateLastMessage(state.messages, {
+ reasoningContent: event.content,
+ }),
+ };
+ case 'tool_start':
+ return {
+ messages: updateLastMessage(state.messages, {
+ activeToolCall: { name: event.toolName! },
+ }),
+ };
+ case 'tool_end':
+ return {
+ messages: updateLastMessage(state.messages, {
+ activeToolCall: undefined,
+ toolCallDisplay: event.content,
+ }),
+ };
+ case 'tool_error':
+ return {
+ messages: updateLastMessage(state.messages, {
+ activeToolCall: undefined,
+ toolCallDisplay: event.error,
+ }),
+ };
+ case 'compaction_start':
+ return { isCompacting: true, statusMessage: 'Compacting context...' };
+ case 'compaction_end':
+ return { isCompacting: false, statusMessage: 'Ready' };
+ case 'todo_status':
+ return {
+ messages: updateLastMessage(state.messages, {
+ toolCallDisplay: event.status,
+ }),
+ };
+ }
+}
+```
+
+### 17.5 File Structure
+
+```
+src/tui/
+├── app.js # Root component, providers, reducer
+├── state/
+│ ├── reducer.js # useReducer implementation, all action handlers
+│ ├── types.js # TUIState, TUIAction, Message interfaces
+│ └── selectors.js # Derived state (contextSize, statusMessage, etc.)
+├── hooks/
+│ ├── useStreaming.js # AbortController, event transformation, auto-continue
+│ ├── useScroll.js # ScrollView ref, resize handling, keyboard scroll
+│ ├── useInput.js # Keyboard routing (scroll vs history vs input)
+│ └── useCommand.js # Command parsing + dispatch
+├── components/
+│ ├── ConversationPanel.js # ScrollView + MessageBubble[]
+│ ├── InputPanel.js # Text input with cursor
+│ ├── StatusBar.js # Status indicator, metrics
+│ ├── MessageBubble.js # Role-colored, markdown-rendered
+│ └── Banner.js # ASCII art banner
+├── panels/
+│ └── OnboardingPanel.js # First-run onboarding flow
+├── utils/
+│ ├── commandParser.js # Command registry, dispatch table
+│ ├── contextTokens.js # tiktoken token calculation
+│ ├── markdownText.js # marked + marked-terminal rendering
+│ └── format.js # Format specifiers, toggle logic
+└── index.js # Entry point, render()
+```
+
+### 17.6 Test Strategy
+
+**Unit tests** (`tests/unit/tui/`):
+- `reducer.test.js` — All action types, edge cases (empty state, concurrent updates)
+- `commandParser.test.js` — Command validation, execution, unknown commands
+- `contextTokens.test.js` — tiktoken calculation, character-count fallback
+- `markdownText.test.js` — Markdown rendering, streaming cursor stripping, cache behavior
+- `useStreaming.test.js` — Event transformation, auto-continue circuit breaker, abort handling
+
+**Integration tests** (`tests/integration/tui/`):
+- `full-flow.test.js` — User input → streaming → message display → command execution
+
+**Mocking**:
+- `dispatchProvider` — Mock with resolved promise, configurable streaming events
+- `tiktoken` — Mock with fixed token counts
+- `marked-terminal` — Mock with plain text output
+- `ink-scroll-view` — Mock ScrollViewRef with no-op methods
+
+**Coverage**: 100% on all new files. Existing files maintain current coverage.
+
+---
+
+## References
+
+### Libraries
+
+- **[Ink](https://github.com/vadimdemedes/ink)** — React for CLIs. [README](https://raw.githubusercontent.com/vadimdemedes/ink/refs/heads/master/readme.md)
+- **[ink-scroll-view](https://github.com/ByteLandTechnology/ink-scroll-view)** — ScrollView component for Ink. [README](https://raw.githubusercontent.com/ByteLandTechnology/ink-scroll-view/refs/heads/main/README.md)
+
+---
+
+*Blueprint for the new madz terminal interface. Grounded in the existing implementation and the
+ink + ink-scroll-view libraries.*
diff --git a/docs/TUI2.md b/docs/TUI2.md
deleted file mode 100644
index 851f730..0000000
--- a/docs/TUI2.md
+++ /dev/null
@@ -1,588 +0,0 @@
-# TUI2: Terminal Interface Blueprint
-
-*A clean-slate design for a code-first terminal interface using Ink and `useCursor`. IRC-inspired layout, not IRC semantics.*
-
----
-
-## 1. Philosophy
-
-The interface is a terminal. Text flows in from the system, text flows out from the user. No panels, no tabs, no switching. One scrollable output area, one input line.
-
-The IRC layout is borrowed for its elegance: messages accumulate above, input sits at the bottom, scrolling is natural. But the content is code, output, system responses — not conversation.
-
-### Core Tenets
-
-1. **Input is primary.** The user lives at the input line. The output area is secondary — read when needed, scroll when needed.
-2. **Output is a log.** System output, code output, agent responses — all flow into the same stream.
-3. **The cursor is the only control.** It appears when you're about to act, disappears when you're reading.
-4. **Silence is the default.** The interface should feel like a quiet terminal — alive with output, not noise.
-
----
-
-## 2. Architecture Overview
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Header: [● connected] │
-├─────────────────────────────────────────────────────────────┤
-│ $ npm run build │
-│ > madz@1.0.0 build │
-│ > tsc --project tsconfig.json │
-│ > Compiled successfully in 1.2s │
-│ [14:23] System: Build complete │
-│ [14:24] System: Running tests... │
-│ ... (scrollable output history) ... │
-├─────────────────────────────────────────────────────────────┤
-│ > _ │
-└─────────────────────────────────────────────────────────────┘
-```
-
-### Component Hierarchy
-
-```
-App
-├── Layout — Full terminal dimensions, manages resize
-├── Header — Connection status only
-├── OutputList — Virtualized output rendering
-├── InputBar — Text input with cursor management
-└── StatusLine — Bottom bar: scroll position, mode indicators
-```
-
-### State Model
-
-```typescript
-interface AppState {
- // Output management
- entries: OutputEntry[]; // Mixed: system output, code output, responses
- scrollOffset: number; // Lines above the fold
- autoScroll: boolean; // True when at bottom
-
- // Input & cursor
- input: string;
- cursorVisible: boolean;
- cursorRow: number; // Row within terminal (for useCursor positioning)
-
- // Connection
- connected: boolean;
- lastActivity: number; // Timestamp of last user input
-}
-```
-
----
-
-## 3. The Cursor Model
-
-This is the heart of the design. `useCursor` from Ink gives us precise control over cursor visibility and position. We use it to create a **breathing cursor** — the interface's only interactive element.
-
-### The Breathing Cycle
-
-```
-Reading → Typing → Idle → Reading
- ↓ ↓ ↓ ↓
-Hidden Visible Fading Hidden
-```
-
-| State | Cursor | Trigger |
-|-------|--------|---------|
-| **Reading** | Hidden | Default state. User is consuming output. |
-| **Active** | Visible at input position | User presses any key. Cursor appears at end of input. |
-| **Idle** | Fading (opacity transition) | No input for 2 seconds while in Active state. |
-| **Submit** | Visible | User presses Enter. Cursor stays visible during send. |
-| **Navigate** | Hidden | User presses Up/Down arrows for scroll. |
-
-### Implementation Strategy
-
-```jsx
-function InputBar() {
- const { setVisibility } = useCursor({ isVisible: false });
- const [input, setInput] = useState('');
- const lastActivity = useRef(Date.now());
-
- // Show cursor when user starts typing
- const handleInput = (key) => {
- setVisibility(true);
- lastActivity.current = Date.now();
- setInput(prev => prev + key);
- };
-
- // Hide cursor after 2s of idle
- useEffect(() => {
- const interval = setInterval(() => {
- if (Date.now() - lastActivity.current > 2000) {
- setVisibility(false);
- }
- }, 500);
- return () => clearInterval(interval);
- }, []);
-
- // Hide cursor on navigation keys
- const handleKey = (key) => {
- if (key === 'up' || key === 'down') {
- setVisibility(false);
- handleScroll(key);
- return;
- }
- handleInput(key);
- };
-
- return (
-
-
- {input}
- {cursorVisible && }
-
-
- );
-}
-```
-
-### Why This Matters
-
-A visible cursor during reading creates visual noise. The cleanest interface is one where the cursor is only present when you're about to contribute. The breathing model captures this naturally — the cursor is a living thing that appears when needed and rests when not.
-
----
-
-## 4. Virtual Scrolling
-
-Terminal virtualization is different from browser virtualization. We can't use DOM nodes — we render text rows. The strategy:
-
-### Windowed Rendering
-
-```
-Terminal height: 40 lines
-Header: 1 line
-Input: 1 line
-Status: 1 line
-Output area: 37 lines
-
-Visible window: 37 entries
-Buffer above: 10 entries (for smooth scroll back)
-```
-
-### Rendering Pipeline
-
-```
-1. Calculate visible range: [scrollOffset, scrollOffset + visibleRows]
-2. Render only entries in that range
-3. On new output:
- - If autoScroll is true: append to end, scrollOffset = total - visibleRows
- - If autoScroll is false: append to end, user sees nothing new until they scroll down
-4. On scroll up: autoScroll = false, user can review output
-5. On scroll to bottom: autoScroll = true, cursor hides
-```
-
-### Scroll Behavior
-
-| Action | Effect |
-|--------|--------|
-| New output arrives + at bottom | Auto-scroll, cursor hidden |
-| New output arrives + scrolled up | No scroll, cursor hidden |
-| User scrolls up | Auto-scroll off, cursor hidden |
-| User scrolls to bottom | Auto-scroll on, cursor hidden |
-| User presses Down arrow | Scroll down 1 line, cursor hidden |
-| User presses Up arrow | Scroll up 1 line, cursor hidden |
-| User presses PageDown | Scroll down 1 page, cursor hidden |
-| User presses PageUp | Scroll up 1 page, cursor hidden |
-| User starts typing | Cursor visible, scroll to bottom |
-
----
-
-## 5. Component Breakdown
-
-### 5.1 Layout
-
-**Responsibility:** Full terminal management, resize handling, overall structure.
-
-```jsx
-function Layout() {
- const { columns, rows } = useStdout();
- const [headerHeight] = useState(1);
- const [inputHeight] = useState(1);
- const [statusHeight] = useState(1);
- const outputAreaHeight = rows - headerHeight - inputHeight - statusHeight;
-
- return (
-
-
-
-
-
-
-
-
- );
-}
-```
-
-### 5.2 Header
-
-**Responsibility:** Connection status. Minimal, always visible.
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ ● connected │
-└─────────────────────────────────────────────────────────────┘
-```
-
-| Element | Position | Style |
-|---------|----------|-------|
-| Connection status | Left | Colored dot (green=connected, red=disconnected) |
-
-### 5.3 OutputList
-
-**Responsibility:** Virtualized output rendering, scroll management, auto-scroll logic.
-
-```jsx
-function OutputList({ height }) {
- const { entries, scrollOffset, autoScroll, setScrollOffset, setAutoScroll } = useApp();
- const visibleEntries = entries.slice(scrollOffset, scrollOffset + height);
-
- // Auto-scroll on new output when at bottom
- useEffect(() => {
- if (autoScroll) {
- setScrollOffset(Math.max(0, entries.length - height));
- }
- }, [entries.length]);
-
- return (
-
- {visibleEntries.map((entry, i) => (
-
- ))}
-
- );
-}
-```
-
-#### Output Row
-
-Each entry is a terminal row. Entries can be different types:
-
-```
-$ npm run build ← User command (echoed)
-> madz@1.0.0 build ← Command output
-> tsc --project tsconfig.json
-> Compiled successfully in 1.2s
-[14:23] System: Build complete ← System message
-[14:24] System: Running tests...
-```
-
-| Entry Type | Format | Style |
-|------------|--------|-------|
-| **User command** | `$ command` | Bold, no timestamp |
-| **Command output** | `> line` | Default, no timestamp |
-| **System message** | `[HH:MM] System: text` | Dim timestamp, italic body |
-| **Agent response** | `[HH:MM] Mads: text` | Dim timestamp, color-coded nickname |
-| **Error** | `[HH:MM] Error: text` | Dim timestamp, red text |
-
-### 5.4 InputBar
-
-**Responsibility:** Text input, command parsing, cursor management.
-
-```
-> _
-```
-
-| Element | Behavior |
-|---------|----------|
-| `>` prompt | Always visible, visual cue before input |
-| Input text | Grows to fill available width |
-| Cursor | Breathing model (see Section 3) |
-
-#### Command Handling
-
-Commands are parsed from input when Enter is pressed:
-
-| Command | Behavior |
-|---------|----------|
-| `/clear` | Clear output history |
-| `/help` | Show command reference |
-| `/quit` | Disconnect and exit |
-
-All other input is treated as commands to execute — shell commands, code, system commands. No slash prefix needed.
-
-### 5.5 StatusLine
-
-**Responsibility:** Contextual information, scroll position, mode indicators.
-
-```
-─── ↑ 127 lines above ─── [normal]
-```
-
-| Element | Content |
-|---------|---------|
-| Scroll position | Lines above fold (when scrolled up) |
-| Mode indicator | `[normal]`, `[command]`, `[search]` |
-
----
-
-## 6. Interaction Model
-
-### Keyboard Shortcuts
-
-| Key | Action | Cursor |
-|-----|--------|--------|
-| Any character | Append to input | Visible |
-| Enter | Submit command | Visible → Hidden (after submit) |
-| Escape | Clear input, hide cursor | Hidden |
-| Up arrow | Scroll up | Hidden |
-| Down arrow | Scroll down | Hidden |
-| PageDown | Scroll down 1 page | Hidden |
-| PageUp | Scroll up 1 page | Hidden |
-| Home | Scroll to top | Hidden |
-| End | Scroll to bottom, enable auto-scroll | Hidden |
-| Ctrl+C | Cancel input, hide cursor | Hidden |
-| Tab | Command autocomplete | Visible |
-
-### Command History
-
-The up/down arrows scroll through command history when the user is at the bottom of the output (not scrolling the output itself). This is a terminal convention:
-
-```
-1. User presses Enter → command executes, output appears, auto-scroll active
-2. User presses Up → scrolls through previous commands (not output)
-3. User presses Down → scrolls forward through command history
-4. User presses Up while scrolled up in output → scrolls output
-```
-
-### Input Lifecycle
-
-```
-1. User presses key → cursor appears, input focused
-2. User types → cursor follows text end
-3. User presses Enter → command executed, output appended, input cleared, cursor fades
-4. 2 seconds idle → cursor hidden
-5. User presses key again → cycle repeats
-```
-
----
-
-## 7. Rendering Pipeline
-
-### Update Cycle
-
-```
-1. State change detected (new output, input change, etc.)
-2. Re-render triggered by Ink
-3. Layout recalculates dimensions
-4. OutputList virtualizes visible window
-5. InputBar renders with cursor state
-6. Terminal updates only changed cells (Ink's diffing)
-```
-
-### Performance Considerations
-
-| Concern | Strategy |
-|---------|----------|
-| Large output history | Virtual scroll — only render visible rows |
-| Frequent re-renders | Ink's diffing engine — only update changed cells |
-| Terminal resize | Recalculate visible window, adjust scrollOffset |
-| High-frequency output | Batch renders — coalesce updates within 50ms |
-| Memory with long sessions | Trim old entries beyond retention limit |
-
----
-
-## 8. Visual Design
-
-### Color Palette
-
-```
-Background: Terminal default (respects user's theme)
-Text: Terminal default
-Timestamp: Dim (#555555 or 240)
-User command: Bold
-Command output: Default
-System: Italic, dim
-Error: Red
-Input: Inverse (white on dark) when cursor visible
-Header: Bold, subtle background
-Divider: ─── characters, dim
-```
-
-### Entry Styling
-
-| Entry Type | Color | Emphasis |
-|------------|-------|----------|
-| User command | Terminal default | Bold |
-| Command output | Terminal default | Normal |
-| System message | Terminal default | Italic |
-| Agent response | Color-coded | Bold nickname |
-| Error | Red | Normal |
-
-### Typography
-
-- **Font:** Terminal default monospace (no overrides)
-- **Timestamp:** Fixed-width, always 6 chars `[HH:MM]`
-- **Command output prefix:** `> ` (2 chars, aligned)
-- **Body:** Wraps to terminal width
-- **User commands:** Prefixed with `$ ` (2 chars)
-
-### Layout Spacing
-
-```
-Terminal width: N columns
-Header: 1 row, full width
-Output area: N-3 rows, full width
-Input: 1 row, full width
-Status: 1 row, full width
-
-Output padding: 2 chars left (indent)
-Command prefix: Variable, padded to align output
-Body start: After prefix + space
-```
-
----
-
-## 9. Edge Cases & Resilience
-
-### Terminal Resize
-
-```
-1. Detect resize via useStdout
-2. Recalculate visible window
-3. If previously at bottom: stay at bottom
-4. If scrolled up: preserve scrollOffset
-5. Re-render with new dimensions
-```
-
-### Connection Loss
-
-```
-1. Show red dot in header
-2. Display system message: "* Disconnected"
-3. Attempt reconnection (exponential backoff)
-4. On reconnect: system message "* Reconnected"
-5. Cursor behavior unchanged
-```
-
-### Multi-line Output
-
-```
-1. Command output longer than terminal width: wrap to multiple rows
-2. Track wrapped row count for accurate scroll calculation
-3. Virtual scroll accounts for wrapped entries
-4. User commands (echoed) are single-line
-```
-
-### Output Retention Limit
-
-```
-1. Retain last 1000 entries
-2. When limit reached: trim oldest entries
-3. If user scrolled up past trim point: show "output truncated" indicator
-```
-
----
-
-## 10. The Cursor: A Deeper Look
-
-The cursor is not just a UI element — it's the interface's personality. It is a quiet promise: *you can type now.* In this design, we make that promise explicit through the breathing model.
-
-### States
-
-```
-┌─────────────┐ ┌─────────────┐ ┌─────────────┐
-│ READING │────▶│ ACTIVE │────▶│ IDLE │
-│ (Hidden) │◀────│ (Visible) │◀────│ (Fading) │
-└─────────────┘ └─────────────┘ └─────────────┘
- ▲ │
- └────────────── Escape ────────────────┘
-```
-
-### Transition Rules
-
-| From | To | Trigger |
-|------|-----|---------|
-| Reading | Active | Any key press |
-| Active | Idle | 2s no input |
-| Idle | Active | Any key press |
-| Active | Reading | Enter (submit) |
-| Any | Reading | Escape |
-
-### The "Why"
-
-A always-visible cursor creates visual tension — the eye is drawn to it even when it's not needed. A hidden cursor during reading creates a clean canvas. The breathing model gives the interface a sense of presence without demanding attention. It's the difference between a terminal with a blinking cursor and one that waits patiently for input.
-
----
-
-## 11. Implementation Notes
-
-### Ink-Specific Patterns
-
-1. **`useStdout`** — For terminal dimensions and resize events
-2. **`useInput`** — For keyboard handling (replaces custom key listeners)
-3. **`useCursor`** — For cursor visibility control (the star of the show)
-4. **`Box` with `overflow="hidden"`** — For the output list viewport
-5. **`Text` with `wrap="never"`** — For output rows (handle wrapping manually)
-
-### Key Ink Patterns
-
-```jsx
-// Cursor visibility toggle
-const { setVisibility } = useCursor({ isVisible: false });
-setVisibility(true); // Show
-setVisibility(false); // Hide
-
-// Terminal dimensions
-const { columns, rows } = useStdout();
-
-// Input handling
-useInput((input, key) => {
- if (key.enter) handleSubmit();
- if (key.up) handleScroll('up');
- // ...
-});
-```
-
-### State Management Recommendation
-
-For this scale, React's built-in state is sufficient. Use `useReducer` for complex state transitions (scroll management, cursor lifecycle) and `useState` for simple values (input text, connection state).
-
-Consider a custom hook for the cursor breathing model:
-
-```typescript
-function useBreathingCursor() {
- const { setVisibility } = useCursor({ isVisible: false });
- const lastActivity = useRef(Date.now());
- const [visible, setVisible] = useState(false);
-
- const activate = useCallback(() => {
- setVisibility(true);
- setVisible(true);
- lastActivity.current = Date.now();
- }, [setVisibility]);
-
- const deactivate = useCallback(() => {
- setVisibility(false);
- setVisible(false);
- }, [setVisibility]);
-
- // Auto-deactivate after idle
- useEffect(() => {
- const interval = setInterval(() => {
- if (Date.now() - lastActivity.current > 2000) {
- deactivate();
- }
- }, 500);
- return () => clearInterval(interval);
- }, [deactivate]);
-
- return { activate, deactivate, visible };
-}
-```
-
----
-
-## 12. Summary
-
-This blueprint describes a code-first TUI built on Ink with `useCursor` at its core. The IRC layout is borrowed — scrollable output above, input at the bottom — but the content is commands, output, and system responses. The design is defined by three principles:
-
-1. **Input is primary.** The user lives at the input line. Output is secondary.
-2. **The cursor breathes.** It appears when you're about to act, disappears when you're reading.
-3. **The terminal is the window.** Virtual scrolling, clean rendering, respect for the user's environment.
-
-The result is an interface that feels like a quiet terminal — present, alive, and ready for work. Not a dashboard. Not a tool. A workspace.
-
----
-
-*Blueprint complete. No implementation references — this is a clean slate.*