Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
29c9025
feat(tui): propose TUI redesign with OpenSpec artifacts
avoidwork Jun 16, 2026
1084afb
feat(tui): add state management foundation
avoidwork Jun 16, 2026
2064596
feat(tui): add streaming and input hooks
avoidwork Jun 16, 2026
3a8e024
feat(tui): add event-driven command registry
avoidwork Jun 16, 2026
0841a5a
feat(tui): add runtime toggle system
avoidwork Jun 16, 2026
d819fb8
refactor(tui): rewrite app.js with useReducer, hooks, and new structure
avoidwork Jun 16, 2026
6ec42c6
refactor(tui): remove panel system entirely
avoidwork Jun 16, 2026
e0f7e51
refactor(tui): remove old flat files, complete directory reorganization
avoidwork Jun 16, 2026
632e9ff
test(tui): add comprehensive test suite for new TUI architecture
avoidwork Jun 16, 2026
62b82a6
fix: update test imports for new TUI file structure
avoidwork Jun 16, 2026
ae49663
fix: update test imports and fix syntax errors
avoidwork Jun 16, 2026
2886927
fix: update dynamic import paths in tui.test.js
avoidwork Jun 16, 2026
ce39bc3
fix: update InputPanel test props to match new API
avoidwork Jun 16, 2026
9386d32
fix: resolve all lint errors and test failures
avoidwork Jun 16, 2026
981e4bc
fix: format tests/unit/tui.test.js
avoidwork Jun 16, 2026
a292a11
fix: correct todo_queue import path in useStreaming.js
avoidwork Jun 16, 2026
fdeeff4
fix: import initialState from types.js, not reducer.js
avoidwork Jun 16, 2026
5f91b95
fix: setInputText now handles function arguments like React setState
avoidwork Jun 16, 2026
bffad78
Update ConversationPanel component
avoidwork Jun 17, 2026
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/refactor-tui-opentui/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-16
32 changes: 32 additions & 0 deletions openspec/changes/refactor-tui-opentui/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Why

The current TUI (`src/tui/app.js`) is a single monolithic file with eight independent `useState` calls, inline streaming logic, and a flat 17-file structure that doesn't scale. As the TUI grows, state coordination, streaming complexity, and file navigation all compound. The TUI.md blueprint documents the architectural debt and proposes a reorganization that groups code by concern, extracts streaming into its own hook, consolidates state into `useReducer`, and removes the panel system that contradicts the blueprint's philosophy.

## What Changes

- **State consolidation**: Replace eight `useState` calls with a single `useReducer` using a `TUIState` interface and typed actions
- **Streaming extraction**: Move streaming callback logic (AbortController, event transformation, auto-continue circuit breaker) into a dedicated `useStreaming()` hook
- **File structure reorganization**: Group files by concern into `state/`, `hooks/`, `components/`, `utils/` subdirectories
- **Panel system removal**: Remove `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js` — replace with commands (`/skills`, `/memory`) that produce output in the conversation stream
- **Command parser evolution**: Shift from switch-driven dispatch to event-driven command registry with `validate`, `execute`, `help` properties
- **Runtime toggles**: Implement built-in `/toggle` commands for `autoScroll`, `timestamps`, `commandEcho`, `cursorBreathe`, `debugOutput`
- **Status bar enhancement**: Add toggle/filter indicators to show active runtime features at a glance

## Capabilities

### New Capabilities
- `tui-state-management`: Consolidated `useReducer` state with typed actions, selectors, and a single `TUIState` interface
- `tui-streaming-hook`: Extracted streaming logic into `useStreaming()` hook with AbortController lifecycle, event transformation, and auto-continue circuit breaker
- `tui-command-registry`: Event-driven command registry with validate/execute/help schema replacing switch-driven dispatch
- `tui-runtime-toggles`: Built-in `/toggle` commands for runtime config overrides stored in state
- `tui-file-structure`: Organized file tree grouped by concern (state/, hooks/, components/, utils/)

### Modified Capabilities
- `cron-scheduler`: Status bar context size display may shift from inline calculation to selector-derived state

## Impact

- **Affected code**: `src/tui/app.js` (primary refactor target), `src/tui/panels.js`, `src/tui/skillsPanel.js`, `src/tui/memoryPanel.js`, `src/tui/settingsPanel.js`, `src/tui/commandParser.js`
- **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`
- **Removed files**: `src/tui/panels.js`, `src/tui/skillsPanel.js`, `src/tui/memoryPanel.js`, `src/tui/settingsPanel.js`
- **Breaking changes**: None for end users — all changes are internal reorganization. Commands (`/quit`, `/clear`, `/toggle`, etc.) remain the same.
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
88 changes: 88 additions & 0 deletions openspec/changes/tui-redesign/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
## Context

The TUI (`src/tui/`) is a 17-file flat directory with eight independent `useState` calls in `app.js`, inline streaming logic scattered across handlers, panel files that contradict the blueprint's "no panels" philosophy, and a switch-driven command parser that doesn't scale. The TUI works but structural debt compounds with every new feature. The blueprint (`docs/TUI.md`) defines four core tenets: input is primary, output is a log, silence is the default, and batteries are included.

Current state:
- `app.js`: 8 `useState` calls, inline streaming callback, mixed concerns
- `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js`: panel system contradicting blueprint
- `commandParser.js`: switch-driven dispatch table
- Flat file structure: no grouping by concern

## Goals / Non-Goals

**Goals:**
- Consolidate state into a single `useReducer` with typed actions and selectors
- Extract streaming logic into a dedicated `useStreaming()` hook
- Reorganize file structure into `state/`, `hooks/`, `components/`, `utils/` directories
- Remove the panel system entirely — replace with command-based output in the conversation stream
- Convert command parser to event-driven registry pattern
- Add runtime toggle commands (`/toggle`) with status bar indicators
- Add keyboard scrolling when input is unfocused

**Non-Goals:**
- Rewriting the markdown rendering pipeline
- Changing the structured logger (pino dual-file)
- Modifying the session management layer
- Adding format customization system (YAGNI per blueprint)
- Adding message-level filtering (YAGNI per blueprint)

## Decisions

### 1. `useReducer` over `useState`
**Decision**: Consolidate all TUI state into a single `useReducer` with a `TUIState` interface.
**Rationale**: Eight independent state calls cause multiple renders per meaningful change. A reducer ensures atomic updates and a single render cycle. The blueprint explicitly calls this out in Section 16.1.
**Alternatives considered**: `useReducer` with separate dispatch functions — rejected because it adds boilerplate without benefit over a single dispatch.

### 2. Extract streaming to `useStreaming()` hook
**Decision**: Move AbortController lifecycle, stream event transformation, and auto-continue circuit breaker into `useStreaming()`.
**Rationale**: The streaming callback is currently set up inline in `handleChat()` / `handleCommand()` and passed through multiple layers. A hook separates *how we stream* from *what we stream*.
**Alternatives considered**: Context provider — rejected because it adds unnecessary indirection for a single-consumer pattern.

### 3. File structure: group by concern
**Decision**: Create `state/`, `hooks/`, `components/`, `utils/` subdirectories.
**Rationale**: Predictability over dogma. When looking for streaming logic, you know exactly where to look. The blueprint Section 16.3 defines the target structure.
**Alternatives considered**: Keep flat — rejected because it doesn't scale and the blueprint explicitly calls this out.

### 4. Remove panel system
**Decision**: Delete `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js`. Replace with command-based output.
**Rationale**: The blueprint's core tenet is "no panels, no tabs, no switching." The panel system directly contradicts this. Commands like `/skills` and `/memory` produce output in the conversation stream.
**Alternatives considered**: Keep panels but make them optional — rejected because it adds complexity and the blueprint is explicit.

### 5. Event-driven command registry
**Decision**: Convert `commandParser.js` from switch-driven to event-driven registry.
**Rationale**: Adding a new command becomes a registration, not a switch case edit. More extensible and testable.
**Alternatives considered**: Keep switch-driven — rejected because it doesn't scale and the blueprint explicitly calls this out.

## Risks / Trade-offs

| Risk | Mitigation |
|------|-----------|
| Breaking existing tests due to state shape changes | Update tests alongside implementation; maintain backward-compatible action types |
| Regression in streaming behavior during hook extraction | Write dedicated `useStreaming.test.js` covering all event types, auto-continue, and abort |
| Panel removal breaks user expectations | Commands (`/skills`, `/memory`) produce equivalent output in the conversation stream |
| Reducer complexity grows with new features | Selectors keep derived state logic separate; reducer stays focused on mutations |
| File reorganization causes git blame noise | Commit reorganization and logic changes together so blame points to the final state |

## Migration Plan

1. Create new directory structure (`state/`, `hooks/`, `components/`, `utils/`)
2. Write `state/types.js` with `TUIState` and `TUIAction` interfaces
3. Write `state/reducer.js` with all action handlers
4. Write `state/selectors.js` with derived state functions
5. Write `hooks/useStreaming.js` with streaming logic
6. Write `hooks/useScroll.js` with scroll management
7. Write `hooks/useInput.js` with keyboard routing
8. Write `hooks/useCommand.js` with command parsing
9. Write `utils/commandParser.js` with registry pattern
10. Write `utils/format.js` with toggle logic
11. Rewrite `app.js` to use reducer, hooks, and new structure
12. Remove panel files
13. Update `index.js` entry point
14. Write tests for all new files
15. Run full test suite

## Open Questions

1. Should toggle overrides persist to `config.yaml` on exit, or only on explicit save?
2. Should the command registry support async commands that return promises?
3. Should the streaming hook expose a `streamingState` object or dispatch actions directly?
31 changes: 31 additions & 0 deletions openspec/changes/tui-redesign/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

The current TUI implementation works but suffers from structural debt that compounds as the interface grows. 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 blueprint's "no panels" philosophy. The TUI needs reorganization — not a rewrite, but a structural realignment with the blueprint's four core tenets: input is primary, output is a log, silence is the default, and batteries are included.

## What Changes

- **Consolidate state management**: Replace eight `useState` calls with a single `useReducer` using a `TUIState` interface and typed actions
- **Extract streaming logic**: Move streaming callback, AbortController lifecycle, and auto-continue circuit breaker into a dedicated `useStreaming()` hook
- **Reorganize file structure**: Group files by concern (`state/`, `hooks/`, `components/`, `utils/`) instead of flat layout
- **Remove panel system**: Eliminate `panels.js`, `skillsPanel.js`, `memoryPanel.js`, `settingsPanel.js` — replace with command-based output in the conversation stream
- **Refactor command parser**: Convert switch-driven dispatch table to event-driven command registry with `validate`, `execute`, and `help` properties
- **Add runtime toggles**: Built-in `/toggle` commands for `autoScroll`, `timestamps`, `commandEcho`, `cursorBreathe`, `debugOutput` — stored in memory, persisted via `config.yaml` on restart
- **Add status bar toggle indicators**: Show active toggle states at a glance (e.g., `[ts:1 scroll:1]`)

## Capabilities

### New Capabilities
- `tui-state-management`: Centralized `useReducer` with `TUIState` interface, typed actions, and selectors
- `tui-streaming-hook`: Extracted `useStreaming()` hook managing AbortController, event transformation, and auto-continue circuit breaker
- `tui-command-registry`: Event-driven command registry replacing switch-driven dispatch table
- `tui-runtime-toggles`: Built-in `/toggle` commands for runtime config overrides with status bar indicators

### Modified Capabilities
- `tui-scrolling`: Enhanced with keyboard scrolling (up/down/page up/page down) when input is unfocused
- `tui-status-bar`: Extended with toggle/filter indicators

## Impact

- **Affected code**: `src/tui/app.js` (major refactor), `src/tui/panels.js`, `src/tui/skillsPanel.js`, `src/tui/memoryPanel.js`, `src/tui/settingsPanel.js` (removal), `src/tui/commandParser.js` (rewrite), `src/tui/index.js` (entry point adjustments)
- **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/utils/format.js`
- **Tests**: New test files under `tests/unit/tui/` for reducer, command parser, context tokens, markdown rendering, and streaming hook
56 changes: 56 additions & 0 deletions openspec/changes/tui-redesign/specs/tui-command-registry/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## ADDED Requirements

### Requirement: Event-Driven Command Registry
The TUI SHALL replace the switch-driven command parser with an event-driven command registry where commands are registered as objects with `validate`, `execute`, and `help` properties.

#### Scenario: Commands are registered as objects
- **WHEN** the command registry is initialized
- **THEN** each command is a `{ name, description, usage, validate, execute }` object stored in a `Record<string, Command>`

#### Scenario: Command validation runs before execution
- **WHEN** a command is dispatched
- **THEN** the `validate` function runs first and returns an error message on failure
- **WHEN** validation fails
- **THEN** the command is not executed and the error message is displayed to the user

#### Scenario: Unknown commands produce a helpful response
- **WHEN** a user types an unrecognized `/command`
- **THEN** the system responds with "Unknown command: /command. Type /help for available commands."

#### Scenario: Unrecognized skill patterns execute as skills
- **WHEN** a user types `/skillName [args]` where `skillName` matches a registered skill
- **THEN** the skill body is loaded and streamed to the agent as a prompt

### Requirement: Registered Commands
The TUI SHALL support the following commands via the registry:

| Command | Behavior |
|---------|----------|
| `/quit` | Disconnect and exit |
| `/clear` | Clear conversation |
| `/new` | Start a new session |
| `/help` | Show available commands |
| `/config set <path> <value>` | Set a config value |
| `/provider set <name>` | Switch AI provider |
| `/schedule list` | List scheduled tasks |
| `/schedule pause <name>` | Pause a scheduled task |
| `/schedule resume <name>` | Resume a scheduled task |
| `/schedule run-now <name>` | Run a scheduled task immediately |
| `/gc` | Trigger V8 garbage collection |
| `/gc status` | Show GC status |

#### Scenario: /quit exits the application
- **WHEN** the user types `/quit`
- **THEN** the application disconnects and exits

#### Scenario: /clear removes all messages
- **WHEN** the user types `/clear`
- **THEN** the conversation panel is cleared and messages array is reset to empty

#### Scenario: /help shows available commands
- **WHEN** the user types `/help`
- **THEN** the system displays a grouped list of available commands with descriptions

#### Scenario: /config set updates configuration
- **WHEN** the user types `/config set tui.autoScroll false`
- **THEN** the configuration value is updated and takes effect immediately
21 changes: 21 additions & 0 deletions openspec/changes/tui-redesign/specs/tui-interface/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## REMOVED Requirements

### Requirement: Keyboard Navigation (panel-based)
The system SHALL support panel-based keyboard navigation using Tab, Shift+Tab, and arrow keys to switch between the conversation, memory, skills, and settings panels.
**Reason**: The panel system is removed entirely per the blueprint's core tenet "no panels, no tabs, no switching." Panel navigation is replaced with command-based output in the conversation stream.
**Migration**: Users who need to inspect skills or memory should use `/skills` and `/memory` commands, which produce output in the conversation stream.

### Requirement: TUI Command Entry (colon syntax)
The system SHALL allow users to issue commands via a slash-syntax (`:command`) input mode for system control.
**Reason**: The blueprint specifies slash-syntax (`/command`) as the standard. The colon syntax is replaced by the new command registry.
**Migration**: Commands previously entered as `:provider set openai` should now be entered as `/provider set openai`.

## MODIFIED Requirements

### Requirement: Startup Banner Display
The system SHALL display a BBS-style startup banner with ASCII art and a built-in command help menu when the TUI enters interactive mode. The banner SHALL also display the application version string below the ASCII art.
**Changes**: The banner now dismisses on Escape key press (which exits the app) in addition to any other key press.

#### Scenario: Banner dismisses on Escape key
- **WHEN** the banner is displayed and the user presses Escape
- **THEN** the system exits the application entirely
58 changes: 58 additions & 0 deletions openspec/changes/tui-redesign/specs/tui-runtime-toggles/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## ADDED Requirements

### Requirement: Runtime Toggle Commands
The TUI SHALL provide built-in `/toggle` commands for runtime overrides of TUI configuration defaults, stored in memory and persisted via `config.yaml` on restart.

#### Scenario: /toggle without arguments shows all toggles
- **WHEN** the user types `/toggle`
- **THEN** the system displays all toggle names and their current states

#### Scenario: /toggle <key> toggles a setting
- **WHEN** the user types `/toggle timestamps`
- **THEN** the `timestamps` toggle flips from `true` to `false` (or vice versa)
- **WHEN** the user types `/toggle timestamps` again
- **THEN** the `timestamps` toggle flips back to `true`

#### Scenario: Toggle overrides are in-memory only
- **WHEN** the TUI restarts
- **THEN** toggle overrides revert to `config.yaml` defaults

### Requirement: Available Toggles
The TUI SHALL support the following runtime toggles with these 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 |

#### Scenario: autoScroll toggle controls scroll behavior
- **WHEN** `autoScroll` is `true` and a new message arrives
- **THEN** the conversation scrolls to the bottom
- **WHEN** `autoScroll` is `false` and a new message arrives
- **THEN** the conversation does not auto-scroll

#### Scenario: timestamps toggle controls timestamp display
- **WHEN** `timestamps` is `true`
- **THEN** messages display `[HH:MM]` timestamps
- **WHEN** `timestamps` is `false`
- **THEN** messages do not display timestamps

#### Scenario: commandEcho toggle controls command display
- **WHEN** `commandEcho` is `true` and the user submits a command
- **THEN** the command is echoed to the output area
- **WHEN** `commandEcho` is `false` and the user submits a command
- **THEN** the command is not echoed to the output area

### Requirement: Status Bar Toggle Indicators
The TUI SHALL display active toggle states in the status bar for quick visual reference.

#### Scenario: Toggle indicators appear in status bar
- **WHEN** the status bar renders with active toggles
- **THEN** it displays toggle states such as `[ts:1 scroll:1]` alongside existing metrics

#### Scenario: Toggle indicators update on change
- **WHEN** the user toggles a setting via `/toggle`
- **THEN** the status bar indicators update immediately
Loading