feat: add TUI mode (code-puppy -t) via Textual#544
Open
mpfaffenberger wants to merge 9 commits into
Open
Conversation
Owner
Author
|
@dudeinthemirror — heads up: I accidentally merged the original PR (#530) while pushing another set of commits to |
dudeinthemirror
force-pushed
the
feature/add-tui
branch
7 times, most recently
from
July 19, 2026 18:59
3d68620 to
6924b8f
Compare
added 7 commits
July 20, 2026 14:25
TUI Foundation (Phases 0–2) • Scaffold Textual UI behind ui_mode flag (Phase 0) • Add 3-layer ui_mode resolver + /ui command • Renderer parity via capture bridge (Phase 1) • Run real agent turns from the prompt (Phase 2a) • Cancel + steer control plane (Phase 2b) • Interactive request modals (Phase 2d) • Shell passthrough + live token streaming (Phase 2 polish) • Prompt completion for /command and @path (Phase 2e) TUI Screens & Menus (Phase 3) • Menu kit + /model as a ModalScreen (Phase 3 foundation) • /agent switcher as a ModalScreen • /set as a two-step ModalScreen flow • Autosave load picker as a ModalScreen • FormScreen multi-field form kit • /diff color picker with swatches • /colors banner-color picker • /mcp install flow as Textual screens • /judges CRUD as Textual screens • /add_model manual form • Long-tail completers for /model, /agent, /mcp args • /uc tool browser as Textual screens • Onboarding slide deck + first-run hook • register_screen plugin hook (Phase 3 complete) Phase 4: Legacy Bridge & Bus • Bridge the legacy queue + route approval via the bus • Route the last direct-stdout banners through the queue Chat Surface & Streaming • Echo submitted prompt into scrollback • Show prompt echo as a tool-call-style banner • Unify scrollback into one flow; stream response inline • Stop dropping the first chunk of streamed response • Stop dropping characters in streamed responses • Stream ThinkingPart blocks into dim Static widgets • Animated is thinking... spinner with token counter • Match classic spinner animation speed (0.05s/frame) • Show AGENT RESPONSE banner above agent's reply • Keep streamed response below tool output via inline anchor UI Polish & Fixes • Minimalist chat surface (borderless log + prompt top rule) • Blend completion panel + footer into window background • Status bar + framed PROMPT turns • /history command + Ctrl+R, status line in footer row • Two-panel /diff picker with live syntax-highlighted preview • Two-panel /colors picker + configurable PROMPT banner • Two-panel /autosave_load picker with session preview • Two-panel /agent picker with details preview + actions • Two-panel /model_settings editor • Two-panel /set and /uc screens • Serve Textual UI to a browser via textual-serve • Framed status bar in footer row • Richer list modal + bare menu commands open on first Enter Ports of Classic Commands to TUI • /plugins → Textual ModalScreen • /hooks → Textual ModalScreen • /skills → Textual ModalScreen browser • /prune → Textual multi-select ModalScreen • /theme → points to command palette in TUI • Port ask_user_question to split-panel QuestionModal • /btw → Textual ModalScreen Vim Mode • Vim editing mode for the prompt box Bug Fixes • Stop agent key-listener racing Textual; native Ctrl+X • Insert newline on Shift+Enter / Ctrl+J • Scrollbar-safe capture width + responsive /tools modal • Form buttons show labels + add Esc hint • Open no-arg menu commands on single Enter • Surface plugin custom commands in completion • Mirror classic statusline exactly in TUI status bar • Clean up /plugins row labels (drop truncated [buil] tag) • Flush uncommitted 'Other' text on question modal submit • Show CODE PUPPY logo + help banner at TUI startup • Move 'cooper is ready' to bottom of startup banner Attachments • Drag-drop/paste attachment chips + PDF support Performance • Batch render-pipeline writes to kill firehose stutter Config & UI Mode • ui_mode setting takes interactive/tui (not true/false) • -i/--interactive overrides ui_mode=tui from config TUI Parity Gap Fixes (rebased onto main @ 0.0.605) All 249 TUI tests pass; 8 new behavior tests added (tests/tui/test_parity_gaps.py). Gap #3 — Spinner catalogue wiring: • tui/app.py: _tick_spinner/_render_spinner now read _current_frames_and_interval() from puppy_spinner instead of the deprecated static FRAMES shim — custom /spinner choices now animate correctly in the TUI • puppy_spinner/register_callbacks.py: add register_screen guard so bare /spinner shows available names inline instead of launching the prompt_toolkit picker (which would corrupt the Textual screen) • /set spinner_style: replace free-text input with a live FilterableListScreen showing all spinner names, first frame sample, and description — Enter to pick, Esc to cancel (tui/screens/set_picker.py) Gap #4 — Turn-boundary hooks: • tui/app.py: _run_agent_turn now fires on_interactive_turn_end (success + error) and on_interactive_turn_cancel (result=None path) — unblocks wiggum retry/judge loops and herdr idle-state tracking in the TUI • Ports classic's while-True continuation loop: if a plugin returns {"prompt": ..., "delay": ...} the TUI re-runs with the new prompt, making /goal and /wiggum work end-to-end • action_cancel_turn fires on_interactive_turn_cancel before cancelling the worker Gap #1 — Sub-agent live panel: • callbacks.py: register new subagent_panel_lines_changed phase • subagent_panel/register_callbacks.py: _push_panel fires the new hook in addition to the classic bottom-bar push • tui/app.py: add #subagent-panel Static widget above the spinner; register/ unregister _on_subagent_panel_lines_changed on mount/unmount; thread-safe routing via call_from_thread or direct call when already on the event loop Gap #2 — Trust ceremony in /plugins: • plugins_tui.py: complete rewrite — _load_rows now merges get_project_plugin_status() for untrusted/changed project plugins; new TrustCeremonyScreen (Textual ModalScreen) requires typing 'trust' before activating; _toggle branches on status across all five states (loaded, disabled, untrusted, changed, error) Other • Namespaced markdown commands + exec: directive + // normalization • scripts/upload_pr_video.py to attach large videos to PRs • Docs: revise TUI plan to permanent dual-mode (no cutover) • Docs: add Current Status & Resume Guide to the TUI plan Follow-up: /queue + /spinner native TUI screens (register_screen audit) A per-plugin register_screen audit found the last interactive-picker plugins still missing (or faking) a real Textual screen. Both now have native ModalScreens over their existing data layers; classic mode is unchanged. Supersedes the inline-names stopgap noted under Gap #3 above. Gap #5 (new) - /queue interactive menu was dead in the TUI: - steer_queue's /queue used prompt_toolkit (arrow_select_async / PromptSession) driven by suspended_run_ui; with no run-UI to suspend in Textual it no-oped / fought the screen. It had only a custom_command hook, no register_screen. - Add steer_queue/queue_tui.py: QueueScreen (view/add/edit/delete) over the same PauseController steer-queue; add/edit reuse the shared tui/screens/form.py FormScreen (DRY). Wired via register_screen. - Documented as Gap #5 (FIXED) in docs/TUI_PARITY_GAPS.md. Gap #3 (completed) - real /spinner picker replaces the text redirect: - The earlier register_screen guard only printed "use /spinner <name>" - graceful degradation, not parity. - Add puppy_spinner/spinner_tui.py: SpinnerScreen, a native live-preview modal that animates each spinner at its own speed, with a speed dial (-/+), i to init spinners.json, and Enter/OptionSelected to apply. Reads/writes the same catalogue / set_active layer and reuses the classic picker's _step_interval speed-grid math (DRY). - Fix Enter wiring: OptionList swallows Enter for OptionSelected, so the screen-level binding never fired - handle on_option_list_option_selected. - Part (b) confirmed already fixed (thinking-spinner reads active style); Gap #3 now fully closed in docs/TUI_PARITY_GAPS.md. Also: - tui/app.py submit_prompt now persists submissions to the shared prompt-history file (HistoryStore) so /history reflects TUI prompts; classic got this free via prompt_toolkit FileHistory, the Textual prompt had no such hook. Tests: +15 behavior tests - tests/plugins/test_steer_queue_tui.py (7) - tests/plugins/test_puppy_spinner_tui.py (8) /queue UX improvements + idle queue drain for TUI Ctrl+G shortcut: - Add Ctrl+G binding to tui/app.py BINDINGS + action_queue() delegating to get_menu_opener("queue") — appears in the Textual Footer alongside ^t steer. Key chosen as ctrl+u was already claimed by TextArea (delete-to-line-start). Queue modal UX fixes: - Hint text "↑/↓ move" was wrong (arrows only navigate); corrected to "↑/↓ select". - Add Shift+↑ / Shift+↓ to reorder prompts in the queue (swap adjacent items via replace_pending_steer_queued); hint updated to "SHIFT+↑/↓ reorder". 4 new tests. Idle queue drain in TUI: - After each successful turn _set_busy(False) now calls _maybe_run_next_queued() which pops and auto-submits the next queued prompt — mirrors cli_runner.py's idle loop. Only fires on clean completion (not on cancel/exception). 2 new tests. Queued-prompt PROMPT banner: - Prompts injected between turns by the runtime (prepare_queued_steer_ injection in _run_signals.py) now show a proper PROMPT banner in the TUI, identical to manually-typed prompts. - Root cause of previous attempt (bus polling delay): PromptEchoMessage via get_message_bus().emit() had ~10 ms latency vs stream deltas enqueued synchronously; banner arrived after the response. - Fix: prepare_queued_steer_injection now returns (content, echo_text) tuple; _runtime._do_run fires await on_stream_event("prompt_echo", {"text": echo_text}) before _follow_up_run() — synchronous, ordered before next turn's deltas by construction. - _on_stream_event handles "prompt_echo" by calling _finalize_stream() (closes the previous turn's open markdown widget) then enqueue_render(("prompt_echo", text)); _render_loop calls _echo_prompt(text). Without _finalize_stream(), the previous turn's _md_stream was still open and the ObjC response was appended into hello-world's widget with the banner landing after the combined blob. - Add PromptEchoMessage to messages.py (kept for future use; current between-turns path uses stream_event instead of the bus). - Tests: +2 idle-drain parity tests in test_parity_gaps.py, updated test_runtime_queued_steer.py for new return type.
Add the Green Screen palette, improve Purple Puppy contrast, paginate the theme picker, and make prompts plus Termflow markdown and syntax highlighting follow the active theme. Also align shutdown callbacks with their awaited async contract.
Add ~/.code_puppy/models.json (MODELS_FILE) to ModelFactory's extra model sources, loaded first so extra_models.json and the OAuth catalogues still win on name collisions -- the more specific/ephemeral source overrides the broad user catalogue.
_start_keyboard_listener called the removed _key_listeners.set_escape_handler, which crashed the moment any shell command ran (TUI and classic) and meant the Ctrl+X/Ctrl+B shell chords were never registered. Replace it with the _register_shell_chords() call the docstring already promised and _stop_keyboard_listener was already unregistering. Re-add the Ctrl+X chord hints to the classic startup help. Repair the accompanying test suite: - Fix wrong mock patch targets that silently no-opped: get_current_agent in tests/mcp/conftest.py (patched the source, not the from-imported name) and emit_system_message in the cli_runner startup-help test. - Align stale expectations with deliberate design changes: empty shipped models.json (model/summarization resolve to None), the theme thinking- banner icon, MCP session-binding messaging, and the new PromptEchoMessage messaging export.
The wait-loop broke on 'cooper123', which appears immediately in the echoed '$ echo cooper123' command line, before the command output and the 'Done' completion marker render. On slow CI runners it bailed early and failed the 'Done' assertion. Wait on the 'Done' terminal marker instead.
#6) Rebasing feature/add-tui onto main pulled in two sub-agent-panel commits (b9f5d12 removed the plugin's fixed row cap; 153063b re-added a terminal-height clamp only inside the classic BarPainterMixin). _push_panel() fans the same UNCAPPED line list to both the classic bottom bar (clamped) and the TUI's subagent_panel_lines_changed hook (NOT clamped), so the TUI's Static silently clipped overflow agents past its max-height with no indicator. - Extract the clamp into a shared bar_painters.clamp_panel_lines(lines, budget) helper (single source of truth for the "... +N more" overflow behavior); BarPainterMixin._visible_panel_lines() now delegates to it (behavior unchanged). - TUI _apply_subagent_panel() runs the uncapped list through the SAME helper with a height-relative budget (_subagent_panel_budget() = terminal_height - RESERVE, floored at MIN_ROWS), pinning widget max-height to the painted row count. Panel sits above #log (height:1fr), so growth only steals transcript rows, never the prompt/footer -- mirrors classic's height-scaling instead of a stingy fixed cap. Tests: clamp_panel_lines contract + TUI overflow/no-overflow/height-scaling. Docs: TUI_PARITY_GAPS.md Gap #6 (FIXED) + docs/manual-tests/subagent-panel.md. Also fix two flaky/broken CI tests surfaced on this branch: - conftest isolate_global_state_between_tests now repoints the customizable_commands plugin's module-level CONFIG_DIR (captured by value at import) alongside _COMMAND_DIRECTORIES[0], and restores both on teardown. Previously only the latter was overridden, so test_global_directory_in_command_directories saw the two drift apart and failed its os.path.join(rc.CONFIG_DIR, "commands") == [0] assertion. - test_user_approval_routes_through_bus_in_tui now polls for the #opt-0 button node before clicking instead of clicking the instant the ConfirmModal lands on the screen stack (its buttons compose a pump-cycle later), fixing an intermittent textual NoMatches under CI load.
dudeinthemirror
force-pushed
the
feature/add-tui
branch
from
July 20, 2026 21:26
6924b8f to
c26d620
Compare
added 2 commits
July 20, 2026 14:54
Post-rebase follow-up: after replaying the TUI branch onto latest main, an audit of the classic-vs-TUI model-settings parity surfaced a bug where retry overrides silently no-opped in the Textual UI. The retry settings (retry_main_strategy, retry_main_max_attempts, retry_subagent_strategy, retry_subagent_max_attempts) live in the shared SETTING_DEFINITIONS, so the TUI displays them -- but its _save_setting / _reset_setting helpers fell through to the generic set_model_setting store instead of the dedicated retry_model_ namespace that ModelSettingsMenu (and the retry engine) actually read. Edits were written to a key nothing consumes, so they never took effect. Reuse the classic _write_per_model_retry helper (DRY) rather than reimplementing the namespace logic. Add tests locking in all three routing backends (global setter / retry namespace / generic per-model).
- tests/mcp/conftest.py: remove unused ExitStack import (ruff F401) - tests/plugins/test_chatgpt_oauth_utils.py: update expected supported_settings for gpt-5.6* models to include reasoning_context and reasoning_mode, matching source behavior in chatgpt_oauth/utils.py
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.
Summary
Demos
https://github.com/mpfaffenberger/code_puppy/releases/download/pr-530-assets/puppy-tui.mp4
Spinner picker
Ask user question
Steer
Dangerous command dialog
Vim support
--tuiflag) as a permanent dual-mode alongside the classic prompt_toolkit UI — same plugin/hook system, no cutover needed/plugins; 249 TUI tests pass, 8 new behavior tests addedWhat's included
TUI Foundation (Phases 0–4)
ui_modeflag; 3-layer resolver +/uicommand/model,/agent,/set,/diff,/colors,/autosave_load,/mcp,/judges,/add_model,/uc,/plugins,/hooks,/skills,/prune,/btw,/theme→ all Textual ModalScreensParity gap fixes (rebased onto main @ 0.0.605)
_tick_spinner/_render_spinnernow read live catalogue frames via_current_frames_and_interval()instead of deprecated shim; bare/spinnerredirected with available names;/set spinner_stylegets aFilterableListScreenpicker instead of free-text input_run_agent_turnfireson_interactive_turn_end+on_interactive_turn_cancel; ports classic'swhile Truecontinuation loop so/goaland/wiggumwork end-to-end in the TUIsubagent_panel_lines_changedhook +#subagent-panelStatic widget — live per-sub-agent rows appear above the spinner while sub-agents are running/pluginsTUI screen rewritten — shows untrusted/changed project plugins; newTrustCeremonyScreen(Textual ModalScreen) with file listing +trustconfirmationTest plan
./scripts/code-puppy-dev -t/spinner zoomies→ thinking spinner should show zoomies animation;/spinneralone → shows available names/set→ search "spinner" → Enter → spinner picker dropdown with names + descriptions/plugins→ project plugins in untrusted state should appear; Enter opens trust ceremony#subagent-panelwidget appears above spinner with live rowsuv run pytest -q tests/tui/→ 249 passedNotes