Skip to content

fix(tui): Reset committed_line_count on markdown line count regression#353

Closed
theahura wants to merge 91 commits intodevfrom
auto/tui-text-rendering-freezes-stalls-bug-20260225-033452
Closed

fix(tui): Reset committed_line_count on markdown line count regression#353
theahura wants to merge 91 commits intodevfrom
auto/tui-text-rendering-freezes-stalls-bug-20260225-033452

Conversation

@theahura
Copy link
Copy Markdown
Contributor

Summary

🤖 Generated with Nori

  • Fix streaming text freeze when pulldown-cmark retroactively reclassifies markdown content (e.g., partial [foo text later becomes a link reference definition producing 0 rendered lines)
  • Reset committed_line_count to the current render count when a line count regression is detected, preventing the counter from permanently blocking new output
  • Add 3 regression tests covering the link ref def reclassification, empty code fence, and finalize-after-regression scenarios

Test Plan

  • All 920 nori-tui tests pass (including 3 new regression tests)
  • just fix -p nori-tui passes with no clippy warnings
  • just fmt passes cleanly
  • Manual testing: verify streaming text continues after markdown reclassification events

Share Nori with your team: https://www.npmjs.com/package/nori-ai

theahura and others added 30 commits October 31, 2025 12:52
Created a basic Rust binary package that prints Hello, world!
Includes standard cargo project structure with Cargo.toml and src/main.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Adds automated checks for:
- Code formatting (cargo fmt --check)
- Linting (cargo clippy -D warnings)
- Tests (cargo test --verbose)

Runs on push to main and all pull requests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- pr-ci.yml runs on pull_request events only
- main-ci.yml runs on push to main only
- Removes unified rust-ci.yml workflow

This allows future differentiation between PR checks (fast feedback)
and main branch checks (comprehensive validation + deployment)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
## Summary
- Converted nori-cli from simple println to ratatui-based TUI
- Added dependencies: ratatui 0.29.0, crossterm 0.28.1, color-eyre 0.6.3
- Implemented three-phase pattern: init → event loop → restore
- Displays stylized "Hello, World!" with green/bold and cyan/italic text

## Test Plan
- [x] cargo build succeeds
- [x] cargo test passes (0 tests)
- [x] cargo fmt passes
- [x] cargo clippy passes with -D warnings
- [ ] Manual test: Run application in terminal to verify TUI displays
correctly
- [ ] Verify application exits cleanly on key press

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Implement a terminal user interface (TUI) that routes user prompts to
either Claude Code or GPT Codex CLI implementations as subprocesses,
streaming their responses back in real-time.

**Key Changes:**
- Built TUI with ratatui using The Elm Architecture (TEA) pattern for
state management
- Implemented async subprocess spawning with `tokio::process::Command`
- Added JSONL parsing for streaming events (agent_message, file_change,
command_execution)
- Created trait-based backend abstraction for easy extensibility
- Integrated tui-textarea for multi-line prompt input

**Architecture:**
- **Backends**: Spawn `claude --print --output-format stream-json` or
`codex exec --json` as child processes
- **Event Loop**: `tokio::select!` multiplexes keyboard events,
subprocess stdout, and rendering
- **State Machine**: Clean state transitions between Selection → Input →
Streaming modes

## Test Plan

- [x] All unit tests pass (state machine, subprocess spawning, JSON
parsing)
- [x] `cargo fmt` applied
- [x] `cargo clippy` passes with `-D warnings`
- [x] Manual testing with mock backend (printf JSONL output)
- [ ] Manual testing with real Claude Code CLI (requires API key)
- [ ] Manual testing with real Codex CLI (requires API key)

## Technical Details

**Files Added:**
- `src/app.rs` - TEA state machine (Model, Message, update)
- `src/ui.rs` - Rendering for 3 modes (selection, input, streaming)
- `src/backends/` - AgentBackend trait + implementations (claude, codex,
mock)
- `tests/` - State machine and subprocess integration tests
- `README.md` - Comprehensive usage and architecture documentation

**Dependencies:**
- ratatui 0.29, tokio (full), tui-textarea 0.7, serde_json, crossterm
with event-stream

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Fixed a race condition where the event handler task maintained its own
local copy of `current_mode` that would become desynchronized with the
actual Model state.

- Event handler now receives mode updates via dedicated channel from
main loop
- Establishes `Model.current_mode` as single source of truth
- Prevents key events from being filtered based on stale state

## What Was Broken

After submitting a prompt and receiving a streamed response:
- User would return to the Selection screen
- Arrow keys and other input would not work
- Only 'q' for quit would respond

## Root Cause

The event handler's local `current_mode` variable would update
immediately when messages were sent, but the actual `Model.current_mode`
wouldn't update until the message was processed in the main loop. This
race condition meant the event handler was filtering key events based on
outdated state.

## The Fix

Created a bidirectional communication pattern:
- Added `mode_tx`/`mode_rx` channel for mode synchronization
- Main loop sends current mode to event handler after each state change
- Event handler updates its local mode only from the channel
- No more local state inference from messages

## Test Plan

- [x] Added `test_post_stream_state_handling` test to verify state
transitions
- [x] All existing tests pass (5 tests)
- [x] Manual testing: Selected agent → submitted prompt → stream
completed → verified arrow keys work correctly
- [x] Cargo fmt and clippy pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Transformed JSONL streaming output into natural, styled chat interface
by introducing an intermediate conversation type that separates
backend-specific parsing from display rendering.

- Created `ConversationEvent` enum to represent different event types
(assistant messages, system events, results, stderr)
- Implemented `parse_jsonl_event()` to parse actual Claude CLI JSONL
format
- Implemented `render_event()` to convert events into styled Ratatui
Lines with visual distinction
- Refactored data flow: Backend JSONL → ConversationEvent (parsing) →
Line (rendering) → UI
- Updated all tests and added comprehensive edge case coverage

## Test Plan

- [x] All 17 tests pass (12 conversation rendering tests + 3 state
machine + 2 subprocess)
- [x] cargo fmt and clippy pass with no issues
- [x] Verified actual Claude CLI event format through testing
- [x] Manual smoke test confirms build succeeds

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Refactor the TUI from a three-screen mode-based interface to a
chat-style interface where conversation history is always visible and
the agent router is accessed via an overlay.

### Key Changes

- **Chat-style UI**: Single view with messages above and input at bottom
  - Title shows currently selected agent
  - Messages display full conversation history (user + assistant)
  - Input field (tui-textarea) always at bottom
  - Instructions adapt to context

- **Agent Router Overlay**: Toggle with Alt+A
  - Renders centered overlay using Clear widget
  - Navigate with arrow keys, Enter to select, Esc to close
  - Does not disrupt conversation flow

- **Conversation History**: Preserved across interactions
  - Added `UserMessage` event type with cyan `[user]` prefix
  - Messages accumulate in `response_events`
  - Terminal scrolling handles long conversations

### Implementation

- Added `show_agent_router` boolean to track overlay visibility
- Refactored state management: navigation/input now gate on overlay
state
- Rewrote rendering: `render_chat()` base +
`render_agent_router_overlay()`
- Updated all tests to reflect new architecture (14 tests passing)

## Test Plan

- [x] All 14 tests passing (7 conversation + 5 state machine + 2
subprocess)
- [x] cargo fmt and cargo clippy clean
- [ ] Manual testing of chat flow
- [ ] Manual testing of overlay toggle (Alt+A)
- [ ] Manual testing of agent selection
- [ ] Verify conversation history persists
- [ ] Verify terminal scrolling works for long conversations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Replace keyboard shortcuts (Alt+A, q) with an extensible slash command
system that uses a trait-based registry pattern for command routing.

- Add `/exit` command to quit the application
- Add `/switch-model` command to open the agent router overlay
- Remove Alt+A and 'q' keyboard shortcuts
- Create CommandHandler trait and CommandRegistry for extensibility
- Support command parsing with error messages for unknown commands

## Architecture

The slash command system uses:

- **CommandHandler trait**: Standard interface for all commands
- **CommandRegistry**: HashMap-based routing with auto-registration
- **Individual command files**: exit.rs, switch_model.rs for modularity
- **Parse function**: Detects "/" prefix and extracts command names

Adding new commands requires implementing CommandHandler and registering
in CommandRegistry::default().

## Test Plan

- [x] All 22 existing tests pass
- [x] 8 new tests for slash command parsing and execution
- [x] Commands execute successfully (exit, switch-model)
- [x] Unknown commands show helpful error messages
- [x] Regular input (non-commands) works as before
- [x] cargo fmt and cargo clippy pass with -D warnings

## Files Changed

- `src/commands/mod.rs` - CommandHandler trait, registry, parsing
- `src/commands/exit.rs` - /exit implementation
- `src/commands/switch_model.rs` - /switch-model implementation
- `src/main.rs` - Integration with main loop
- `src/ui.rs` - Updated instructions
- `tests/slash_commands_test.rs` - Comprehensive test coverage
- `README.md`, `src/commands/docs.md` - Documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Implements backend installation checking and user prompts when backends
are missing. When a user attempts to use an unavailable backend (claude,
codex), the system detects this and guides them through installation.

## Implementation Details

### Core Features
- **Backend availability detection**: Uses `which` crate to check if
commands exist in PATH
- **Proactive checking**: Validates backend availability before
attempting to spawn
- **User prompts**: Modal overlay with options to open installation page
or cancel
- **Visual indication**: Agent router shows "[Not Installed]" for
unavailable backends
- **Improved error handling**: Distinguishes NotFound errors from other
spawn failures

### Architecture Changes
- Extended `AgentBackend` trait with `command_name()` and
`install_url()` methods
- Added `is_available()` function for PATH checking
- New state in Model: `show_install_prompt`, `install_prompt_backend`,
`install_prompt_url`, `install_prompt_choice`
- New messages: `ShowInstallPrompt`, `NavigateInstallChoice`,
`ConfirmInstall`, `CancelInstall`
- Install prompt overlay rendered on top of all other UI elements

### User Experience
1. User selects backend in agent router
2. Unavailable backends display with "[Not Installed]" suffix in dark
gray
3. On prompt submission, system checks if backend is available
4. If not: install prompt appears with backend name and options
5. User can navigate with arrow keys, confirm with Enter, cancel with
Esc
6. "Open Installation Page" opens the install URL in default browser via
`opener` crate

### Dependencies Added
- `which` 7.0: Cross-platform PATH lookup
- `opener` 0.7: Open URLs in default browser

## Test Plan

All 19 tests passing:
- ✅ Backend availability detection (installed/missing commands)
- ✅ Backend metadata (command_name, install_url)
- ✅ Install prompt state transitions (show, navigate, confirm, cancel)
- ✅ Existing functionality (streaming, state machine, rendering)

Manually tested:
- Install prompt displays correctly with proper styling
- Arrow keys navigate between options
- Enter opens browser when "Open Installation Page" selected
- Esc cancels prompt
- Unavailable backends show visual indication in agent router

## Breaking Changes

None. This is a purely additive feature that enhances the existing UX
when backends are missing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Implements user-initiated stream cancellation via Escape key during
streaming mode. When the user presses Escape while a backend subprocess
is streaming, the stream is cancelled, the process is cleaned up, and
"Interrupted" displays in red in the conversation history.

- Add CancellationToken-based cancellation coordinated between UI and
streaming task
- Update AgentBackend trait to accept cancellation token parameter
- Implement tokio::select! multiplexing in spawn_and_stream for
cancellation
- Add CancelStream message and StreamCancelled event with red
"Interrupted" rendering
- Track active streams via Model.current_stream_token field
- Update all backends (Claude, Codex, Mock) to support cancellation
- Add comprehensive test coverage for cancellation behavior

Closes the limitation documented in docs: "Process cancellation not
implemented - subprocess continues running even if user presses Esc"

## Test Plan

- [x] Unit tests pass (28 tests total, including new
test_cancel_stream_during_streaming)
- [x] cargo fmt applied successfully
- [x] cargo clippy passes with no warnings
- [ ] Manual testing: Start stream, press Escape, verify "Interrupted"
appears and process terminates
- [ ] Manual testing: Verify subprocess is killed (ps aux | grep claude
after cancellation)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Fixed cursor positioning issue when exiting the TUI with `/exit`
command. The cursor now appears at the bottom of the painted TUI area
instead of remaining in the middle.

**Changes:**
- Added `MoveToNextLine(1)` cursor command in terminal cleanup sequence
- Cursor moves down one line before `disable_raw_mode()` is called
- Shell prompt now appears cleanly below TUI content

## Test Plan

- [x] All 28 existing automated tests pass
- [ ] Manual test: Run `cargo run`, type `/exit`, verify cursor at
bottom
- [ ] Manual test: Verify shell prompt appears below TUI content, not in
middle
- [x] cargo fmt passes
- [x] cargo clippy passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Added text wrapping for long conversation event lines in terminal
scrollback.

- Implements word-level wrapping with character-level fallback for very
long words (JSON, URLs)
- Preserves span styling across wrapped lines
- Uses unicode-width for accurate multi-byte character width calculation

## Changes

- `src/main.rs`: Added `wrap_text_to_width()` function with word and
character-level wrapping logic
- `src/main.rs`: Modified `StreamEvent` handling to wrap before
`insert_before()`
- `Cargo.toml`: Added `unicode-width = "0.2"` dependency

## Technical Details

The solution wraps text manually before calling `insert_before()`
because:
- `insert_before()` only supports single-line insertion
- Ratatui's `Paragraph::wrap()` applies during render, after
`insert_before()` captures
- This ensures no text is lost and all content wraps properly within
terminal width

## Test Plan

- [x] All 28 existing tests pass
- [x] Manual testing with long responses confirms proper wrapping
- [x] Long JSON strings (`[unknown]` events) wrap character-by-character
- [x] Regular text wraps at word boundaries
- [x] No compiler warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Clear the prompt textarea immediately when the user submits a message,
providing instant visual feedback and enabling immediate composition of
follow-up messages. This aligns with modern chat UX patterns.

- Textarea clears immediately in `SubmitInput` handler after capturing
user text
- Removed redundant clearing from `StreamComplete` and `CancelStream`
handlers
- Added comprehensive test suite (6 new tests) covering immediate
clearing behavior and edge cases
- Updated existing test to properly set up state via `SubmitInput`
instead of manual manipulation

## Test Plan

- [x] All 34 tests pass (including 6 new textarea clearing tests)
- [x] cargo fmt (no changes needed)
- [x] cargo clippy (no warnings)
- [x] Manual testing: textarea clears immediately on submit, can type
follow-up while streaming
- [ ] Verify CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Fixed bug where user messages disappeared after submission instead of
appearing in conversation history
- User messages are now rendered to terminal scrollback using
`insert_before()` following the same pattern as StreamEvent messages
- Updated documentation to reflect the rendering flow

## Test Plan
- [x] All 34 existing tests pass
- [x] rustfmt applied
- [x] clippy passed with no warnings
- [x] Manual testing: user messages now appear in scrollback with [user]
prefix before agent responses

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Implement two-stage Ctrl-C keyboard interrupt handling for quick
textarea clearing and application exit.

- First Ctrl-C press clears textarea and displays hint message
- Second Ctrl-C within 2 seconds exits application (like /exit)
- After timeout, behavior resets to first-press pattern
- Works globally - even with overlays or install prompts open

## Implementation Details

- Added `Message::ClearTextarea` variant and `last_ctrl_c_time` state
field
- Timeout logic in `Model::update()` distinguishes first vs second press
- State synced to event handler via mpsc channel
- Ctrl-C detection prioritized before all other key handling
- Quit triggered by monitoring timestamp transition Some → None
- Visual feedback via existing `error_message` field

## Test Plan

- [x] All 37 existing tests pass
- [x] 3 new unit tests for Ctrl-C behavior:
  - First press clears textarea and shows hint
  - Second press within timeout signals quit
  - Press after timeout resets to first press
- [x] cargo fmt applied
- [x] cargo clippy passes with no warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Fixed cursor positioning on exit that was broken by recent text wrapping
and user message scrollback features. The cursor now appears cleanly
below the TUI content instead of in the middle or at the text area.

**Root cause:** Multiple `insert_before()` calls from text wrapping
(f7a191e) and user message scrollback (624511d) caused unpredictable
cursor positions. The previous relative cursor movement approach
(`MoveToNextLine(8)`) no longer worked correctly because
`ratatui::restore()` was resetting the cursor to the last draw position.

**Solution:** Switched to absolute cursor positioning by:
- Capturing viewport starting position before terminal initialization
- Using `MoveTo(0, viewport_start + 8)` for absolute positioning after
restore
- Ensures cursor is always exactly 8 lines below viewport start,
regardless of rendering state

## Changes
- `src/main.rs:4`: Import `MoveTo` instead of `MoveToNextLine`
- `src/main.rs:20`: Capture `viewport_start_row` before creating
terminal
- `src/main.rs:36`: Use absolute positioning with `MoveTo(0,
viewport_start_row + 8)`

## Test Plan
- [x] All 34 existing tests pass
- [x] cargo fmt passes
- [x] cargo clippy passes (no warnings)
- [x] Manual testing: cursor appears below TUI on `/exit`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Move agent info from bordered title block to plain text below prompt
input
- Remove 'Prompt' label from input border
- Remove 'Enter: send' from instructions line
- Adjust layout constraints to accommodate new positioning

## Test Plan
- [x] All 38 existing tests pass
- [x] cargo fmt, clippy, and check pass
- [x] Manual verification of UI changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Replaced centered modal overlays with fullscreen mode switching to fix
rendering issues in inline terminal viewports.

- Convert agent selection and install prompt from overlay modals to
fullscreen UIs
- Remove percentage-based `centered_rect()` positioning that failed in
constrained inline viewports
- Use flexible layout constraints (`Constraint::Min`) that adapt to
varying viewport heights
- Add text wrapping to install prompt messages for automatic sizing

## Test Plan

- [x] All 38 existing tests pass
- [x] cargo fmt and cargo clippy pass
- [ ] Manual testing in inline viewport (Claude Code inline mode)
- [ ] Manual testing in fullscreen mode
- [ ] Verify agent selection UI renders correctly
- [ ] Verify install prompt UI renders correctly
- [ ] Verify navigation works (↑/↓/Enter/Esc)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

- Remove absolute cursor positioning that became invalid after terminal
scrolling
- Use relative positioning (`MoveToColumn(0)`) after
`ratatui::restore()`
- Add screen clearing (`Clear::FromCursorDown`) to remove visual
artifacts
- Swap cleanup order: `ratatui::restore()` before `disable_raw_mode()`

This fixes the issue where the cursor would appear in the middle of the
screen after exiting the TUI, which occurred because the absolute
positioning calculation didn't account for terminal scrolling that
happened as content was added to the scrollback buffer via
`insert_before()`.

## Test Plan

Manual testing performed:
- [x] Exit with few messages (3-5) - cursor at bottom ✓
- [x] Exit with many messages (20+) - cursor at bottom ✓
- [x] Exit via `/exit` command - cursor at bottom ✓
- [x] Exit via Ctrl-C twice - cursor at bottom ✓
- [x] All 34 automated tests pass ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

- Remove duplicate import of `execute` macro (was imported on both line
3 and line 5)
- Remove unused `MoveTo` import leftover from commit 8d6e636
- Remove unused `viewport_start_row` variable leftover from refactoring
in commit fec6099

## Root Cause

The duplicate `execute` import was introduced in commit fec6099 when
adding the import on line 3, but line 5 already had it from commit
8d6e636.

The `viewport_start_row` variable was added in commit 8d6e636 for
absolute cursor positioning, but its usage was removed in commit fec6099
when switching to relative positioning with `MoveToColumn`.

## Test Plan

- [x] `cargo build` compiles with 0 errors, 0 warnings
- [x] All 38 tests pass (`cargo test`)
- [x] `cargo clippy` passes with no warnings
- [x] `cargo fmt` applied

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Implemented autocomplete dropdown for slash commands with real-time
filtering and keyboard navigation.

- Autocomplete appears when "/" is typed at the start of input
- Filters commands as you type (case-insensitive prefix matching)  
- Navigate with Up/Down/j/k arrow keys
- Select with Tab or Enter
- Dismiss with Escape
- Dropdown positioned below prompt textarea

## Implementation

- Added `get_all_command_names()` to CommandRegistry
- Created `filter_commands()` for prefix-based filtering
- Built autocomplete module with state management
- Extended Model with autocomplete state fields
- Added 5 new messages for autocomplete navigation
- Integrated keyboard event handling in TEA pattern
- Added dropdown rendering using existing overlay pattern

## Testing

- 48 tests passing (5 new autocomplete tests)
- All existing functionality preserved
- Follows TDD methodology throughout

## Test Plan

- [x] Type "/" - dropdown appears with all commands
- [x] Type "/e" - filters to "exit"
- [x] Type "/sw" - filters to "switch-model"
- [x] Press Up/Down - navigates through filtered list
- [x] Press Tab/Enter - completes selected command
- [x] Press Escape - closes dropdown
- [x] All existing tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

Implemented dynamic prompt window resizing so the textarea grows
vertically as users type more lines.

- Added `calculate_textarea_height()` function that calculates height
based on line count and wrapping
- Accounts for text wrapping when lines exceed terminal width using
unicode-width
- Enforces minimum height (3 lines) and maximum height (10 lines) bounds
- Updated viewport from Inline(14) to Inline(20) to accommodate larger
prompts
- Added comprehensive tests for minimum height, multi-line, wrapping,
and maximum bounds

## Test Plan
- [x] All 52 existing tests pass
- [x] Added 4 new tests for dynamic height calculation
- [x] Clippy passes with no warnings
- [x] Manual testing: typing single lines, multi-line input, very long
lines, and maximum height scenarios

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
## Summary

- Removed underline styling from the TextArea prompt input field
- Created `create_textarea()` helper function for consistent styling
- Updated all 6 TextArea creation sites to use the new styling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

## Test Plan

- [x] All 48 tests pass
- [x] cargo fmt passes
- [x] cargo clippy passes with no warnings
- [x] Visual verification: underline removed from prompt input
- [x] Cursor remains visible with reversed-color styling
- [x] All TextArea operations work correctly (submit, clear,
autocomplete)

Share Claude Code with your team: https://claude.com/claude-code

Co-authored-by: Claude <noreply@anthropic.com>
CSRessel and others added 28 commits November 14, 2025 03:00
…ation (#42)

## Summary

- Replaced external `tui-textarea` crate with internal
`tui_components::textarea` implementation
- Updated all API calls to use new TextArea interface (.text(),
.handle_key(), .is_empty())
- Consolidated dependencies within tui-components library for better
component consistency

## Test Plan

- [x] All 87 tests pass
- [x] Cargo clippy clean (no warnings)
- [x] Cargo fmt applied
- [x] Manual verification of TextArea functionality
- [x] Documentation updated for new API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Add background, padding, prefix symbol, and border support to TextArea:
- Background styling with customizable colors
- Independent padding control (top/bottom/left/right)
- Optional prefix symbol (›, >, •, ▸) vertically centered on left
- Optional borders using ratatui Block widget

Updated UI to use styled textarea:
- Gray background for visual distinction
- › prefix symbol for input affordance
- Removed external Block wrapper (TextArea now self-contained)
- Height calculation accounts for padding and prefix width

All 27 tests passing including new snapshot tests for 4 style variations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…es (#44)

## Summary

This PR adds command-line argument parsing to nori-cli, allowing users
to specify the agent and initial message from the command line.

### Changes

- **Add clap dependency**: Added clap v4 with derive feature for CLI
argument parsing
- **New CLI module**: Created `src/cli.rs` with `Cli` struct and agent
name mapping functions
- **Main function updates**: Modified `main.rs` to parse CLI args,
validate agent names, and read from stdin when piped
- **Run app updates**: Updated `run_app` to accept optional
`agent_index` and `initial_message` parameters
- **Comprehensive tests**: Added `tests/cli_args_test.rs` with tests for
all CLI parsing scenarios
- **Documentation updates**: Updated docs in both `src/docs.md` and
`tests/docs.md`

### Usage Examples

```bash
# Use specific agent
nori-cli --agent claude

# Use specific agent with initial message
nori-cli --agent codex "Hello world"

# Pipe input as initial message
echo "Hello from stdin" | nori-cli

# Short flag for agent
nori-cli -a mock "Test message"
```

### Features

- Agent selection via `--agent` or `-a` flag (claude, codex, claudecode,
mock)
- Initial message as positional argument or via stdin piping
- Case-insensitive agent name matching
- Input validation with helpful error messages
- CLI arguments take precedence over stdin when both provided
## Summary
- Add comprehensive blackbox TUI tests with snapshot verification to
ensure the terminal interface renders correctly across different input
scenarios including initial state, user input, text wrapping, empty
submission handling, unicode support, and multiline input
## Summary

This PR refactors the CLI app to use the SelectionList component from
tui-components for both the autocomplete experience and agent selection
dropdown, providing cleaner and more consistent UI styling.

## Changes

- **Model Updates**: Replace manual List state management with
SelectionList instances
- **UI Rendering**: Use SelectionList for both autocomplete and agent
selection rendering
- **Event Handling**: Update navigation and selection logic to work with
SelectionList API
- **Autocomplete Logic**: Populate SelectionList with properly formatted
SelectionItems
- **Tests**: Update test assertions to work with the new SelectionList
API

## Benefits

- Consistent styling and behavior across selection interfaces
- Better keyboard navigation and visual feedback
- Reduced code duplication and improved maintainability
- Easier to extend with additional features like search filtering

## Testing

- All existing autocomplete tests pass
- Application builds and runs successfully
- Manual testing confirms proper selection behavior
## Summary

🤖 Generated with [Nori](https://www.npmjs.com/package/nori-ai)

Modernizes TUI code to consistently use `tui_components` library APIs,
replacing custom implementations with standardized components.

### Key Changes

- **TextArea Height**: Replace custom `calculate_textarea_height()`
function (36 lines) with `TextArea::desired_height()` API
- **Loading Animation**: Remove legacy spinner code, always use
`Shimmer` component
- **Code Cleanup**: Delete `use_codex_components` and `loading_frame`
fields that toggled between implementations

### Benefits

- **90 lines removed** (net code reduction)
- **Single code path** for loading animations (no more conditionals)
- **Better consistency** with tui_components patterns throughout
codebase
- **Reduced maintenance** by eliminating duplicate height calculation
logic

## Test Plan

- [x] All 100+ existing tests pass
- [x] Added `test_shimmer_renders_during_streaming()` with snapshot
verification
- [x] Updated `dynamic_textarea_height_test.rs` to test TextArea's
built-in method
- [x] Verified UI behavior unchanged from user perspective
- [x] Clippy and rustfmt checks pass
- [x] Build succeeds without warnings

## Notes

Task 4 (converting install prompt to SelectionList) was deferred as
Tasks 1-2 provide the core value and Task 4 would require extensive
additional changes across multiple handlers and UI code.

Share Nori with your team: https://www.npmjs.com/package/nori-ai

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

This PR introduces a new inline streaming system for ACP agents that
displays assistant messages incrementally with proper word wrapping
before committing them to the scrollback buffer.

### Key Changes

- **New BackendEvent enum**: Wraps `ConversationEvent` and adds inline
events (`InlineBegin`, `InlineUpdate`, `InlineCommit`, `InlineAbort`)
- **InlineEntryState tracking**: Manages streaming text with dynamic
word wrapping in `src/history.rs`
- **Text wrapping utility**: New `text_utils` module with
`wrap_text_to_width` function for proper text reflow
- **Backend updates**: All backends now emit `BackendEvent` instead of
`ConversationEvent`
- **UI integration**: Model tracks inline entries and renders them
separately from committed scrollback
- **Test updates**: All tests updated to handle new `BackendEvent`
structure with inline event tracking

### Benefits

✨ Assistant messages appear immediately as they stream  
📐 Proper word wrapping adapts to terminal width changes  
🎯 Cleaner separation between streaming and committed content  
🧪 All tests passing with comprehensive inline event handling

## Test Plan

- [x] All existing tests pass
- [x] New inline event tracking tested in `acp_runner_test.rs`
- [x] Word wrapping behavior verified
- [x] Terminal resize handling tested

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
- Modified InlineEntryState.rewrap() to preserve all whitespace and newlines
- Changed from using wrap_text_to_width (which normalizes spaces) to splitting on newlines
- Each line now preserves exact formatting including multiple spaces
- Long lines are displayed as-is (may overflow terminal width)
- This ensures ACP agents' text formatting is preserved in the UI
## Summary
🤖 Generated with [Nori](https://www.npmjs.com/package/nori-ai)

- Added GeminiAcpBackend for Google's `@google/gemini-cli` npm package
- Refactored backend availability checking into single
`compute_backend_availability()` method
- Fixed existing bug where backend_availability array had mismatched
indices with actual backends

## Implementation Details

**New Backend:**
- Created `src/backends/gemini_acp.rs` following the exact pattern from
Codex/Claude Code ACP backends
- Uses JavaScript runtime detection (bunx/npx) 
- Delegates to AcpAgentRunner for protocol handling
- Install command: `npm install -g @google/gemini-cli`

**Refactoring:**
- Extracted `Model::compute_backend_availability()` method to eliminate
duplication
- Fixed off-by-one bug in backend_availability array

**Backend Ordering:**
- Index 0: Claude Code ACP
- Index 1: Codex ACP
- Index 2: Mock ACP Agent
- Index 3: Gemini ACP (new)

## Test Plan
- [x] All 114 tests passing (110 existing + 4 new Gemini backend tests)
- [x] cargo fmt (no formatting issues)
- [x] cargo clippy (no warnings)
- [x] cargo build successful
- [x] CI tests passing

Share Nori with your team: https://www.npmjs.com/package/nori-ai

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary
🤖 Generated with [Nori](https://www.npmjs.com/package/nori-ai)

- Created a single `BACKEND_OPTIONS` constant containing all backend
metadata (name, availability check, factory function)
- Eliminated disconnected `agents` and `backend_availability` vectors
that could get out of sync
- Updated `get_backend()` to use centralized options instead of
hardcoded match statement
- Updated UI and installation handling to use the centralized system
- Added comprehensive tests to verify backend ordering and instantiation

## Test Plan
- [x] All existing tests pass
- [x] New tests verify backend ordering consistency
- [x] New tests verify correct backend instantiation for each index
- [x] Integration tests verify UI displays backends correctly

Share Nori with your team: https://www.npmjs.com/package/nori-ai
## Summary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

- Implemented Drop trait for AcpAgentRunner to prevent orphaned
processes
- Uses libc::kill with SIGTERM for synchronous process termination
- Added comprehensive integration tests verifying actual OS-level
cleanup
- Added tracing logs for debugging process lifecycle

## Technical Approach

The implementation uses `libc::kill` instead of
`tokio::process::Child::kill()` because Drop requires synchronous
execution and `block_on` cannot be called from within an existing tokio
runtime.

## Test Plan

- [x] All existing tests pass (112 tests)
- [x] New integration tests verify process cleanup on:
  - Runner drop
  - Stream reuse (spawning new stream kills old process)
  - Initialization failure
- [x] Tests use OS-level verification via `kill -0 <pid>`
- [x] cargo fmt passes
- [x] cargo clippy passes (warnings are pre-existing in other tests)

## Files Changed

- `src/acp_runner.rs`: Added Drop impl and agent_pid() method
- `Cargo.toml`: Added libc dependency
- `tests/acp_process_cleanup_test.rs`: New integration tests
- `src/backends/docs.md`: Updated documentation
- `tests/docs.md`: Updated test documentation

Share Nori with your team: https://www.npmjs.com/package/nori-ai

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

- Added file-based logging via tuicore's `.use_disk_logs(true)`
configuration
- Instrumented ACP runner with tracing macros (info/debug/warn)
throughout lifecycle
- Logs written to `~/.nori-cli/logs/` with automatic daily rotation
- Fixed pre-existing test failure in `model_backend_ordering_test.rs`

## Changes

**src/main.rs**: Enabled disk logs in TuiApp builder
**src/acp_runner.rs**: Added ~15 tracing calls covering:
- ACP initialization and handshake
- Session creation and management  
- Prompt handling
- File operations (read/write)
- Permission requests
- All error conditions

**tests/model_backend_ordering_test.rs**: Fixed test to use
`BACKEND_OPTIONS` constant

## Test Plan

- [x] All 107 existing tests pass
- [x] Cargo build succeeds
- [x] Cargo clippy passes (pre-existing warnings only)
- [x] Manual verification: logs created in `~/.nori-cli/logs/`
- [x] Manual verification: ACP lifecycle events logged with appropriate
levels

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

- Store backend in Model to reuse across multiple prompts to the same
agent
- Backend only replaced after the conversation stream ends
- Agent selection change alone doesn't affect running backend (allows
safe browsing
during active streams)

## Changes

- Added `current_backend` and `current_backend_agent_index` fields to
`Model`
- Implemented `ensure_backend_for_current_agent()` to manage backend
lifecycle
- Modified `spawn_and_stream()` to accept stream instead of consuming
backend
- Updated documentation to reflect subprocess persistence behavior

## Test Plan

- [x] All existing tests pass (104 tests)
- [x] Code formatted with `cargo fmt`
- [x] Linted with `cargo clippy`
- [x] Added persistence test structure (tests verify behavior will work
correctly once
ACP runners expose PIDs)

## Testing

The implementation enables efficient multi-turn conversations where the
agent subprocess
persists between turns. The subprocess is only killed and recreated
when:
1. User switches to a different agent AND submits a new prompt
2. Application exits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Shortens the command name for better usability while maintaining the same functionality for opening the agent router overlay.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When pulldown-cmark retroactively reclassifies content (e.g., partial
text `[foo` becomes a link reference definition `[foo]: url` producing
0 rendered lines), the committed_line_count could stay above the actual
rendered count, permanently blocking new streaming output.

Reset the counter to the current render count when the guard fires, so
future commits are not blocked after a regression.
🤖 Generated with [Nori](https://usenori.ai)

Co-Authored-By: Nori <contact@tilework.tech>
…ng-freezes-stalls-bug-20260225-033452

# Conflicts:
#	.claude/settings.json
#	.gitignore
#	README.md
#	docs.md
…ace fix

The instructions footer line had one extra trailing space compared to the
terminal width, causing all 7 blackbox_tui_test snapshots to be stale.
Regenerated snapshots to match current rendering output.
🤖 Generated with [Nori](https://usenori.ai)

Co-Authored-By: Nori <contact@tilework.tech>
@theahura theahura changed the base branch from main to dev February 25, 2026 04:34
@theahura theahura closed this Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants