Problem
boxen_window_set_modal(win, true) (boxen.c) sets a per-window flag but does NOT maintain a focus or modal stack. When a UserTalk script calls dialog.getString from inside a dialog.getString callback (legal, even if uncommon), the outer modal opens win1+focus, the inner opens win2+focus. When win2 closes, nothing automatically re-focuses win1.
PR #793 mitigated this for the common case (the bridge's restore_focus callback hands focus to the REPL input bar after every close), but that's wrong when an outer modal is still open. The outer's mini-loop re-focuses its own window on the next 100ms tick (via boxen_window_invalidate + present), so the user sees a brief flash where the outer modal looks unfocused.
Fix
Implement a proper modal stack in boxen core:
boxen_window_set_modal(win, true) pushes win onto a per-compositor modal stack.
boxen_window_close(win) pops win from the stack (if present).
- Focus + input routing always goes to
stack.top().
boxen_window_set_modal(win, false) removes from the stack (rare; typically just close).
The bridge's restore_focus callback then becomes either:
- A no-op (the stack handles it), or
- Removed entirely if
boxen_window_close does the right thing automatically.
Affected callers to verify:
Context
Acceptance
Problem
boxen_window_set_modal(win, true)(boxen.c) sets a per-window flag but does NOT maintain a focus or modal stack. When a UserTalk script callsdialog.getStringfrom inside adialog.getStringcallback (legal, even if uncommon), the outer modal opens win1+focus, the inner opens win2+focus. When win2 closes, nothing automatically re-focuses win1.PR #793 mitigated this for the common case (the bridge's
restore_focuscallback hands focus to the REPL input bar after every close), but that's wrong when an outer modal is still open. The outer's mini-loop re-focuses its own window on the next 100ms tick (viaboxen_window_invalidate+ present), so the user sees a brief flash where the outer modal looks unfocused.Fix
Implement a proper modal stack in boxen core:
boxen_window_set_modal(win, true)pusheswinonto a per-compositor modal stack.boxen_window_close(win)popswinfrom the stack (if present).stack.top().boxen_window_set_modal(win, false)removes from the stack (rare; typically just close).The bridge's
restore_focuscallback then becomes either:boxen_window_closedoes the right thing automatically.Affected callers to verify:
boxen_palette_backend.c(slash menu palette)boxen_ui.c(this bridge — see PR feat(repl): C.0.7g boxen<->UserTalk UI bridge Phase 1 (input prompts) #793)dialog.alertetc., the file picker)Context
/gatesecurity review of feat(repl): C.0.7g boxen<->UserTalk UI bridge Phase 1 (input prompts) #793, 2026-06-23 (P1-7)planning/phase_c/BOXEN_USERTALK_UI_BRIDGE.md"Open follow-ups" as a Phase 2 candidateAcceptance