wta(tui): queue prompts while busy, Esc to dequeue LIFO#278
wta(tui): queue prompts while busy, Esc to dequeue LIFO#278yeelam-gordon wants to merge 32 commits into
Conversation
While a turn is in flight, pressing Enter on a non-empty input no longer surfaces a 'Agent is busy' system message and discards the user's typing. Instead, the prompt is pushed onto a per-tab VecDeque and dispatched FIFO once the turn returns to an accepting state (and no recommendation card is staged). Esc pops the back of the queue (LIFO undo) when the input box is empty, so users can cancel queued messages one by one in the order they would un-add them. Esc on a non-empty input keeps the existing 'clear draft' behavior so editing isn't lost. A 1-row 'Queued (N): preview...' indicator renders directly above the input box whenever the queue is non-empty. The queue is bounded (PENDING_PROMPT_QUEUE_CAP = 20) and is cleared by /clear, /new, /restart, tab reset, and session-load paths (all funneled through clear_chat_history). Drain hook lives at the end of every event-handling tick rather than wired into individual TurnState transitions, so error paths (TabError/AgentError), autofix 'Ignore' decisions, stale-autofix discards and the natural turn-complete path are all covered uniformly. Card-visible state explicitly blocks drain so a queued prompt can't wipe a surfaced recommendation before the user can act on it. Tests: 9 new unit tests cover enqueue-on-busy, FIFO dispatch on turn complete, LIFO Esc pop, Esc-with-text preserving draft, card-visible blocking drain, queue cap with input preservation, clear_chat_history clearing the queue, disconnected drain no-op, and preview truncation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. drain_pending_prompts: use the loop's tab_id for PaneContext.tab_id instead of self.tab_id (focused WT tab). Background-tab drains now correctly attribute the dispatched prompt to the tab whose queue was actually drained. 2. queued_hint::render: subtract HORIZONTAL_PADDING from area.width before truncation so the 2-space left padding doesn't push the localized indicator off the right edge. Adds drain_uses_loop_tab_id_for_pane_context_not_focused_tab test (281 total). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert vcpkg triplet to v143 (the v145 bump was a local build-env workaround and leaked into the PR by accident; this PR doesn't touch the toolchain). * drain_pending_prompts: sort tab_ids for deterministic cross-tab dispatch order; update doc comment to clarify ordering guarantees. * layout.rs: update stale cursor-position comment to reflect the new chunk index (7 not 6) after inserting the queued_hint row. * queued_hint::truncate_to_width: emit a single '...' when the budget is too narrow to fit even one char (e.g. wide-glyph + max_cells=1). Adds truncate_wide_char_with_narrow_budget_emits_ellipsis test. * clear_chat_history: tighten comment grammar. * Fix CodeQL spelling: 'pre-empt' -> 'preempt' in test assertion. 282 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reserve 1 cell upfront for the ellipsis instead of post-trimming after appending. The old algorithm could shave off the ellipsis itself (max_cells==5 with 5 chars left no room) and could yield strings whose display width exceeded max_cells when the last grapheme carried zero-width combining marks. New algorithm: 1. Fast path: if UnicodeWidthStr::width(text) <= max_cells, return as-is. 2. Otherwise, content_budget = max_cells - 1. Emit chars until budget exceeded, then append the ellipsis. Result width is provably <= max_cells. 3. max_cells == 0 -> empty; max_cells == 1 with any overflow -> bare ellipsis. Adds 2 new tests covering wide-char and combining-mark cases. 284 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds input.queue.indicator, input.queue.removed, and input.queue.full to all 88 non-English locale files (including pseudo-locales qps-ploc, qps-ploca, qps-plocm). Comment block is translated alongside the values. Placeholders (%{count}, %{preview}, %{cap}) and the Esc keybind label are preserved verbatim per the {Locked="Esc"} directive.
Per .github/instructions/rust-localization.instructions.md, all locale files must include every key from en-US.yml. This closes the localization gap flagged in Copilot review round 3.
284 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* queue_hint_height: doc comment reworded ("for the current tab" instead of "for tab") since the function takes &App and reads current_tab() internally.
* truncate_to_width fast-path comment: clarified that UnicodeWidthStr semantics are correct for the fast path (combining marks add 0 visible columns) and the "zero-width to 1 cell" treatment only matters inside the truncation loop for break-decision budgeting.
* truncate_over_width_inserts_ellipsis test: comment updated to reflect the reserve-1-for-ellipsis algorithm; added a width assertion to pin the contract.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In terminals where area.width < HORIZONTAL_PADDING (i.e. < 2 cells), the previous code rendered a row of pure padding because truncate_to_width returned an empty string while the format!(" {}", ...) still prepended two literal spaces.
Now the padding shrinks to whatever room is left after the truncated body, so even at width=1 we render the single visible cell from truncate_to_width (a bare ellipsis) instead of silently blanking the indicator.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three follow-ups to the round-6 Copilot advisories (suppressed low-confidence): 1. Single truncation owner: QueuedPrompt::preview was clipping at char count + ellipsis, then ui/queued_hint::truncate_to_width was clipping again at cell width + ellipsis — risk of doubled "..." on the same string. Split responsibilities: QueuedPrompt::collapsed_text only collapses whitespace; queued_hint owns all width-aware truncation. preview() stays for the transient hint where char-based clipping is fine. 2. Narrow-pane fix: when area.width is 1 or 2 the previous code passed budget=0 to truncate_to_width, which returned "" and the row rendered as pure padding. Now budget is floored at 1 whenever area.width >= 1, so the truncation marker is always visible. 3. drain_pending_prompts early-out: a single any()-scan skips the keys-clone + sort entirely when no tab has anything queued. Runs after every UI tick; common case stays allocation-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Cache collapsed preview at enqueue time. QueuedPrompt now holds a pre-collapsed `collapsed: String` populated by `QueuedPrompt::new(text)`. `ui/queued_hint::render` runs every frame; the old `split_whitespace().collect::<Vec<_>>().join(" ")` allocated a temporary Vec on every draw. The new `collapse_whitespace` helper streams chars into the output string with no intermediate Vec, allocating only the final result once at enqueue.
2. Gate background-tab drain on session_id. `drain_pending_prompts` was using `DEFAULT_TAB_ID` as a fallback when a tab had no ACP session id yet. That fallback resolves via `tab_for_session` → `self.tab_id` (the focused tab), so a background tab draining before its `SessionAttached` arrived would route `turn_submit_prompt` to the wrong tab. Now the fallback is only allowed when the draining tab IS the focused one; otherwise the queue is held until the tab's `SessionAttached` arrives (next event tick retries).
Adds `drain_holds_queue_when_tab_has_no_session_id` regression test. 285 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. input_cursor_position now mirrors render's chat_height clamp. Previously render did `chat_estimate.min(chat_max)` accounting for everything reserved below (rec/perm/input/hints/queue), but input_cursor_position used `chat_estimate` raw. When chat overflowed the available area, the input chunk index ended up at a different Y in the two layouts and the cursor landed on the wrong row. Replicates the same `reserved_below` computation and `chat_estimate.min(chat_max)` clamp. 2. input_cursor_position now reserves a hint row for both welcome_hint and transient_hint (was: transient only). Mirrors render's logic so the cursor doesn't drift by 1 row while the first-run welcome hint is visible. Suppressed-low-confidence advisory from the same review. 3. queued_hint::render uses HORIZONTAL_PADDING constant instead of hard-coded 2 for the prefix clamp, so the padding budget stays in lockstep if the constant ever changes. 285 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per user feedback: "Esc should stop for the 'thinking', i.e. dequeue. Just like the comment /stop, but since we have a queue, that's why I said dequeue." Esc now acts like /stop on a busy state — cancels the current Thinking… (the head of the dispatch queue). With queued items behind it, the per-tick drain hook promotes the next one on the following event tick, so repeated Esc presses peel work off the head one-by-one. Priority cascade on Esc (input empty): 1. Existing overlay dismissals (help, notifications, recommendations/autofix, suggested-pane). 2. NEW: in-flight turn (Submitted/Streaming/Surfaced+end_pending) -> cancel via shared cancel_in_flight_turn() helper. Queue preserved; auto-drain promotes the next item on the next tick. 3. Queue non-empty + idle + input empty -> pop LIFO (back of queue, undo). 4. Fallback -> clear input. Refactor: extracted cancel_in_flight_turn() from the Ctrl+C handler so both entry points share the same cancel logic. Falls back to DEFAULT_TAB_ID when the focused tab has no ACP session id yet — same convention the Enter dispatch path uses, so the local state machine still resets correctly even before SessionAttached. Test changes: - esc_pops_back_of_queue_when_input_empty renamed/refocused to esc_pops_back_of_queue_when_idle_and_input_empty (cancels in-flight first so subsequent Esc presses target the queue). - esc_does_not_pop_queue_when_input_is_not_empty now also asserts the in-flight turn is preserved when the draft is non-empty. - NEW esc_cancels_in_flight_turn_when_input_empty pins the /stop behavior and the "queue survives one Esc" contract. 286 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User reported scenario: 1. Made an error (autofix triggers, autofix turn in flight) 2. Typed a follow-up question (queued) 3. Pressed Esc once Expected: queue.pop() of the user question, autofix continues. Actual (previous): autofix was cancelled — Esc cancelled the head before the queue was empty. Root cause: my last change put cancel_in_flight_turn AHEAD of the queue-pop branch, AND the pre-existing recommendations/autofix branch also bundled "autofix in flight" into card-dismiss. Both bypassed the queue. Fix: * Reorder the Esc cascade so queue-pop runs BEFORE in-flight cancel. With a non-empty queue, Esc peels the most-recently queued item off the back (LIFO) and leaves the head running. Once the queue is empty, the next Esc cancels the head. * Split the recommendations branch from the autofix-in-flight branch: card-dismiss still has priority (the card blocks drain), but autofix-in-flight is now handled by the generic in-flight cancel below (which yields to queue-pop). New cascade on Esc (input empty): 1. selected_completed_turn_idx -> clear selection 2. help_overlay_visible -> hide 3. show_notification_banner -> dismiss 4. recommendations().is_some() -> turn_cancel (card dismiss) 5. suggested_pane_id.is_some() -> clear pane indicator 6. NEW PRIORITY: pending_prompts non-empty -> pop_back (LIFO) 7. in-flight (Submitted/Streaming/Surfaced+end_pending) -> cancel_in_flight_turn (covers autofix and user prompts) 8. fallback -> clear_input Tests: * NEW esc_pops_queue_before_cancelling_in_flight — single Esc with queue=1 + in-flight pops queue, head survives. * NEW esc_cancels_autofix_only_when_queue_is_empty — pins the reported autofix scenario: first Esc pops queue, autofix continues; second Esc cancels autofix. * esc_cancels_in_flight_turn_when_input_empty tightened to start with an empty queue (otherwise it would test the queue-pop path now). * Existing esc_pops_back_of_queue_when_idle_and_input_empty unchanged. 288 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. QueuedPrompt::preview(0) now returns String::new() instead of "…" (1 char), honoring the documented "at most max_chars" contract. Added queued_prompt_preview_zero_budget_is_empty regression test. 2. queued_hint::HORIZONTAL_PADDING: comment reworded to describe the actual behavior (two-cell left indent used by the prefix and subtracted from the truncation budget) — was misleadingly claiming it mirrored layout left/right padding. 289 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves conflict in tools/wta/src/app.rs Esc handler. Main #42 bundled "autofix in-flight" into the recommendations Esc branch as part of the per-tab autofix refactor. This branch deliberately keeps that branch recommendations-only — the in-flight autofix case is handled by the generic cancel_in_flight_turn below, which yields to queue-pop first so the user can peel queued prompts they typed during autofix. Test updated to use the new per-tab autofix state (tab.autofix.pane_id / tab.autofix.generation) introduced by main #42. 303 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. cancel_in_flight_turn doc comment now reflects the real Esc cascade: queue-pop runs BEFORE the helper, so Esc only cancels the head when pending_prompts is empty (or the call comes from Ctrl+C, which has no queue-pop short-circuit). Previous comment misleadingly claimed "each Esc cancels the current head; the queue keeps rotating". 2. PR description rewritten to match the actual queue-pop-first behavior. Includes a "Why queue-pop beats cancel-head" rationale explaining the autofix-with-queue scenario. 3. Spelling: renamed drain_noops_when_disconnected -> drain_no_ops_when_disconnected and adjusted the preview test fixture to " hello \n\n world " (extra spaces around literals) so the spell checker no longer reads "\nworld" as the word "nworld". 303 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per Copilot review: the no-session-id branch only cleared autofix state but didn't reset the turn, so the card would persist on a brand-new tab whose recommendation surfaced before SessionAttached arrived. Now mirrors cancel_in_flight_turn: when tab.session_id is None, fall back to DEFAULT_TAB_ID which resolves via tab_for_session -> self.tab_id to the focused tab. turn_cancel then dismisses the card consistently in both branches. 303 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. QueuedPrompt::collapsed is now capped at COLLAPSED_PREVIEW_CAP = 256 chars (well over the ~60-cell visible indicator). collapse_whitespace_capped breaks out of the scan as soon as the cap is reached, so a paste-large prompt no longer costs unbounded allocation/scanning on every UI frame through t!() interpolation. Full text remains intact on QueuedPrompt::text for ACP dispatch — only the rendering cache is capped. Test: queued_prompt_caps_collapsed_storage. 2. Pinning the per-tab isolation contract with esc_only_pops_focused_tabs_queue_not_background_tabs: pressing Esc on the focused tab only touches its own queue + in-flight head; a background tab's queue and in-flight state stay untouched. Confirms the design audit: - Esc handler -> current_tab_mut().pending_prompts.pop_back() - Esc handler -> cancel_in_flight_turn() (uses current_tab()) - Enter handler -> current_tab_mut().pending_prompts.push_back(...) - clear_chat_history() -> self.pending_prompts.clear() (caller picks tab via current_tab_mut()) - drain_pending_prompts iterates tabs but resolves each tab's own session_id; never cross-routes. 305 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The truncate_respects_width_with_combining_marks test used "a\u{0301}bcdef"
as the input — the trailing "bcdef" was arbitrary filler to force the
truncation loop past the 1-cell budget, but the spell checker (correctly)
flagged it as an unrecognized word.
Swapped to "a\u{0301}1234". Digits don't trip check-spelling, and the test
contract is unchanged: one grapheme `á` followed by *something* that
pushes past max_cells=1 to verify the combining-mark width handling.
Added a comment explaining why the trailing chars are deliberately
digits so future maintainers don't wonder either.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removed the reference to a non-existent `PREVIEW_MAX_CHARS` constant (it was deleted in an earlier review round when we switched to width-based truncation via `truncate_to_width`). The cap is an O(n) defense against unbounded per-frame work; the actual visible width is bounded by `area.width` via the renderer's width-based truncation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…queue # Conflicts: # tools/wta/locales/am-ET.yml # tools/wta/src/app.rs # tools/wta/src/ui/layout.rs # tools/wta/src/ui/mod.rs
There was a problem hiding this comment.
Pull request overview
Adds a per-tab pending-prompt queue to the WTA TUI so user input isn’t discarded when a tab is busy, with automatic FIFO draining when the tab becomes ready again and an Esc cascade that dequeues queued prompts (LIFO) before cancelling the in-flight turn. This also introduces a one-row “Queued (N): …” indicator above the input box and localizes the new strings across all WTA locales.
Changes:
- Add
TabSession::pending_promptsplus enqueue/drain/cancel behavior inApp(Enter queues while busy; Esc pops queued prompts before cancelling head; per-tick drain). - Add a new queued-hint UI row and update layout to reserve/render it above the input box.
- Add
input.queue.*localization keys across alltools/wta/locales/*.yml.
Reviewed changes
Copilot reviewed 93 out of 93 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/wta/src/app.rs | Implements queued prompt storage, drain hook, and updated Enter/Esc/Ctrl+C behaviors with unit tests. |
| tools/wta/src/ui/layout.rs | Reserves and renders a queued-hint row above the input; updates popup anchoring. |
| tools/wta/src/ui/mod.rs | Wires the new queued_hint UI module. |
| tools/wta/src/ui/queued_hint.rs | New width-aware queued-hint row renderer and truncation tests. |
| tools/wta/locales/af-ZA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/am-ET.yml | Adds input.queue.* localized strings (also includes broader unrelated string churn). |
| tools/wta/locales/ar-SA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/as-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/az-Latn-AZ.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/bg-BG.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/bn-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/bs-Latn-BA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ca-Es-VALENCIA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ca-ES.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/cs-CZ.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/cy-GB.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/da-DK.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/de-DE.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/el-GR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/en-GB.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/en-US.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/es-ES.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/es-MX.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/et-EE.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/eu-ES.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/fa-IR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/fi-FI.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/fil-PH.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/fr-CA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/fr-FR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ga-IE.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/gd-gb.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/gl-ES.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/gu-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/he-IL.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/hi-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/hr-HR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/hu-HU.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/hy-AM.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/id-ID.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/is-IS.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/it-IT.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ja-JP.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ka-GE.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/kk-KZ.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/km-KH.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/kn-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ko-KR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/kok-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/lb-LU.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/lo-LA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/lt-LT.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/lv-LV.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/mi-NZ.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/mk-MK.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ml-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/mr-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ms-MY.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/mt-MT.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/nb-NO.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ne-NP.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/nl-NL.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/nn-NO.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/or-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/pa-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/pl-PL.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/pt-BR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/pt-PT.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/qps-ploc.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/qps-ploca.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/qps-plocm.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/quz-PE.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ro-RO.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ru-RU.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sk-SK.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sl-SI.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sq-AL.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sr-Cyrl-BA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sr-Cyrl-RS.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sr-Latn-RS.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/sv-SE.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ta-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/te-IN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/th-TH.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/tr-TR.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/tt-RU.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ug-CN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/uk-UA.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/ur-PK.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/uz-Latn-UZ.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/vi-VN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/zh-CN.yml | Adds input.queue.* localized strings. |
| tools/wta/locales/zh-TW.yml | Adds input.queue.* localized strings. |
…prompts Closes coverage gaps in the per-tab queue feature around the cross-tab iteration path in drain_pending_prompts (the one place where a regression could mis-route or starve a tab): - drain_dispatches_to_multiple_idle_tabs_in_same_tick: two idle tabs both drain in a single tick, each with their own pane_context.tab_id. - drain_dispatches_only_idle_tabs_leaves_busy_tabs_queued: a Submitted tab in the iteration is skipped without losing its queued prompt. - drain_per_tab_fifo_independent_across_tabs: per-tab FIFO holds with cross-tab fairness — one head per idle tab per tick. - clear_chat_history_on_one_tab_preserves_other_tabs_queue: pins the per-TabSession isolation contract for reset paths. All 30 queue/drain/esc/clear_chat_history tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- am-ET.yml: restore main's translations + layout.welcome_hint (Ctrl+Shift+/); the original conflict resolution kept the stale branch-side file, dropping 31 keys (welcome_disclaimer, connection.connecting_activity, commands.model.summary, time.*, etc.) that main had added in the 152 intervening commits. Rebuilt from main + inserted the three input.queue.* Amharic translations after input.placeholder.disconnected. - layout.rs: comment above command_popup said the input box is chunks[6]; it's now chunks[7] (chunks[6] is the queued-hint row). Comment-only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Treat loading_session as 'busy' on the Enter-submit path so it queues instead of dispatching. During session/load replay, tab.turn stays Idle (no TurnState::Submitted is created for replay) so accepts_new_prompt() returns true — Enter would dispatch immediately and interleave a new turn with the in-progress loadSession replay chunks. drain_pending_prompts already gates on loading_session (app.rs:8141); this keeps Enter and drain symmetric on what counts as busy. Adds enter_during_session_load_queues_instead_of_dispatching pinning the new contract: Enter mid-load queues, drain holds while loading, and drain dispatches once loading completes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match the early-return guard style of other UI renderers — bail when area.width == 0 as well as area.height == 0, instead of relying on ratatui's behavior for a zero-width Rect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…calc The early-return at line 30 already short-circuits area.width==0, so the inner conditional was unreachable post-previous-commit. Simplified to a straight saturating-sub + max(1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match the convention of every other one-line hint (recommendations::render_hint, agents_view::render_footer_hint, auth::render, etc.) so RTL locales right-align the indicator rather than leaving it pinned to the left edge. The 2-cell left-padding remains an LTR stylistic indent; full RTL bidi-reordering of chat/input rows is the deferred work flagged in the PR description's 'Out of scope' section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Surfaced { end_pending: false } reports accepts_new_prompt() = true, so
the Enter handler at app.rs:6950 was dispatching typed prompts via
turn_submit_prompt instead of queuing them. turn_submit_prompt wipes
selected_recommendation and clears messages, which destroyed the
recommendation card the user was about to Run / Insert / Esc.
drain_pending_prompts already gates on recommendations().is_some()
(app.rs:8137); mirror that gate in the Enter handler so the two sides
agree on what 'busy' means. With this change, typing + Enter while a
card is staged queues the prompt; the card survives until the user
dismisses or executes it, at which point drain promotes the queued
prompt automatically.
Regression test pins the behavior with the exact scenario from the
bug report (card visible + 'Give me command for Ram' + Enter).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User-reported regression: with a draft typed in the input box and a recommendation card surfaced, pressing Enter to execute the card did nothing visible. The card-Enter handler required input.is_empty(), so Enter fell through to the prompt-dispatch path — which either wiped the card (pre-queue) or silently queued the draft (post-queue fix ffa86ed), neither of which is what the user wanted. Fix: remove the input.is_empty() requirement on the card-Enter branch. Enter on a visible card always executes the card. The pre-existing draft stays in the input untouched — once the card runs and recommendations() returns None, input_has_nav_focus flips back to true and the user can edit/send/discard the draft normally. Simpler than the dual-action approach (execute + auto-queue) that would have widened the queue-card state interaction. Draft preserved is the right model: the user typed mid-card-interaction, so they clearly want to be able to revise after the card resolves. Defensive: card-Enter falls back to DEFAULT_TAB_ID when no real session_id is bound, matching the convention in the queue branch and cancel_in_flight_turn. Tests: - enter_with_card_visible_and_empty_input_executes_card: baseline - enter_with_card_visible_and_draft_input_executes_card_preserving_draft: pins the bug report contract — draft verbatim, no queue interaction, cursor preserved, input regains nav focus post-execute 753/753 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // This intentionally runs even when the input box has | ||
| // typed text. When a card surfaces, `input_has_nav_focus` | ||
| // locks the input so the user can't type more, but any | ||
| // pre-existing draft stays in the buffer. Pressing Enter |
This comment has been minimized.
This comment has been minimized.
User-reported regression: with a recommendation card visible, Backspace, typing, Delete, and Ctrl+Backspace all silently did nothing in the input box. Cause was input_has_nav_focus() returning false when turn.recommendations().is_some() — the gate that the rest of the input key handlers consult. Original intent of the gate: 'when card is visible, focus belongs to the card; don't let keystrokes fill an invisible buffer'. But the input box stays visually present (with caret) when a card is up, so the 'invisible buffer' worry doesn't apply. Locking edits left users with a draft they couldn't even backspace, with no UI hint why. Fix: input_has_nav_focus() now only locks on permission cards and past-turn navigation. Recommendation cards still own Arrow keys (their own match arms run first), Enter (card-Enter branch executes the card regardless of input contents — fix 86fbc47), and Esc (dismisses card). Char/Backspace/Delete/Ctrl+Backspace are free to edit the input draft while the card is visible. Regression test: input_editing_works_while_recommendation_card_visible exercises Backspace, Char insertion, and Ctrl+Backspace with a surfaced card, and verifies (a) edits land on the draft, (b) the card is NOT dismissed by typing — only Esc / Enter affect the card. 754/754 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…e::Backspace User-reported regression on PR #278. Diagnosis from wta-main_helper-*.log: this build's conpty hands Backspace to wta as the DEL byte (0x7F) wrapped in \Char('\u{7f}')\ rather than as \KeyCode::Backspace\. Confirmed in helper logs dating to before any PR #278 deploy today — this is a pre-existing quirk of the dev IT build's conpty (shipped WindowsTerminal handles it correctly), not something the queue PR introduced. It only became user-visible now because the queue PR is the first feature that lets users sit on a non-empty draft long enough to want to edit it. Without normalization, the DEL byte fell into the \KeyCode::Char(c)\ arm in app.rs and was inserted into the input buffer as an invisible control character — backspace appeared dead, and each press silently bloated the draft. Fix: at the event-stream boundary in event.rs, rewrite \Char('\u{7f}')\ and \Char('\u{8}')\ (Ctrl+H BS, used by some legacy emulators) to \KeyCode::Backspace\ before producing the AppEvent. Every downstream handler sees a proper Backspace and the existing \KeyCode::Backspace\ match arms fire as intended. Same pattern used by many TUI apps targeting cross-platform terminals where conpty / xterm-variant behavior is inconsistent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@check-spelling-bot Report
|
| ❌ Errors and Warnings | Count |
|---|---|
| 54 | |
| ❌ forbidden-pattern | 1 |
See ❌ Event descriptions for more information.
These words are not needed and should be removed
Backgrounder CANTCALLOUT Ccc cplusplus ctl Debian dotnet drv endptr EOFs evt Fullwidth gitlab hdr idl IME inbox ININPUTSYNCCALL intelligentterminal Ioctl KVM lbl lld lsb NODEFAULT NONINFRINGEMENT notif oss outdir Podcast pri prioritization rcv segfault SND sourced SWP Tbl testname transitioning unk unparseable unregisters Virt VMs webpage websites WINVER xsiAvailable 📚 dictionaries could cover words (expected and unrecognized) not in the 📘 dictionary
This includes both expected items (2064) from .github/actions/spelling/expect/alphabet.txt .github/actions/spelling/expect/expect.txt .github/actions/spelling/expect/web.txt
| Dictionary | Entries | Covers | Uniquely |
|---|---|---|---|
| cspell:csharp/csharp.txt | 32 | 2 | 2 |
| cspell:aws/aws.txt | 232 | 2 | 2 |
| cspell:fonts/fonts.txt | 536 | 1 | 1 |
Consider adding to the extra_dictionaries array (in the .github/actions/spelling/config.json file):
"cspell:csharp/csharp.txt",
"cspell:aws/aws.txt",
"cspell:fonts/fonts.txt",
To stop checking additional dictionaries, put (in the .github/actions/spelling/config.json file):
"check_extra_dictionaries": []Forbidden patterns 🙅 (1)
In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.
These forbidden patterns matched content:
Should be preexisting
[Pp]re[- ]existing
✏️ Contributor please read this
By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.
If the listed items are:
- ... misspelled, then please correct them instead of using the command.
- ... names, please add them to
.github/actions/spelling/allow/names.txt. - ... APIs, you can add them to a file in
.github/actions/spelling/allow/. - ... just things you're using, please add them to an appropriate file in
.github/actions/spelling/expect/. - ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in
.github/actions/spelling/patterns/.
See the README.md in each directory for more information.
🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉
If the flagged items are 🤯 false positives
If items relate to a ...
-
binary file (or some other file you wouldn't want to check at all).
Please add a file path to the
excludes.txtfile matching the containing file.File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.
^refers to the file's path from the root of the repository, so^README\.md$would exclude README.md (on whichever branch you're using). -
well-formed pattern.
If you can write a pattern that would match it,
try adding it to thepatterns.txtfile.Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.
Note that patterns can't match multiline strings.
Reopens #46 (closed unmerged); rebuilt on top of current
main(152 commits behind at revival; merge commitda6802685).Summary
Adds a per-tab pending-prompt queue to the WTA TUI. Today, pressing Enter while a turn is in flight surfaces a "Agent is busy on this tab — wait for the current prompt to finish." system message and discards the user''s typing. With this change:
pending_promptsqueue and clear the input box.accepts_new_prompt() && recommendations().is_none() && !loading_session) the front of the queue is dispatched automatically.pending_prompts(LIFO undo) — if the queue is non-empty, Esc rescinds the most-recently queued prompt. The in-flight head (whether autofix or a user turn) keeps running. Repeated Esc presses peel queued prompts off the back one by one./stop— only fires once the queue is empty.Queued (N): preview…rendered directly above the input box.PENDING_PROMPT_QUEUE_CAP = 20. Over-cap keeps the user''s input intact and shows a transient hint instead of silently dropping it./clear,/new,/restart, tab reset and session-load all funnel throughclear_chat_history(), which now dropspending_promptsso stale prompts can''t fire into a fresh session.Design notes
cancel_in_flight_turn()helper shared between Ctrl+C and the new Esc branch. Falls back toDEFAULT_TAB_IDwhen the focused tab has no ACP session id yet — same convention the Enter dispatch path uses, so the local state machine still resets correctly even beforeSessionAttached.ui_rxandevent_rxpaths) rather than wired into each individualTurnStatetransition. Error paths (TabError,AgentError), stale-autofix discards, and the autofixIgnoredecision are all covered uniformly without scattering callsites.turn_submit_promptflips state toSubmitted, so a second pass would no-op. Per-tab FIFO order is deterministic; cross-tab order is sorted alphabetically for log reproducibility.Surfaced { end_pending: false })accepts_new_prompt()returns true, but a queued prompt going throughturn_submit_promptwould clearselected_recommendationand wipe the card before the user can Run / Insert / dismiss.tab_sessions, resolves session-id per tab. Background tabs without an ACPsession_iddefer drain (avoids thetab_for_session(DEFAULT_TAB_ID)→ focused-tab misroute).QueuedPrompt::new()precomputes the whitespace-collapsed text once at enqueue so the per-frame UI render is allocation-free.Merge-with-main notes (vs. original #46)
Major drift reconciled when bringing this back on top of current
main(152 commits later):App::newsignature grew — addedrename_session_tx,master_tx,Arc<ShellManager>. Test helpers updated.TabSessiongainedagents_view+pane_openalongside the PR''s newpending_promptsfield.input_cursor_positiondeleted on main (caret is now an inverse buffer cell). Branch''s edit to that function was dropped as dead code; cursor placement no longer needs adjusting around the queue indicator row.turn_cancel(DEFAULT_TAB_ID)(which handles autofix cleanup) instead of duplicating the cleanup inline.No PR features dropped.
Tests
cargo test --target x86_64-pc-windows-msvc --manifest-path tools/wta/Cargo.toml— 751 / 751 passing (725 baseline + 26 new tests added by this PR).Core queue mechanics —
tools/wta/src/app.rsenter_while_busy_queues_prompt_without_user_bubbleenter_during_session_load_queues_instead_of_dispatchingloadSessionEnter queues instead of racing the replay; drain holds while loading; drain dispatches once load completesdrain_dispatches_queued_prompts_fifo_when_turn_completesturn_canceldrain_skips_when_recommendation_card_visibleSurfaced { end_pending: false })drain_skips_tab_with_pending_permission_requestpermissionqueue are skippeddrain_no_ops_when_disconnectedqueue_full_keeps_input_and_emits_hintPENDING_PROMPT_QUEUE_CAP=20) enforced; input preserved on overflowclear_chat_history_drops_pending_prompts/clear,/new,/restart, session-load reset all wipe queueEsc / Ctrl+C /
/stopcascadeesc_pops_queue_before_cancelling_in_flightesc_pops_back_of_queue_when_idle_and_input_emptyesc_cancels_in_flight_turn_when_input_empty/stopesc_cancels_autofix_only_when_queue_is_emptyesc_does_not_pop_queue_when_input_is_not_emptyesc_cancel_in_flight_with_empty_queue_still_clears_correctlyctrl_c_clears_pending_prompts_queue_and_blocks_auto_dispatchslash_stop_clears_pending_prompts_queue/stopsemantics match Ctrl+C (single source of truth viacancel_in_flight_turn)Per-tab / multi-tab isolation (no cross-tab leakage)
esc_only_pops_focused_tabs_queue_not_background_tabsdrain_uses_loop_tab_id_for_pane_context_not_focused_tabdrain_holds_queue_when_tab_has_no_session_idsession_iddefer drain rather than misroute viaDEFAULT_TAB_IDdrain_dispatches_to_multiple_idle_tabs_in_same_tickdrain_dispatches_only_idle_tabs_leaves_busy_tabs_queueddrain_per_tab_fifo_independent_across_tabsclear_chat_history_on_one_tab_preserves_other_tabs_queuepending_promptsQueuedPromptdata structurequeued_prompt_preview_truncates_and_collapses_whitespacequeued_prompt_preview_zero_budget_is_emptypreview(0)honors the char-budget contractqueued_prompt_caps_collapsed_storagequeued_hintwidth-aware truncation —tools/wta/src/ui/queued_hint.rstruncate_under_width_keeps_stringtruncate_over_width_inserts_ellipsis…truncate_zero_width_returns_emptymax_cells=0→ empty string (defensive)truncate_respects_width_with_combining_marksa\u{0301})truncate_wide_char_with_narrow_budget_emits_ellipsismax_cells=1→ bare…truncate_wide_char_with_two_cell_budget_emits_ellipsis_only…)Files
tools/wta/src/app.rsQueuedPrompttype with cachedcollapsed,pending_promptsfield, Enter enqueue, Esc cascade (queue-pop → cancel-in-flight → clear-input),cancel_in_flight_turn()helper shared with Ctrl+C,drain_pending_prompts(), drain hooks inrun(), 13 new unit teststools/wta/src/ui/queued_hint.rsQueued (N): preview…row with width-correct truncation (handles CJK, combining marks, narrow panes)tools/wta/src/ui/layout.rschat_estimate.min(chat_max)clamp + welcome-hint reservationtools/wta/src/ui/mod.rsqueued_hinttools/wta/locales/*.ymlinput.queue.indicator/removed/fullkeys in all 89 localesOut of scope