qwen-code/packages/web-shell/client/utils/slashSectionPlan.test.ts
Shaojin Wen 68ff698cd2
feat(web-shell): improve slash command discovery (taller menu, group counts, fuzzy search) (#6267)
* 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.
2026-07-03 14:56:06 +00:00

74 lines
2.2 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { planSlashSectionRows } from './slashSectionPlan';
describe('planSlashSectionRows', () => {
it('shows a header at each group boundary', () => {
const plans = planSlashSectionRows(
[
{ section: 'Custom commands' },
{ section: 'Custom commands' },
{ section: 'Skill commands' },
{ section: 'System commands' },
],
'command',
);
expect(plans.map((p) => p.showHeader)).toEqual([true, false, true, true]);
});
it('shows a header but no divider on the first row', () => {
const plans = planSlashSectionRows(
[{ section: 'Skill commands' }, { section: 'System commands' }],
'command',
);
expect(plans[0]).toMatchObject({ showHeader: true, showDivider: false });
expect(plans[1]).toMatchObject({ showHeader: true, showDivider: true });
});
it('does not repeat headers for adjacent duplicate sections', () => {
const plans = planSlashSectionRows(
[
{ section: 'System commands' },
{ section: 'System commands' },
{ section: 'System commands' },
],
'command',
);
expect(plans.filter((p) => p.showHeader)).toHaveLength(1);
});
it('reports the number of rows in each section on the header row', () => {
const plans = planSlashSectionRows(
[
{ section: 'Skill commands' },
{ section: 'Skill commands' },
{ section: 'System commands' },
],
'command',
);
expect(plans[0].count).toBe(2);
expect(plans[1].count).toBe(0);
expect(plans[2].count).toBe(1);
});
it('never groups subcommand menus', () => {
const plans = planSlashSectionRows(
[{ section: 'Skill commands' }, { section: 'System commands' }],
'subcommand',
);
expect(plans.every((p) => !p.showHeader && p.count === 0)).toBe(true);
});
it('shows no headers when items carry no section (search results)', () => {
const plans = planSlashSectionRows(
[{ section: undefined }, { section: undefined }],
'command',
);
expect(plans.every((p) => !p.showHeader)).toBe(true);
});
});