* feat(web-shell): show more slash commands with category headers
The slash-command menu capped its visible height at exactly four rows, so
with 40+ merged commands users had to scroll a thin list to find anything,
and the built-in custom/skill/system grouping was only a faint 1px divider
with no label.
Raise the cap to min(12 rows, 40vh) and render the category name as a visible
header at each group boundary (custom / skill / system), keeping the divider
between groups. Sub-command menus are ungrouped and unchanged.
* feat(web-shell): fuzzy-match slash commands and show per-group counts
Typing in the slash menu now fuzzy-ranks commands with the same fzf engine the
TUI uses, so abbreviated input like "mdl" finds "model" and "arf" finds
"agent-reproduce-feature" — substring matching alone could not. An empty query
still browses the category-ordered list; a non-empty query switches to a flat
relevance-ranked list (headers are dropped since results interleave categories).
Each category header also shows how many commands the group holds (e.g. "Skill
commands 28"), so the volume hidden below the fold is visible at a glance.
The fzf index is built once per command set (keyed on the array identity) and
falls back to substring filtering if construction fails.
* refactor(web-shell): address slash menu review feedback
- Extract the section header/divider boundary logic into a pure
`planSlashSectionRows` helper and unit-test it (headers at group
boundaries, first row header without a divider, no repeated headers for
adjacent duplicate sections, per-group counts). This also moves the
section-count computation past the `!anchorRect` early return so it no
longer runs on first render.
- Simplify `--slash-panel-max-height` to a round `min(460px, 45vh)` instead
of a `12 * rowHeight` formula that ignored header/divider overhead and so
showed only ~9-10 rows; the panel now shows ~12-13 rows.
- Log a warning when fzf fuzzy search throws before falling back to
substring matching, so a silent failure is diagnosable.
- Add a completion test for the zero-match case returning null.
* fix(web-shell): prevent queued-prompt loss from drain race
The auto-drain effect popped a queued prompt, called setQueuedPrompts,
then submitted via setTimeout(0). Because the daemon flips streamingState
asynchronously, the setState re-render could re-run the effect and pop a
second prompt before the first registered as streaming — both submitted
back-to-back and the first was lost.
Arm an "awaiting turn start" gate synchronously at pop so the re-run is
blocked until streamingState goes non-idle, released by a dedicated
effect with a safety-net timer for a prompt that never streams (e.g. a
queued slash command). Cleanup no longer cancels/re-queues the pending
submit while the gate is armed.
* feat(web-shell): friendlier Esc interruption + queued-prompt UX
* refactor(web-shell): tidy Esc/queue code per review
Behavior-preserving cleanups addressing review feedback on the Esc-interruption
and queued-prompt changes:
- Remove the now-dead queue.footer i18n key (EN + ZH) and the unreferenced
.queuedHint CSS, orphaned when the Esc-clears-queue behavior was dropped.
- Co-locate the queued-prompt styles in QueuedPromptDisplay.module.css instead
of reaching into the parent App.module.css.
- Make the Esc confirm-window constants the single source of truth: export them
from escapeIntent.ts and drive the countdown-ring duration from one of them
via a CSS custom property.
- Nudge the queue-drain safety net with a dedicated tick counter instead of
cloning queuedPrompts, so it no longer re-renders the composer for a no-op.
- Drop a redundant !compact guard in StatusBar left over from flattening a
ternary.
- Document the pop/gate-arm ordering invariant in the drain effect.