feat(chat): rich+prompt streaming layout with a virtualized transcript - #482
Merged
Merged
Conversation
Delete the previous pane design (header + session sidebar + grid) and rebuild the shell following the pi alternate-screen model: one full-height scrollable transcript with a fixed working dock beneath it. Shell: - ConversationView owns the full viewport; it is the only scroll region. - Composer dock (#composer) fixed below: status line, input, shortcut hint. - Status bar fixed below the composer. - Session navigation is transient (/sessions picker modal), never a sidebar. - The input holds default + persistent focus; every modal dismissal and session switch restores it (pi's editor-always-focused contract). - User prompts render as background-tinted blocks (pi's userMessageBg box) instead of a '> you:' prefix line. - App commands moved to modified keys (Ctrl+N for new session) since the input owns bare keys. Removed: SessionList sidebar pane, Header, grid layout, _sync_sidebar, dock-based footer positioning, bare-n binding. Kept: streaming renderer (#407), WS attach/pump/dispatch, session routing + replay, slash commands, worktree/config/ask modals, footer reactives. Tests: updated for the new shell (transcript/composer/status stacking, user-message boxes, transient picker, footer session-cell gating, ctrl+n binding, default-focus pty typing). 156 TUI/pty/CLI tests green, ruff + ty clean.
The real-PTY test types directly into the input (default focus) with no Tab navigation, but the marker was written before on_mount's focus assignment landed on slower macOS runners, so the keystrokes went nowhere. Restore a short settle after the first render (the original pre-#375-rewrite test had the same 0.4s pause after its Tabs).
The #409 pinned-after-stream assertion compared scroll_y against max_scroll_y on the first beat after finalise; on loaded CI runners the mounted Markdown's layout can land a beat later, making the pin look one line short (scroll_y=5 vs max_scroll_y-1=6). Poll briefly for the pin instead — the contract (pinned after stream; not yanked after scroll-up) is unchanged.
_finalize_segment pins via scroll_end(immediate=True), which reads the max_scroll_y current at mount time — the freshly-mounted Markdown's height settles a layout pass later, so the pin could sit a couple of lines short once content grows (observed as a flaky pinned-after-stream assertion on slower CI runners: scroll_y=5 vs max_scroll_y=7). When the user was pinned, schedule a second follow after the refresh so the segment end re-pins to the settled height. Per-delta streaming follow stays immediate (#409 scroll-up race unchanged); only the finalise-time mount re-pins deferred.
Replace the prompt_async per-turn REPL with a full-screen prompt_toolkit application (rich-rendered content): - Virtual rendering: the conversation control stores pre-rendered rich lines and the Window requests only the visible slice per frame (UIContent.get_line is lazy) — a long transcript costs O(viewport) per render, not O(content). No widget-per-message DOM growth. - Continuous scroll / follow-end: window.vertical_scroll is the preferred scroll; new content pins to the bottom only while the user is already there (scroll away with PgUp/wheel freezes the viewport; PgDn/wheel down re-follows). - Slash menu: typing "/" pops a completion menu (/help /exit /clear). - Streaming: events from agent.run_stream render into the transcript (dim "calling …" tool lines, live Markdown answer re-rendered per delta, throttled by the virtual renderer). chat now defaults to this streaming chat; the worker-based Textual shell moves behind --tui (opt-in). The dead per-turn _stream_answer + its unused rich imports are deleted. Also: - ConversationView follow re-check: re-pin to the settled bottom after a layout pass, guarded so a reader who scrolled up is never yanked (#409) — fixes a pinned-after-stream flake on loaded runners. - Textual shell (--tui): pi dark palette (userMessageBg boxes, state- colored tool cards, warm markdown headings). Tests: 3 new streaming-tui tests (virtual get_line, stream-block replace, slash completer); chat routing tests updated (default streaming, --tui opt-in). 1673 tests green, ruff + ty clean.
…renderer prompt_toolkit formatted-text fragments are (style, text) tuples; the conversation control emitted (text, style) (mirroring rich's Segment order). When the Window painted a '❯ hello' line, it parsed the text as a style string and crashed: ValueError Wrong color format '❯' — surfaced as an unhandled event-loop exception whose own traceback handler hit the same error during its redraw. Swap both conversions (rich lines and append_text) to (style, text), and add a regression test asserting the tuple order so a future flip fails loudly instead of crashing at paint time. Verified end-to-end: the app paints the prompt char + content through the real prompt_toolkit run loop with no exception (PTY smoke), and the full suite (1674 tests) passes with ruff + ty clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Rebuild the interactive chat around a rich + prompt_toolkit full-screen streaming layout with virtual rendering — and demote the widget shell to an opt-in
--tui.Changes
streaming_tui.py(new) — prompt_toolkit full-screen app:UIContent.get_lineis lazy) → a long transcript costs O(viewport) per render, not O(content). No widget-per-message DOM growth.window.vertical_scrollis the preferred scroll. New content pins to the bottom only while the user is already there; PgUp / wheel up freezes the viewport (the stream never yanks a reader); PgDn / wheel down re-follows./pops a completion menu (/help,/exit,/clear).agent.run_streamevents render into the transcript — dimcalling …tool lines, live Markdown answer re-rendered per delta.chatdefaults to this streaming chat; the worker-based Textual shell moves behind--tui(opt-in). Deleted the dead per-turn_stream_answer+ unused rich imports.--tui) also gets pi's dark palette (slate user-message boxes, state-colored tool cards, warm markdown headings) and a follow re-check that re-pins to the settled bottom without yanking readers who scrolled up (TUI ConversationView has no auto-scroll — streaming output renders off-screen on the primary chat path (invisible without constant manual scrolling) #409) — fixes a pinned-after-stream flake on loaded runners.Problem fixed
prompt_asyncREPL: no persistent transcript, no scrolling, no slash menu.Tests
test_streaming_tui.py: virtualget_line(lazy serving, out-of-range no-op), stream-block replace (only the live block re-renders), slash completer (menu only for/input).test_cli.pyrouting:chat→ streaming chat (asyncio.run),--tui→_launch_tui_app.ruff check+ty checkclean; CI green on ubuntu/macos (windows pending).