M2: TUI #3

Open
darman wants to merge 1 commits from feature/m2 into feature/m1
Owner
  • ratatui app + EngineHandle bridging the async engine to the render loop
  • chat viewport, markdown renderer, input handling
  • permission modal wired to the real oneshot ask path
  • session picker / resume
  • TestBackend snapshot tests (empty session, tool cards, permission modal, session picker, messages)
- ratatui app + `EngineHandle` bridging the async engine to the render loop - chat viewport, markdown renderer, input handling - permission modal wired to the real oneshot ask path - session picker / resume - `TestBackend` snapshot tests (empty session, tool cards, permission modal, session picker, messages)
darman added this to the M2 — TUI milestone 2026-07-10 17:10:00 +02:00
darman self-assigned this 2026-07-10 17:10:00 +02:00
darman added this to the AI Harness project 2026-07-10 17:10:00 +02:00
darman changed target branch from develop to feature/m1 2026-07-10 17:11:48 +02:00
darman added 1 commit 2026-07-10 17:11:48 +02:00
Builds the ratatui TUI: EngineHandle bridging the async engine to the render loop, chat viewport rendering with a markdown renderer, input handling, a permission modal wired to the real oneshot ask path, and a session picker. Adds TestBackend snapshot tests covering empty session, tool cards (running/completed/error), permission modal, session picker, and message rendering.
darman requested review from claude-reviewer 2026-07-11 14:41:57 +02:00
Collaborator

Review: M2 — TUI

Good structure: EngineHandle cleanly separates the non-blocking multi-turn engine from the render loop, the run-slot reservation has a nice comment about the insert-after-spawn race, the terminal guard restores on both drop and panic, and the TestBackend snapshot tests are a great choice for a TUI. Main issue is scroll behavior; a couple of smaller things.

Chat viewport never follows the tail (medium)

render_chat (render.rs:1483) applies .scroll((state.scroll_offset, 0)), but scroll_offset is only ever changed by manual scroll_up/scroll_down and reset to 0 on session load/new. Nothing advances it toward the bottom as content arrives. Consequences:

  • Once a transcript exceeds the chat height, streaming assistant output renders below the fold and is invisible until the user manually scrolls down.
  • Resuming a long session (set_session sets scroll_offset = 0) shows the top of the history, not the latest turn.
  • scroll_down (state.rs:2413) is saturating_add with no upper clamp, so the user can scroll into blank space past the end.

Typical fix: track the total rendered line count vs. viewport height, and when the user is "pinned to bottom" auto-set scroll_offset to max(0, total_lines - viewport) each frame; clamp manual scrolling to that max.

Persistent DB filename uses an unstable hash (low-medium)

db_path (harness-app/src/lib.rs:65) derives the sqlite filename from std::collections::hash_map::DefaultHasher. DefaultHasher's algorithm/output is explicitly not guaranteed stable across Rust releases, so a toolchain upgrade can change the hash → change the filename → silently orphan the user's existing session history under a new DB. Prefer a stable hash (fixed FNV/seahash/blake3) or just the sanitized path.

Minor

  • render(... &requests[0], area) (render.rs:1432) indexes directly. It's safe by the current invariant (Permission modal is cleared to None when the last request resolves), but requests.first() with a guard would be more robust against a future refactor.
  • Loading a session that has an in-flight run via the picker leaves state.running = false (spinner/abort hint stale), since running is only driven by RunStarted/RunFinished for the live session — set_session doesn't consult engine.is_running.
  • PartView::Reasoning re-renders markdown every frame (no cached_lines), unlike Text/Tool. Minor perf.
  • Enter always submits, so there's no way to insert a literal newline into a multi-line prompt (Up/Down navigate existing lines but nothing creates them). Consider Shift+Enter or Alt+Enter for newline.

— automated review (Claude)

## Review: M2 — TUI Good structure: `EngineHandle` cleanly separates the non-blocking multi-turn engine from the render loop, the run-slot reservation has a nice comment about the insert-after-spawn race, the terminal guard restores on both drop and panic, and the TestBackend snapshot tests are a great choice for a TUI. Main issue is scroll behavior; a couple of smaller things. ### Chat viewport never follows the tail (medium) `render_chat` (`render.rs:1483`) applies `.scroll((state.scroll_offset, 0))`, but `scroll_offset` is only ever changed by manual `scroll_up`/`scroll_down` and reset to `0` on session load/new. Nothing advances it toward the bottom as content arrives. Consequences: - Once a transcript exceeds the chat height, streaming assistant output renders *below the fold* and is invisible until the user manually scrolls down. - Resuming a long session (`set_session` sets `scroll_offset = 0`) shows the top of the history, not the latest turn. - `scroll_down` (`state.rs:2413`) is `saturating_add` with no upper clamp, so the user can scroll into blank space past the end. Typical fix: track the total rendered line count vs. viewport height, and when the user is "pinned to bottom" auto-set `scroll_offset` to `max(0, total_lines - viewport)` each frame; clamp manual scrolling to that max. ### Persistent DB filename uses an unstable hash (low-medium) `db_path` (`harness-app/src/lib.rs:65`) derives the sqlite filename from `std::collections::hash_map::DefaultHasher`. `DefaultHasher`'s algorithm/output is explicitly not guaranteed stable across Rust releases, so a toolchain upgrade can change the hash → change the filename → silently orphan the user's existing session history under a new DB. Prefer a stable hash (fixed FNV/seahash/blake3) or just the sanitized path. ### Minor - `render(... &requests[0], area)` (`render.rs:1432`) indexes directly. It's safe by the current invariant (Permission modal is cleared to `None` when the last request resolves), but `requests.first()` with a guard would be more robust against a future refactor. - Loading a session that has an in-flight run via the picker leaves `state.running = false` (spinner/abort hint stale), since `running` is only driven by `RunStarted`/`RunFinished` for the live session — `set_session` doesn't consult `engine.is_running`. - `PartView::Reasoning` re-renders markdown every frame (no `cached_lines`), unlike Text/Tool. Minor perf. - `Enter` always submits, so there's no way to insert a literal newline into a multi-line prompt (Up/Down navigate existing lines but nothing creates them). Consider Shift+Enter or Alt+Enter for newline. — automated review (Claude)
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/m2:feature/m2
git checkout feature/m2
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: darman/ai-harness#3