Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions openspec/changes/tui-redesign/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-16
135 changes: 135 additions & 0 deletions openspec/changes/tui-redesign/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
## Context

The TUI (`src/tui/`) is the primary user-facing interface for the madz agent. It's built on Ink + ink-scroll-view with a structured pino logger. The current implementation works but has accumulated structural debt:

- **State sprawl**: Eight independent `useState` calls in `app.js` with no coordination. A single message arrival triggers four separate state updates across four renders.
- **Inline streaming**: The streaming callback is set up inline in `handleChat()` / `handleCommand()` and passed through multiple layers, mixing streaming concerns with command handling.
- **Flat file layout**: 17 files at the root of `src/tui/` — no grouping by concern, making navigation and onboarding difficult.
- **Panel contradiction**: `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js` exist despite the blueprint's core tenet: "No panels, no tabs, no switching."
- **Switch-driven commands**: `commandParser.js` uses a dispatch table with switch-case logic — adding commands requires editing the switch, not registering a new handler.

The TUI uses no external state management library — just React's built-in `useState` and `useRef`. This is sufficient for the current scale but doesn't scale well.

## Goals / Non-Goals

**Goals:**
- Consolidate all TUI state into a single `useReducer` with a `TUIState` interface and typed action types
- Extract streaming logic into a dedicated `useStreaming()` hook with clean state transitions
- Restructure `src/tui/` into a concern-based hierarchy (`state/`, `hooks/`, `components/`, `panels/`, `utils/`)
- Remove the panel system entirely — replace with commands that produce output in the conversation stream
- Refactor command parser to an event-driven registry pattern
- Implement bitchx-inspired `/toggle` runtime configuration system
- Add toggle/filter indicators to the status bar

**Non-Goals:**
- Changing the core philosophy (input is primary, silence is default, etc.)
- Adding new external dependencies
- Changing the streaming pipeline (LangGraph → dispatchProvider → streamingCallback)
- Modifying the session management layer (compaction, trimming, persistence)
- Changing the message bubble rendering (markdown, role colors, tool call display)
- Adding new commands beyond the existing set + `/toggle`

## Decisions

### 1. `useReducer` over `useState`

**Decision**: Consolidate all TUI state into a single `useReducer` with a `TUIState` interface.

**Rationale**: Eight independent `useState` calls create uncoordinated renders. When a message arrives, `messages`, `statusMessage`, `contextSize`, and `chatHistory` all update separately. A single reducer ensures one render cycle per meaningful state change and provides a clear action taxonomy.

**Alternatives considered**:
- `zustand` or `jotai` — adds external dependency for what is essentially local component state
- `useReducer` with separate reducers per domain — over-engineered for a single component tree

### 2. `useStreaming()` hook

**Decision**: Extract all streaming logic into a dedicated hook.

**Rationale**: The streaming callback currently lives inline in `handleChat()` / `handleCommand()`, mixing streaming concerns with command dispatch. A hook encapsulates:
- AbortController lifecycle management
- Stream event → state transition transformation
- Auto-continue circuit breaker logic
- A clean `streamingState` object exposed to the UI

**Alternatives considered**:
- Service class — overkill for what is fundamentally React state management
- Context provider — adds unnecessary indirection for a single-component concern

### 3. File restructuring by concern

**Decision**: Group files into `state/`, `hooks/`, `components/`, `panels/`, `utils/`.

**Rationale**: Predictability. When looking for streaming logic, you know exactly where to look. The current flat layout works for 17 files but doesn't scale.

**Structure**:
```
src/tui/
├── app.js # Root component, providers, reducer
├── state/ # TUIState, actions, reducer, selectors
├── hooks/ # useStreaming, useScroll, useInput, useCommand
├── components/ # ConversationPanel, InputPanel, StatusBar, MessageBubble, Banner
├── panels/ # OnboardingPanel (only panel that remains)
├── utils/ # commandParser, contextTokens, markdownText, format
└── index.js # Entry point
```

### 4. Panel removal

**Decision**: Remove `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js`. Replace with commands.

**Rationale**: The blueprint's philosophy is explicit: "No panels, no tabs, no switching." The panel system contradicts this. Skills and memory are inspected via `/skills` and `/memory` commands that produce output in the conversation stream — the natural TUI paradigm.

**OnboardingPanel exception**: This is not a "panel" in the navigation sense — it's a one-time first-run flow. It stays in `panels/` as the sole remaining panel.

### 5. Command registry pattern

**Decision**: Refactor `commandParser.js` to an event-driven registry.

**Rationale**: A switch-driven dispatch table requires editing the switch for every new command. A registry where commands are objects with `validate`, `execute`, and `help` properties makes adding commands a registration, not a code edit.

**Schema**:
```typescript
interface Command {
name: string;
description: string;
usage: string;
validate: (args: string[]) => boolean | string;
execute: (args, state, dispatch, helpers) => Promise<void> | void;
}
```

### 6. Runtime toggles in-memory only

**Decision**: Toggle overrides stored in memory. `config.yaml` `tui` section is the source of truth on restart.

**Rationale**: Simpler than persisting to a separate file. The blueprint explicitly states: "No separate `tui-config.json` file is needed."

## Risks / Trade-offs

| Risk | Mitigation |
|------|-----------|
| Breaking existing tests | All 1129 existing tests must pass. New tests cover all refactored files. |
| Regression in streaming behavior | `useStreaming()` hook is fully unit-testable in isolation. |
| Panel removal breaks user workflows | Panels were unused per the blueprint philosophy. Commands provide equivalent functionality. |
| `useReducer` adds complexity for simple state | The reducer is generated from the existing `useState` calls — the migration is mechanical, not conceptual. |
| File restructuring is a large diff | Each file is moved/created independently. The diff is large but each change is focused. |

## Migration Plan

1. **Phase 1**: Create new file structure (`state/`, `hooks/`, `components/`, `utils/`)
2. **Phase 2**: Migrate state from `useState` to `useReducer` in a new `app.js`
3. **Phase 3**: Extract streaming into `useStreaming()` hook
4. **Phase 4**: Move components to `components/` directory
5. **Phase 5**: Refactor command parser to registry pattern
6. **Phase 6**: Implement `/toggle` command and runtime toggle system
7. **Phase 7**: Remove panel files, add `/skills` and `/memory` commands
8. **Phase 8**: Update status bar with toggle indicators
9. **Phase 9**: Run full test suite, fix any regressions

**Rollback**: The change is entirely within `src/tui/`. If regressions occur, revert the TUI directory and the rest of the system is unaffected.

## Open Questions

1. Should the `/toggle` command support `/toggle <key> <value>` (set to specific value) in addition to `/toggle <key>` (toggle)?
2. Should format specifiers (`/format system "[%T] %BSystem%n: %I%t%n"`) be implemented, or deferred per the YAGNI note in the blueprint?
3. Should message-level filtering (`/level debug`) be implemented, or deferred per the YAGNI note?
29 changes: 29 additions & 0 deletions openspec/changes/tui-redesign/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Why

The current TUI works but suffers from structural debt: eight independent `useState` calls with no coordination, inline streaming logic scattered across handlers, a flat 17-file layout that doesn't scale, and panel files that contradict the core philosophy of "no panels, no tabs, no switching." The interface needs a clean architectural reorganization — consolidating state, extracting streaming into its own hook, grouping files by concern, and removing the panel system entirely — while preserving the four core principles that make the TUI feel right: input is primary, silence is the default, batteries included, and the terminal is the window.

## What Changes

- **State consolidation**: Replace eight independent `useState` calls with a single `useReducer` backed by a `TUIState` interface and typed action types
- **Streaming extraction**: Extract streaming logic (AbortController lifecycle, event transformation, auto-continue circuit breaker) into a dedicated `useStreaming()` hook
- **File reorganization**: Restructure `src/tui/` from a flat layout into a concern-based hierarchy (`state/`, `hooks/`, `components/`, `panels/`, `utils/`)
- **Panel removal**: Remove `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js` — replace with commands (`/skills`, `/memory`) that produce output in the conversation stream
- **Command registry**: Refactor `commandParser.js` from a switch-driven dispatch table to an event-driven command registry with `validate`, `execute`, and `help` properties
- **Runtime toggles**: Implement bitchx-inspired `/toggle` commands for runtime config overrides (autoScroll, timestamps, commandEcho, cursorBreathe, debugOutput)
- **Status bar indicators**: Add toggle/filter status indicators to the status bar for quick glance visibility

## Capabilities

### New Capabilities

- `tui-redesign`: Complete TUI architectural reorganization — state consolidation via `useReducer`, streaming extraction via `useStreaming` hook, file restructuring by concern, panel removal, command registry refactor, and runtime toggle system

### Modified Capabilities

<!-- None — this is a new capability, not a modification of existing spec requirements -->

## Impact

- **Affected code**: `src/tui/app.js` (state management, streaming logic), `src/tui/commandParser.js` (command dispatch), `src/tui/panels.js` (removal), `src/tui/skillsPanel.js` (removal), `src/tui/memoryPanel.js` (removal), `src/tui/settingsPanel.js` (removal)
- **New files**: `src/tui/state/reducer.js`, `src/tui/state/types.js`, `src/tui/state/selectors.js`, `src/tui/hooks/useStreaming.js`, `src/tui/hooks/useScroll.js`, `src/tui/hooks/useInput.js`, `src/tui/hooks/useCommand.js`, `src/tui/components/ConversationPanel.js`, `src/tui/components/InputPanel.js`, `src/tui/components/StatusBar.js`, `src/tui/components/MessageBubble.js`, `src/tui/components/Banner.js`, `src/tui/utils/format.js`
- **Dependencies**: No new external dependencies — uses existing `ink`, `ink-scroll-view`, `pino`, `marked`, `marked-terminal`, `tiktoken`
Loading