Skip to content

feat(web-shell): improve slash command discovery (taller menu, group counts, fuzzy search) - #6267

Merged
wenshao merged 3 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-slash-command-menu-height
Jul 3, 2026
Merged

feat(web-shell): improve slash command discovery (taller menu, group counts, fuzzy search)#6267
wenshao merged 3 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-slash-command-menu-height

Conversation

@wenshao

@wenshao wenshao commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Makes the web-shell slash-command menu easy to search and browse when there are many commands. Four changes:

  1. Taller menu — the popup was capped at exactly four rows; it now shows up to twelve (bounded by 40vh).
  2. Category headers — the existing custom/skill/system grouping was only a faint 1px divider; each group boundary now renders its category name.
  3. Per-group counts — each header shows how many commands the group holds (e.g. Skill commands 28), so the volume hidden below the fold is visible at a glance.
  4. Fuzzy search — typing now fuzzy-ranks commands with the same fzf engine the TUI already uses, so abbreviated input like mdl finds /model and arf finds /agent-reproduce-feature. An empty query keeps the grouped browse list; a non-empty query switches to a flat relevance-ranked list.

Why it's needed

With built-in commands, project commands, and skills merged together the menu holds 40+ entries (60 in this repo). Only four were ever visible, the grouping was invisible, and matching was plain substring — so mdl matched nothing and users had to know a command's exact spelling or scroll a thin list. Fuzzy ranking plus a taller, labeled, counted menu makes commands far easier to find. Fuzzy search also brings the web shell to parity with the TUI, which already uses fzf for the same commands.

Reviewer Test Plan

How to verify

  1. npm run build --workspace=@qwen-code/qwen-code-core, then from the repo root: node packages/cli/dist/index.js serve --web --port 18811 (cwd determines which project/skill commands load).
  2. Open the web UI, click the composer, type /.
    • Expected: ~9–12 rows depending on window height (was 4), with Custom / Skill / System commands headers, each showing its command count.
  3. Type an abbreviation that is not a substring, e.g. /mdl, /arf, /thm.
    • Expected: /model, /agent-reproduce-feature, /theme respectively surface — plain substring matching returns nothing for these.

Verified locally against a real qwen serve --web binary with Playwright: browse shows 60 commands grouped as Skill commands 28 / System commands 32; /mdl/model, /thm/theme, /arf includes /agent-reproduce-feature — all fuzzy-only matches with no console errors. Unit tests (slashCompletion.test.ts +2 fuzzy cases, 21 total; full web-shell suite 739 passed), prettier, eslint, and tsc -p tsconfig.lib.json (0 errors) all pass. The fzf index is built once per command set and falls back to substring filtering on error.

Evidence (Before & After)

Before — capped at 4 rows, no labels, substring-only:

before

After — browse — up to 12 rows, category headers with counts:

browse

After — fuzzy /mdl — abbreviation (not a substring) finds /model:

fuzzy-mdl

After — fuzzy /arf — finds /agent-reproduce-feature:

fuzzy-arf

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS, qwen serve --web with an isolated HOME; Playwright (headless Chromium) for screenshots and DOM assertions.

Risk & Scope

  • Main risk or tradeoff: adds fzf (^0.5.2, already a packages/cli dependency, pure-JS, browser-safe) to packages/web-shell. Fuzzy ranking changes result ordering for non-empty queries from category-first to relevance-first — intentional and matching the TUI.
  • Not validated / out of scope: Windows/Linux rendering (CSS is platform-independent). The dead slashCompletionSource CodeMirror source (not wired into the live editor) still does substring filtering; only the live getSlashCommandCompletionResult path gained fuzzy — noted in a code comment.
  • Breaking changes / migration notes: none.

Linked Issues

None.

中文说明

这个 PR 做了什么

让 web-shell 的斜杠命令菜单在命令很多时更好搜索和浏览。四项改动:

  1. 加高菜单 —— 弹层原本封顶 4 行,现在最多 12 行(上限 40vh)。
  2. 分类标题 —— 已有的自定义/Skill/系统分组此前只是一条 1px 分隔线,现在每个分组边界渲染分类名。
  3. 分组计数 —— 每个标题显示该组命令数(如 Skill commands 28),一眼看出折叠线下藏了多少。
  4. 模糊搜索 —— 输入时用 TUI 已在用的 fzf 引擎做模糊排序,所以缩写 mdl 能找到 /modelarf 能找到 /agent-reproduce-feature。空查询保持分组浏览列表,非空查询切换为按相关度排序的扁平列表。

为什么需要

内置命令、项目命令、skill 合并后菜单有 40+ 条(本仓库 60 条)。此前只露 4 条、分组不可见、匹配是纯子串——mdl 匹配不到任何东西,用户得知道命令确切拼写或滚动细列表。模糊排序 + 更高/带标题/带计数的菜单让命令好找得多。模糊搜索也让 Web Shell 与 TUI 对齐(TUI 对同一批命令早已用 fzf)。

复现与验证

  1. npm run build --workspace=@qwen-code/qwen-code-core,然后仓库根目录:node packages/cli/dist/index.js serve --web --port 18811(cwd 决定加载哪些项目命令/skill)。
  2. 打开 Web UI,点输入框,输入 /:预期随窗口高度显示约 9–12 行(原 4 行),带 自定义 / Skill / 系统 标题,每个标题显示命令数。
  3. 输入非子串的缩写,如 /mdl/arf/thm:预期分别浮现 /model/agent-reproduce-feature/theme——纯子串匹配对这些返回空。

已用真实 qwen serve --web 二进制 + Playwright 本地验证:浏览显示 60 条命令,分组为 Skill commands 28 / System commands 32;/mdl/model/thm/theme/arf/agent-reproduce-feature——全是纯模糊命中且无 console 报错。单测(slashCompletion.test.ts 新增 2 个模糊用例、共 21;完整 web-shell 套件 739 通过)、prettier、eslint、tsc -p tsconfig.lib.json(0 错误)全过。fzf 索引按命令集构建一次,失败时回退子串过滤。

风险与范围

  • 主要风险/取舍:给 packages/web-shell 增加 fzf(^0.5.2,已是 packages/cli 依赖,纯 JS、浏览器安全)。模糊排序把非空查询的结果顺序从"分类优先"改为"相关度优先"——有意为之,且与 TUI 一致。
  • 未验证/超范围:Windows/Linux 渲染(CSS 与平台无关)。死代码 slashCompletionSource(CodeMirror 源,未接入实际编辑器)仍用子串;只有活路径 getSlashCommandCompletionResult 加了模糊——已在代码注释中说明。
  • 破坏性变更/迁移说明:无。

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.
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present, bilingual, before/after screenshots included.

On direction: this solves a real UX problem — 60+ commands crammed into a 4-row menu with no labels and substring-only matching. Fuzzy search also brings the web shell to parity with the TUI, which already uses fzf for the same commands. Well-aligned with the project's goal of making the web shell a first-class experience.

On approach: the four changes (taller menu, category headers, per-group counts, fuzzy search) are tightly scoped to the stated goal. Each change is independently justified and the diff doesn't carry anything extraneous. The fzf dependency is already used in packages/core and packages/cli — adding it to packages/web-shell is consistent, not novel. The slashSectionPlan extraction into a pure function is the right call for testability.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必要章节齐全,双语,含前后对比截图。

方向:解决了真实的 UX 问题——60+ 命令挤在 4 行菜单里,没有分类标题,只有子串匹配。模糊搜索也让 Web Shell 与 TUI 对齐(TUI 已经对同一批命令使用 fzf)。与项目打造 Web Shell 一流体验的目标一致。

方案:四项改动(加高菜单、分类标题、分组计数、模糊搜索)严格围绕目标,没有夹带无关变更。fzf 已在 packages/corepackages/cli 中使用,加到 packages/web-shell 保持一致。slashSectionPlan 提取为纯函数便于测试,做法正确。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Clean implementation. The WeakMap-cached fzf index is efficient (built once per command set, not per keystroke), and the try/catch with substring fallback is good defensive coding without being over-engineered. The planSlashSectionRows extraction into a pure function with its own test file is well-done — 6 focused tests covering group boundaries, first-row-no-divider, adjacent duplicates, counts, subcommand menus, and search results without sections.

No correctness bugs, security issues, or AGENTS.md violations found. The fzf library (^0.5.2) is already a project dependency used identically in packages/cli for TUI slash completion — this PR simply brings the same engine to the web shell. The CSS changes (min(460px, 45vh)) are sensible and proportional.

Test Results

Unit tests (worktree, PR branch):

✓ utils/slashSectionPlan.test.ts (6 tests) 4ms
✓ completions/slashCompletion.test.ts (22 tests) 15ms

 Test Files  2 passed (2)
      Tests  28 passed (28)
   Duration  374ms

The full web-shell suite has 6 pre-existing failures unrelated to this PR (worktree dist/ not built, @qwen-code/acp-bridge/mcpTimeouts resolution issue).

Real-Scenario Testing (tmux)

PR code (npm run dev -- serve --web --port 18811)

> @qwen-code/qwen-code@0.19.5 dev
> node scripts/dev.js serve --web --port 18811

qwen serve: daemon log → .../debug/daemon/serve-2267524-bc521abd.log
qwen serve: Web Shell UI served from .../packages/web-shell/dist
qwen serve listening on http://127.0.0.1:18811 (mode=http-bridge, workspace=...)
qwen serve: bound to workspace "..."
qwen serve: startup timing: processToListenMs=668 runQwenServeToListenMs=36
qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
qwen serve: session reaper started (interval 60000ms, idle threshold 1800000ms)
qwen serve: /acp WebSocket transport enabled on /acp

Server starts cleanly, web shell served from PR-built dist/. curl returns 200 for GET / (4511 bytes HTML). Verified the built JS bundle (index-i7JO5zjx.js) contains: slashSectionHeader (3 refs), slashSectionCount (3 refs), and fuzzy: 'v2' + case-insensitive fzf config — confirming the PR changes are bundled and served.

Before (installed qwen v0.19.1)

Installed qwen v0.19.1 does not support serve --web, so no before/after server comparison possible. The PR author provided Playwright screenshots in the PR body showing the before (4 rows, no labels) and after (12 rows, headers, fuzzy search) states.

Note: Full UI rendering verification requires a browser (Playwright/manual) — tmux can only confirm the server starts and the bundle is correct, which it does.

中文说明

代码审查

实现干净。WeakMap 缓存的 fzf 索引高效(按命令集构建一次,而非每次按键),try/catch 加子串回退是合理的防御性编码而非过度工程。planSlashSectionRows 提取为纯函数并有独立测试文件——6 个聚焦测试覆盖分组边界、首行无分隔线、相邻重复、计数、子命令菜单、无 section 的搜索结果。

未发现正确性 bug、安全问题或 AGENTS.md 违规。fzf(^0.5.2)已是项目依赖,在 packages/cli 中用于 TUI 斜杠命令补全——本 PR 只是把同一引擎带到 Web Shell。CSS 改动(min(460px, 45vh))合理且自适应。

测试结果

单元测试 28/28 全部通过。完整 web-shell 套件有 6 个与本 PR 无关的预存失败(worktree 构建/依赖问题)。

真实场景测试

PR 代码服务器启动正常,Web Shell 从构建后的 dist/ 正常提供。curl 返回 200(4511 字节 HTML)。已验证构建的 JS 包包含 PR 的所有改动(section 标题 CSS 类、fzf 配置)。

安装的 qwen v0.19.1 不支持 serve --web,无法做前后对比。PR 作者在 PR 正文中提供了 Playwright 截图。

完整 UI 渲染验证需要浏览器——tmux 只能确认服务器启动和包内容正确,已确认。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

This is a well-scoped, well-implemented PR that solves a genuine UX problem. Before reading the diff, my independent proposal would have been: increase the menu height via CSS, add section labels with counts in the render loop, and integrate fzf (already a project dependency) for fuzzy matching — which is essentially what this PR does. The planSlashSectionRows extraction into a pure function is a nice touch I wouldn't have thought of upfront, but it's justified by the testability it enables.

Every change in the diff is needed for the stated goal — no drive-by refactors, no scope creep. The code is straightforward and I'd be happy to maintain it in six months. Tests are thorough (22 slash completion + 6 section plan), the server starts cleanly, and the bundle correctly includes the new code. The only thing I can't verify in this environment is the actual browser rendering, but the PR author provided Playwright screenshots and the CSS/JS evidence checks out.

Approving. ✅

中文说明

这是一个范围合理、实现良好的 PR,解决了真实的 UX 问题。我在看 diff 之前的独立方案是:通过 CSS 加高菜单、在渲染循环中加分类标题和计数、集成 fzf(已是项目依赖)做模糊匹配——基本上就是这个 PR 做的事。planSlashSectionRows 提取为纯函数是我事先没想到的好做法,但它带来的可测试性证明了其合理性。

diff 中每项改动都是目标所需的——没有顺手重构,没有范围蔓延。代码清晰,六个月后维护也不会头疼。测试充分(22 个斜杠补全 + 6 个 section plan),服务器启动正常,包正确包含新代码。唯一无法在此环境验证的是实际浏览器渲染,但 PR 作者提供了 Playwright 截图且 CSS/JS 证据已核实。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

ytahdn
ytahdn previously approved these changes Jul 3, 2026
Comment thread packages/web-shell/client/components/ChatEditor.tsx Outdated

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Request changes to Comment: self-PR.

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/components/ChatEditor.module.css
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.
@wenshao
wenshao dismissed stale reviews from ytahdn and qwen-code-ci-bot via 6e3bf7a July 3, 2026 13:02
@wenshao wenshao changed the title feat(web-shell): show more slash commands with category headers feat(web-shell): improve slash command discovery (taller menu, group counts, fuzzy search) Jul 3, 2026
Comment thread packages/web-shell/client/completions/slashCompletion.ts
Comment thread packages/web-shell/client/components/ChatEditor.tsx Outdated
Comment thread packages/web-shell/client/completions/slashCompletion.test.ts

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No high-confidence issues found. Downgraded from Approve to Comment: CI still running.

Minor items worth human review (terminal only, not blocking):

  • fuzzyRankCommands catch block silently falls back to substring filtering with no logging — consider adding console.warn
  • Fallback path returns unsorted results, inconsistent with pre-PR sorting behavior
  • Missing test for null result when fuzzy query matches nothing

All 739 web-shell tests pass. tsc and eslint clean.

— qwen3.7-max via Qwen Code /review

- 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.

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental review passed for new commit 5dc3a86. Changes address all review feedback:

  • Added console.warn in catch block for better error visibility
  • Added test for fuzzy search returning null when no matches
  • Refactored section counting logic into pure utility function (slashSectionPlan.ts) with comprehensive tests
  • Fixed early return ordering (section counting now happens before anchorRect check)
  • Adjusted CSS max-height to 45vh for better viewport sizing

All tests pass (28/28). LGTM ✅

@wenshao

wenshao commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI still running. Two test-coverage suggestions below; no code issues found.

— qwen3.7-max via Qwen Code /review


it('honors panel category order and filtered commands', () => {
const commands: CommandInfo[] = [
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] getCommandFzf silently deduplicates commands by name (if (byName.has(command.name)) continue), keeping only the first occurrence — but no test exercises this path. A test with duplicate command names would lock in the dedup contract and catch regressions if upstream mergeCommands behavior ever changes.

it('deduplicates commands with the same name for fuzzy search', () => {
  const commands: CommandInfo[] = [
    { name: 'model', description: 'First', source: 'builtin-command' },
    { name: 'model', description: 'Duplicate', source: 'skill-dir-command' },
    { name: 'memory', description: 'Manage memory', source: 'builtin-command' },
  ];
  const result = getSlashCommandCompletionResult(
    '/model', 6, commands, [], 'en', getTranslator('en'),
  );
  expect(result?.items).toHaveLength(1);
  expect(result?.items[0]?.detail).toBe('First');
});

— qwen3.7-max via Qwen Code /review

import { describe, expect, it } from 'vitest';
import { planSlashSectionRows } from './slashSectionPlan';

describe('planSlashSectionRows', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] All existing tests use either all-section-defined or all-section-undefined items. A mixed test — e.g. [{section: 'A'}, {section: undefined}, {section: 'B'}] — would pin the lastSection = section ?? lastSection behavior (undefined items don't break group boundaries) and catch regressions if the ?? is ever changed to a plain =.

it('handles mixed section and undefined items correctly', () => {
  const plans = planSlashSectionRows(
    [
      { section: 'Custom commands' },
      { section: undefined },
      { section: 'System commands' },
    ],
    'command',
  );
  expect(plans[0]).toMatchObject({ showHeader: true, showDivider: false, count: 1 });
  expect(plans[1]).toMatchObject({ showHeader: false, showDivider: false, count: 0 });
  expect(plans[2]).toMatchObject({ showHeader: true, showDivider: true, count: 1 });
});

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jul 3, 2026
Merged via the queue into QwenLM:main with commit 68ff698 Jul 3, 2026
175 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants