Part of epic #595, milestone #9 TUI ShellApp Migration.
Split out of #634 on 2026-08-07. #634 originally carried both the A–F prerequisite
work and the entry-point flip. That is the same over-scoping that already forced #605
to be rescoped once (to Stage 6a, the parity sweep) — and #634's own acceptance bar
rejects any PR that leaves event_loop() in place, so a partial attempt cannot land.
This issue carries the prerequisites only; #634 is now the flip alone.
Why this is safe to do in one pass
Nothing here touches the live path. tui_main::run() / event_loop() remain the
live TUI entry point for the whole of this issue. TuiShellApp stays the dormant
scaffold it has been since Stage 0, exercised only by its own #[cfg(test)]
driver_with_shell tests. So there is no regression surface: if a port is wrong, the
driver_with_shell test catches it, and the running editor is unaffected either way.
That is exactly what makes this separable from the flip, which is all risk and no
mechanical work.
The upstream blockers are now clear
All three quadraui gaps that blocked #634 have landed on develop:
| gap |
status |
quadraui#531 — Backend::draw_settings_chrome |
landed (b586086, 2ccea55) |
quadraui#532 — AppShell::set_title_bar_visible |
landed (7d4ba53, 5f06867) |
quadraui#533 — Backend::draw_terminal_divider |
landed |
Before writing code, confirm all three are in this machine's checkout —
git -C ~/src/quadraui log --oneline -20 and grep for the trait methods. If any is
absent, git -C ~/src/quadraui pull first. Do not proceed on a stale checkout.
Scope — items A–F
Lettering is preserved from #634 / src/tui_main/shell_app.rs's module doc, which
Stage 6a wrote precisely so the next session starts from a scoped list rather than a
re-survey. Read that module doc first — it is the authoritative per-item detail.
A. Menu bar + command centre + menu dropdown — unblocked by quadraui#532
draw_frame carves a full-width row 0 for these above the activity bar, and
vimcode's menu bar is runtime-toggleable (engine.menu_bar_visible,
menu_bar_toggleable = true). AppShell used to reserve its title-bar row at
construction with no runtime toggle — set_title_bar_visible is exactly that toggle.
Drive it from engine.menu_bar_visible. draw_menu_bar, draw_command_center and
MenuSystem::render are already trait calls, so this should collapse to wiring.
B. Split terminal panes — unblocked by quadraui#533
render_terminal_panel's split arm drew its divider via the free
quadraui::tui::draw_terminal_divider. Call Backend::draw_terminal_divider(rect)
instead. Note the trait method is geometry-neutral (rect: Rect), not the free
function's raw x/y/height — convert at the call site. Also retire
panels::draw_settings_chrome_via_backend (the #531 stand-in) down to the real
Backend::draw_settings_chrome call, which was always the stated plan for it.
C. Plugin extension panel + AI sidebar panel
render_ext_panel's help-popup overlay and manual scrollbar are raw set_cell box
drawing; render_ai_sidebar takes buf: &mut ratatui::buffer::Buffer outright.
Check the existing trait surface before filing anything upstream —
Backend::draw_tooltip and Backend::draw_scrollbar both already exist in
quadraui/src/backend.rs, so the popup and scrollbar most likely need no new
primitive. The fill_row / fill_rect / draw_rule_row rule-row trick that Stages 2a
and 2c used covers plain box drawing. render_ai_sidebar should lose its Buffer
parameter and take &mut dyn Backend, one implementation shared by draw_frame and
render_content — the same shape the settings / source-control / extensions sidebar
renderers already converted to.
D. The three unported keyboard tiers
Activity-bar-focused, sidebar-focused, and cmd_sel. Follow the dispatch pattern
Stages 3 and 4 established in TuiShellApp::handle.
E. ShellConfig build-out
Today's config() is #[cfg(test)]-only and declares a single panel:explorer
PanelDefinition, so AppShell would render one activity-bar icon. A live config needs
every panel render::build_activity_bar emits (src/render.rs:8147 — menu/hamburger,
explorer, search, debug, source control, extensions, and the bottom items), split across
ShellConfig.panels / with_bottom_items, plus an on_shell_event arm treating the
hamburger item as "open the menu" rather than "switch panel".
build_activity_bar is the source of truth for the list — derive from it, don't
hand-transcribe. (AppShell::build_activity_bar leaves active_accent/selection_bg
None where vimcode's sets them from the theme; the TUI rasteriser falls back to
theme.cursor — cosmetic, not a blocker, but note it if you touch it.)
F. run()'s own non-loop responsibilities
The panic hook, core::swap::register_emergency_engine, the emergency swap flush and
the custom crash message all live in run() around event_loop. run_with_shell →
tui::run::run does its own catch_unwind and resume_unwinds after restoring the
terminal, so an outer catch_unwind in run() still sees the payload — the ordering
just has to be re-established deliberately. Establish it here, so the flip in #634
is purely an entry-point swap.
keyboard_enhanced is not a blocker: setup() can call
crossterm::terminal::supports_keyboard_enhancement() itself and get the same answer
the runner's push used.
The Platform-Neutrality Rule still applies
If an item genuinely needs surface quadraui does not have, file the upstream issue and
stop on that item — do not write a local stand-in. This repo has scar tissue for
exactly this (#319, three reverted attempts; #605's review caught another). Land whatever
else is complete and say plainly in the PR which item is parked on which new upstream
issue. A partial PR is fine on this issue — unlike #634, there is no all-or-nothing
acceptance bar here.
The one sanctioned exception already in the tree is draw_settings_chrome_via_backend,
which was explicitly written as a temporary stand-in with its upstream issue filed —
and item B retires it.
Acceptance
Do not touch
README, CHANGELOG, or any shared docs — the coordinator handles those.
Blocks: #634. Part of: #595.
Part of epic #595, milestone #9 TUI ShellApp Migration.
Split out of #634 on 2026-08-07. #634 originally carried both the A–F prerequisite
work and the entry-point flip. That is the same over-scoping that already forced #605
to be rescoped once (to Stage 6a, the parity sweep) — and #634's own acceptance bar
rejects any PR that leaves
event_loop()in place, so a partial attempt cannot land.This issue carries the prerequisites only; #634 is now the flip alone.
Why this is safe to do in one pass
Nothing here touches the live path.
tui_main::run()/event_loop()remain thelive TUI entry point for the whole of this issue.
TuiShellAppstays the dormantscaffold it has been since Stage 0, exercised only by its own
#[cfg(test)]driver_with_shelltests. So there is no regression surface: if a port is wrong, thedriver_with_shelltest catches it, and the running editor is unaffected either way.That is exactly what makes this separable from the flip, which is all risk and no
mechanical work.
The upstream blockers are now clear
All three quadraui gaps that blocked #634 have landed on
develop:Backend::draw_settings_chromeb586086,2ccea55)AppShell::set_title_bar_visible7d4ba53,5f06867)Backend::draw_terminal_dividerBefore writing code, confirm all three are in this machine's checkout —
git -C ~/src/quadraui log --oneline -20and grep for the trait methods. If any isabsent,
git -C ~/src/quadraui pullfirst. Do not proceed on a stale checkout.Scope — items A–F
Lettering is preserved from #634 /
src/tui_main/shell_app.rs's module doc, whichStage 6a wrote precisely so the next session starts from a scoped list rather than a
re-survey. Read that module doc first — it is the authoritative per-item detail.
A. Menu bar + command centre + menu dropdown — unblocked by quadraui#532
draw_framecarves a full-width row 0 for these above the activity bar, andvimcode's menu bar is runtime-toggleable (
engine.menu_bar_visible,menu_bar_toggleable = true).AppShellused to reserve its title-bar row atconstruction with no runtime toggle —
set_title_bar_visibleis exactly that toggle.Drive it from
engine.menu_bar_visible.draw_menu_bar,draw_command_centerandMenuSystem::renderare already trait calls, so this should collapse to wiring.B. Split terminal panes — unblocked by quadraui#533
render_terminal_panel's split arm drew its divider via the freequadraui::tui::draw_terminal_divider. CallBackend::draw_terminal_divider(rect)instead. Note the trait method is geometry-neutral (
rect: Rect), not the freefunction's raw
x/y/height— convert at the call site. Also retirepanels::draw_settings_chrome_via_backend(the #531 stand-in) down to the realBackend::draw_settings_chromecall, which was always the stated plan for it.C. Plugin extension panel + AI sidebar panel
render_ext_panel's help-popup overlay and manual scrollbar are rawset_cellboxdrawing;
render_ai_sidebartakesbuf: &mut ratatui::buffer::Bufferoutright.Check the existing trait surface before filing anything upstream —
Backend::draw_tooltipandBackend::draw_scrollbarboth already exist inquadraui/src/backend.rs, so the popup and scrollbar most likely need no newprimitive. The
fill_row/fill_rect/draw_rule_rowrule-row trick that Stages 2aand 2c used covers plain box drawing.
render_ai_sidebarshould lose itsBufferparameter and take
&mut dyn Backend, one implementation shared bydraw_frameandrender_content— the same shape the settings / source-control / extensions sidebarrenderers already converted to.
D. The three unported keyboard tiers
Activity-bar-focused, sidebar-focused, and
cmd_sel. Follow the dispatch patternStages 3 and 4 established in
TuiShellApp::handle.E.
ShellConfigbuild-outToday's
config()is#[cfg(test)]-only and declares a singlepanel:explorerPanelDefinition, soAppShellwould render one activity-bar icon. A live config needsevery panel
render::build_activity_baremits (src/render.rs:8147— menu/hamburger,explorer, search, debug, source control, extensions, and the bottom items), split across
ShellConfig.panels/with_bottom_items, plus anon_shell_eventarm treating thehamburger item as "open the menu" rather than "switch panel".
build_activity_baris the source of truth for the list — derive from it, don'thand-transcribe. (
AppShell::build_activity_barleavesactive_accent/selection_bgNonewhere vimcode's sets them from the theme; the TUI rasteriser falls back totheme.cursor— cosmetic, not a blocker, but note it if you touch it.)F.
run()'s own non-loop responsibilitiesThe panic hook,
core::swap::register_emergency_engine, the emergency swap flush andthe custom crash message all live in
run()aroundevent_loop.run_with_shell→tui::run::rundoes its owncatch_unwindandresume_unwinds after restoring theterminal, so an outer
catch_unwindinrun()still sees the payload — the orderingjust has to be re-established deliberately. Establish it here, so the flip in #634
is purely an entry-point swap.
keyboard_enhancedis not a blocker:setup()can callcrossterm::terminal::supports_keyboard_enhancement()itself and get the same answerthe runner's push used.
The Platform-Neutrality Rule still applies
If an item genuinely needs surface quadraui does not have, file the upstream issue and
stop on that item — do not write a local stand-in. This repo has scar tissue for
exactly this (#319, three reverted attempts; #605's review caught another). Land whatever
else is complete and say plainly in the PR which item is parked on which new upstream
issue. A partial PR is fine on this issue — unlike #634, there is no all-or-nothing
acceptance bar here.
The one sanctioned exception already in the tree is
draw_settings_chrome_via_backend,which was explicitly written as a temporary stand-in with its upstream issue filed —
and item B retires it.
Acceptance
draw_settings_chrome_via_backendis gone (collapsed intoBackend::draw_settings_chrome).render_contentpaints the terminal-panel split divider throughBackend::draw_terminal_divider.#[cfg(test)])ShellConfigcovering everybuild_activity_barpanel.driver_with_shelltests covering each newly painted surface — thesame bar Stages 2a/2b/2c met. In-crate
#[cfg(test)], nottests/.event_loop()is still present and still live. Flipping the entry point is#595 Stage 6 — cutover TUI entry point to run_with_shell; delete event_loop() #634's job, not this one — a PR that flips it here will be sent back.
cargo build/clippy/fmt --check/test --no-default-featuresall EXIT=0.src/tui_main/shell_app.rs's module doc is updated to match reality, so #595 Stage 6 — cutover TUI entry point to run_with_shell; delete event_loop() #634starts from an accurate list.
Do not touch
README, CHANGELOG, or any shared docs — the coordinator handles those.
Blocks: #634. Part of: #595.