qwen-code/packages/web-shell/client/components/StatusBar.module.css
Shaojin Wen dc0706dc18
feat(web-shell): make input shortcuts discoverable and clickable (#5096)
* feat(web-shell): make input shortcuts discoverable and clickable

- Add an always-on, clickable hint row below the input: ↑ previous / ↓ next history, ctrl+r search, / commands, @ files — each click runs the matching action.
- Make the status bar "? for shortcuts" a persistent clickable button shown next to the mode indicator.
- Dismiss the shortcuts panel by clicking outside it or pressing Esc (no dedicated close button).
- Cancel reverse-i-search by clicking outside the panel (same as Esc), restoring the original draft.
- Distinguish ctrl+r (search history) from up/down (cycle history) in the shortcuts panel.
- Auto-prepend a space for mid-word @-mentions, and make the / and @ triggers idempotent so a second click re-opens the menu instead of producing "//" or "@ @".

* fix(web-shell): address PR review — touch dismissal, primary-button guard, @ idempotency

- Outside-press dismissal for the shortcuts panel and reverse-i-search now also listens for touchstart, ignores non-primary (middle/right) buttons, and respects defaultPrevented — matching the Settings/Mode inline panels.
- insertText('@') no longer inserts a duplicate '@' when the cursor sits directly before an existing '@'; it steps over the existing one and opens the menu.

* fix(web-shell): close shortcuts panel idempotently on outside-press

Touch fires touchstart plus a synthesized mousedown; a toggle onClose would reopen the panel right after closing. Use a dedicated close (set false) for the outside-press / Escape dismissal, matching the other inline panels.

* fix(web-shell): address /review — disabled guard, mid-line slash, focus, dedup

- Hide the hint row while the editor is disabled and bail the history/search callbacks on disabledRef, so the buttons can't bypass the disabled guard. insertText stays usable for App-driven injection; the now-hidden row is the only internal path that passes '/' or '@'.
- Clicking the / hint on non-empty, non-slash text replaces the content with '/' so the command menu actually opens, instead of leaving a stray mid-line '/'.
- Outside-click search dismissal no longer steals focus from the clicked target (closeSearch keepFocus=false).
- Extract a shared hintProps() helper for the five hint buttons.

* fix(web-shell): address /qreview — capture-phase Escape, hint aria-haspopup

- The shortcuts panel's Escape now wins over the App-level global Escape (capture phase + preventDefault/stopPropagation), so Esc closes the panel instead of being swallowed (clearing queued prompts / cancelling the stream) while it's open.
- Add aria-haspopup to the popup-opening hint buttons (dialog for ctrl+r, listbox for / and @), matching the sibling toolbar buttons.

* fix(web-shell): theme the slash-completion popup scrollbar

The CodeMirror autocomplete list and info panel used the browser-default (light) scrollbar, clashing with the dark theme. Apply the repo's scrollbar convention (thin + var(--border-color) thumb over a transparent track, with the -webkit fallback) so it matches in both themes.

* fix(web-shell): don't destroy the draft when clicking / on non-empty input

Replacing the document with '/' (the previous /review fix) silently wiped a typed draft. Instead, no-op when the line isn't already a command and the editor is non-empty — the slash menu needs a line-leading '/', which can't be added without either a stray mid-line '/' or clobbering the draft. Empty input still inserts '/' and opens the menu.

* fix(web-shell): guard hint history nav on multi-line input; hide hints behind dialogs

- navigatePrev/NextHistory now early-return on doc.lines > 1, matching the ArrowUp/ArrowDown keymap, so clicking the up/down hints no longer replaces a multi-line draft with a single history entry.
- showShortcutHints also requires !dialogOpen, matching the Ctrl+R keymap guard, so the hint buttons aren't interactive while a dialog is open.

* feat(web-shell): undo click-inserted / or @ on cancel; align hint nav with keymap

- Clicking the / or @ hint then pressing Escape (without typing past the inserted char) now removes the trigger too — it was clicked in, not typed. Editing past it cancels this.
- navigatePrev/NextHistory move the completion selection when the menu is open, matching the ArrowUp/ArrowDown keymap.
- The / hint no-op (non-empty draft) no longer fires startCompletion, which would pop an empty menu; a line-leading / still re-opens it.

* fix(web-shell): undo click-inserted trigger on any completion dismissal, not just Escape

Only the Escape keymap undid the click-inserted / or @. Clicking away or blurring also closes the menu but left a stray trigger behind. Watch the completion status (active -> closed) via an updateListener instead, so every dismissal path (Escape, click-away, blur) removes an untouched click-inserted trigger; the per-key special-case in the Escape handler and restarter is removed.

* feat(web-shell): grey out history hint arrows when there's nowhere to go

useInputHistory now exposes a nav { canUp, canDown } state, kept in sync on push/navigate/reset. The up/down hint buttons are disabled when there's no older entry to recall, or when not currently browsing history. Keyboard and mouse share the same state, so the affordance stays accurate however history is navigated.
2026-06-14 16:41:37 +08:00

207 lines
3.4 KiB
CSS

.bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 12px;
font-size: 13px;
color: var(--text-dimmed);
flex-shrink: 0;
}
.left,
.right {
display: flex;
align-items: center;
gap: 10px;
}
.left {
position: relative;
}
.settingsButton {
/* Sits in the bar's 2ch left-padding gutter (plus a few px of the footer
margin) so the mode label keeps its input-text alignment — the gear takes
no flex space. The gutter alone (~15.6px) is narrower than the button, so
the extra -6px buys a visible gap between the icon and the mode label
instead of the two touching. font: inherit so the -2ch resolves against
the same font as the bar's 2ch padding; the 2px padding is hit area, free
since we're out of flow. */
left: calc(-2ch - 6px);
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px;
margin: 0;
border: none;
background: none;
font: inherit;
color: var(--text-dimmed);
cursor: pointer;
}
.settingsButton:hover,
.settingsButton:focus-visible {
color: var(--text-primary);
}
.modeButton {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 0;
margin: 0;
border: none;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
}
.modeButton:hover .modeHint,
.modeButton:focus-visible .modeHint {
color: var(--text-secondary);
}
.modeLabel {
font-size: 13px;
}
.modeDefault {
color: var(--text-secondary);
}
.modePlan {
color: var(--success-color);
}
.modeAutoEdit {
color: var(--warning-color);
}
.modeAuto {
color: var(--warning-color);
}
.modeYolo {
color: var(--error-color);
}
.modeHint {
color: var(--text-dimmed);
font-size: 12px;
}
.shortcutsButton {
display: inline-flex;
align-items: center;
padding: 0;
margin: 0;
border: none;
background: none;
font: inherit;
font-size: 12px;
color: var(--text-dimmed);
cursor: pointer;
}
.shortcutsButton:hover,
.shortcutsButton:focus-visible {
color: var(--text-primary);
}
.modelButton {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0;
margin: 0;
border: none;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
}
.modelButton:hover .model,
.modelButton:focus-visible .model {
color: var(--text-primary);
text-decoration: underline;
}
.escapeHint {
color: var(--text-secondary);
font-size: 13px;
}
.model {
color: var(--text-dimmed);
font-size: 12px;
}
.disconnected {
color: var(--error-color);
}
.contextButton {
display: inline-flex;
align-items: center;
padding: 0;
margin: 0;
border: none;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
}
.contextButton:hover .context,
.contextButton:focus-visible .context {
color: var(--text-primary);
text-decoration: underline;
}
.context {
font-size: 11px;
color: var(--text-dimmed);
}
.goal {
font-size: 12px;
color: var(--accent-color);
white-space: nowrap;
}
.separator {
color: var(--text-dimmed);
}
.taskPill {
border: 0;
border-radius: 0;
padding: 0 4px;
background: transparent;
color: var(--text-dimmed);
font: inherit;
font-size: 12px;
cursor: pointer;
}
.taskPill:hover,
.taskPill:focus-visible {
background: var(--text-dimmed);
color: var(--bg-primary);
outline: none;
}
.taskPill:disabled {
cursor: default;
}
@media (max-width: 700px) {
.settingsButton,
.modelButton,
.modeHint {
display: none;
}
}