@deepseek-ai/dsh-client-ui-commands 0.2.0-rc.1 → 0.2.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +4 -4
- package/README.md +7 -1
- package/README.zh.md +7 -1
- package/lib/client.js +111 -50
- package/lib/types/client/contract.d.ts +24 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/option-groups.d.ts +15 -0
- package/lib/types/client/popup.d.ts +11 -6
- package/package.json +16 -16
package/README.i18n.yaml
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
en: 01adce72791579fa
|
|
19
19
|
zh: 70eb55a1529038e7
|
|
20
20
|
/deepseek-ai-dsh-client-ui-commands/use-this-package/kinds-and-decorations:
|
|
21
|
-
en:
|
|
22
|
-
zh:
|
|
21
|
+
en: 57679a20574d8b3f
|
|
22
|
+
zh: 98d5e5a7781e6de1
|
|
23
23
|
/deepseek-ai-dsh-client-ui-commands/use-this-package/built-in-row-faces:
|
|
24
24
|
en: e96156be95ea87e3
|
|
25
25
|
zh: f5350db26483ec9f
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
en: 4feecdbec0c3f39e
|
|
28
28
|
zh: 618944ecade8a3a2
|
|
29
29
|
/deepseek-ai-dsh-client-ui-commands/understand-the-implementation:
|
|
30
|
-
en:
|
|
31
|
-
zh:
|
|
30
|
+
en: 3aeeba6c91c3ef19
|
|
31
|
+
zh: d3afb3df6d15e17b
|
|
32
32
|
/deepseek-ai-dsh-client-ui-commands/further-exploration:
|
|
33
33
|
en: 409c04f534d23de7
|
|
34
34
|
zh: 165022d7a0a554eb
|
package/README.md
CHANGED
|
@@ -31,6 +31,12 @@ Mount this plugin alongside `ui-input-trigger` and `ui-conversation`; the `/` so
|
|
|
31
31
|
|
|
32
32
|
A contribution is a client-owned command; a host-name collision fails loudly. Its UI is a popupSelect spec or an action: a callback a bare invocation runs after the trigger token is consumed, without submitting a message. Business packages own their actions and availability; the composer registers File through this same API. A decoration adds a bare-invocation popup or action to an existing host command while preserving its catalog row, argument claim, and lifecycle logging; it never fires without a matching host row. Menu queries fuzzy-match ordered, case-insensitive subsequences of command names and titles, with prefixes first and no section headings.
|
|
33
33
|
|
|
34
|
+
A popupSelect spec may supply `searchLabels()` to resolve its placeholder, empty-catalog text, and no-match text for each opening. Omitted labels retain the shell's generic copy; labels from one popup never carry into the next.
|
|
35
|
+
|
|
36
|
+
Options may carry `group: { name, label }`: `name` identifies the group and `label` supplies its localized sticky heading. Equal names share one group; groups retain first-occurrence order in the loaded options, including when a query hides earlier rows. Ungrouped options form one headingless block at their first occurrence. Groups without matching rows are hidden.
|
|
37
|
+
|
|
38
|
+
The optional `searchMode` defaults to `'substring'`, matching `label` and `detail` case-insensitively without ranking. `'fuzzy-label'` matches only labels as ordered, case-insensitive subsequences and ranks within each group: prefixes first, then alignment score, then original row order. Both modes ignore leading and trailing query spaces; a blank query keeps every row in group order. Rendering, keyboard navigation, and selection use the same filtered order. `/model` opts into `'fuzzy-label'`; other commands retain substring matching unless they opt in.
|
|
39
|
+
|
|
34
40
|
### Built-in row faces
|
|
35
41
|
|
|
36
42
|
First-party command definitions carry stable `definitionId` values. The client selects their localized titles, descriptions, icons, and input spellings by identity; changing a Host description cannot change that selection. Same-name overrides without the matching identity keep their own copy and receive no first-party aliases. Chinese and English spellings resolve through the same effective Session catalog in every locale, preserving the typed spelling in the draft and submitting the registered Host name. Contributions supply their own `label`, `description`, and `icon`, read on every candidate pass. Empty-query section order follows names, with unlisted rows closing Commands.
|
|
@@ -44,7 +50,7 @@ When the composer submits with images or generic files, only a host command decl
|
|
|
44
50
|
<a id="understand-the-implementation"></a>
|
|
45
51
|
## Understand the implementation
|
|
46
52
|
|
|
47
|
-
Menus use the shared `MenuSurface` material, including the macOS backing for background blur; custom content follows the [menu rules](../../../docs/web-styling.md#component-rules).
|
|
53
|
+
Popup selection panels fill the composer overlay's width, matching the slash menu. Popup search fields keep transparent backgrounds and borders in both palettes. Grouped options use [ui-primitives](../ui-primitives/README.md#understand-the-implementation)' shared `MenuGroup` headings and `observeStickyMenuGroups`; an ordinary effect owns the asynchronous observation and rebuilds it when rendered groups change. Menus use the shared `MenuSurface` material, including the macOS backing for background blur; custom content follows the [menu rules](../../../docs/web-styling.md#component-rules).
|
|
48
54
|
|
|
49
55
|
<details>
|
|
50
56
|
<summary>Implementation internals — click to expand</summary>
|
package/README.zh.md
CHANGED
|
@@ -31,6 +31,12 @@ kind: "package-reference"
|
|
|
31
31
|
|
|
32
32
|
贡献项是客户端自有命令,与宿主命令同名会明确报错。它的 UI 是 popupSelect 规格或动作:裸调用消费触发 token 后运行回调,不提交消息。业务包负责自己的动作及可用性,输入框通过同一 API 注册「文件」。装饰为已有宿主命令添加裸调用弹窗或动作,并保留其目录行、参数认领与生命周期记录;没有匹配的宿主行时不触发。菜单查询按顺序、不区分大小写地模糊匹配命令名与标题的子序列,前缀优先,不显示小节标题。
|
|
33
33
|
|
|
34
|
+
popupSelect 规格可提供 `searchLabels()`,在每次打开时解析占位文字、空目录提示和无匹配提示。未提供时保留外壳的通用文案;一个弹窗的文案不会沿用到下一个弹窗。
|
|
35
|
+
|
|
36
|
+
选项可携带 `group: { name, label }`:`name` 标识分组,`label` 提供本地化吸顶标题。同名选项归入同一组;分组保持在已加载选项中首次出现的顺序,即使查询隐藏了较早的行也不变。未分组选项在首次出现的位置组成一个无标题区块。没有匹配行的分组会隐藏。
|
|
37
|
+
|
|
38
|
+
可选的 `searchMode` 默认为 `'substring'`,不区分大小写地匹配 `label` 与 `detail` 的子串,不进行相关性排序。`'fuzzy-label'` 仅匹配标签中按顺序、不区分大小写的子序列,并在组内排序:前缀优先,其次按匹配得分,最后按原始行顺序。两种模式均忽略查询首尾空格;空查询按分组顺序保留所有行。渲染、键盘导航与选择使用相同的筛选顺序。`/model` 启用 `'fuzzy-label'`;其他命令未显式启用时仍使用子串匹配。
|
|
39
|
+
|
|
34
40
|
### 内置行的展示面
|
|
35
41
|
|
|
36
42
|
内置命令定义携带稳定的 `definitionId`。客户端按标识选择本地化标题、说明、图标和输入写法,修改宿主说明不会改变选择结果。没有匹配标识的同名覆盖保留自己的文案,也不获得内置别名。在任何界面语言下,中英文写法都通过同一个会话有效目录解析,草稿保留手输写法,提交使用宿主注册名。贡献项提供自己的 `label`、`description` 和 `icon`,每次生成候选项时读取。空查询按名称确定小节顺序,未列出的行排在「指令」末尾。
|
|
@@ -44,7 +50,7 @@ composer 携带图片或通用文件提交时,只有声明了 `input.attachmen
|
|
|
44
50
|
<a id="understand-the-implementation"></a>
|
|
45
51
|
## 理解实现
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
弹窗选择面板撑满 composer 浮层宽度,与斜杠菜单一致。弹窗搜索框在浅/深色主题下均保持背景和边框透明。分组选项使用 [ui-primitives](../ui-primitives/README.zh.md#understand-the-implementation) 共享的 `MenuGroup` 标题与 `observeStickyMenuGroups`;普通 effect 负责异步观察,并在渲染分组变化时清理和重建观察器。菜单采用共享 `MenuSurface` 材质,包括用于背景模糊的 macOS 底层;自定义内容遵循[菜单规则](../../../docs/web-styling.zh.md#component-rules)。
|
|
48
54
|
|
|
49
55
|
<details>
|
|
50
56
|
<summary>实现细节——点击展开</summary>
|
package/lib/client.js
CHANGED
|
@@ -334,6 +334,27 @@ window.__ModuleLoader__.load({
|
|
|
334
334
|
return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("command directory wait aborted");
|
|
335
335
|
}
|
|
336
336
|
//#endregion
|
|
337
|
+
//#region lib/types/client/option-groups.js
|
|
338
|
+
/**
|
|
339
|
+
* Group rows by their caller-owned group name, without sorting groups or rows.
|
|
340
|
+
* Ungrouped rows occupy one block at their first occurrence.
|
|
341
|
+
* @param options - Options in display order.
|
|
342
|
+
* @returns groups in first-occurrence order, with original option objects.
|
|
343
|
+
*/
|
|
344
|
+
function groupOptions(options) {
|
|
345
|
+
const groups = /* @__PURE__ */ new Map();
|
|
346
|
+
for (const option of options) {
|
|
347
|
+
const name = option.group?.name;
|
|
348
|
+
const existing = groups.get(name);
|
|
349
|
+
if (existing !== void 0) existing.rows.push(option);
|
|
350
|
+
else groups.set(name, {
|
|
351
|
+
group: option.group,
|
|
352
|
+
rows: [option]
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return [...groups.values()];
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
337
358
|
//#region lib/types/client/popup.js
|
|
338
359
|
/**
|
|
339
360
|
* Headless popupSelect shell state: one controller per client
|
|
@@ -352,6 +373,8 @@ window.__ModuleLoader__.load({
|
|
|
352
373
|
status: "pending",
|
|
353
374
|
options: [],
|
|
354
375
|
search: "",
|
|
376
|
+
searchLabels: null,
|
|
377
|
+
searchMode: "substring",
|
|
355
378
|
active: 0,
|
|
356
379
|
submitting: false,
|
|
357
380
|
confirming: null,
|
|
@@ -359,16 +382,23 @@ window.__ModuleLoader__.load({
|
|
|
359
382
|
error: null
|
|
360
383
|
};
|
|
361
384
|
/**
|
|
362
|
-
* Filter
|
|
363
|
-
*
|
|
385
|
+
* Filter rows using substring matching, or rank labels fuzzily within each group.
|
|
386
|
+
* Blank search keeps every row; fuzzy matching preserves group order.
|
|
364
387
|
* @param options - the loaded rows.
|
|
365
388
|
* @param search - the shell's search text.
|
|
366
|
-
* @
|
|
389
|
+
* @param mode - the command's policy; defaults to substring over label and detail.
|
|
390
|
+
* @returns the original option objects in the order shown and used for selection.
|
|
367
391
|
*/
|
|
368
|
-
function filterOptions(options, search) {
|
|
392
|
+
function filterOptions(options, search, mode = "substring") {
|
|
369
393
|
const query = search.trim().toLowerCase();
|
|
370
|
-
|
|
371
|
-
|
|
394
|
+
const groups = groupOptions(options);
|
|
395
|
+
const ordered = groups.some((group) => group.group !== void 0) ? groups.flatMap((group) => group.rows) : options;
|
|
396
|
+
if (query === "") return ordered;
|
|
397
|
+
if (mode === "fuzzy-label") return groups.flatMap((group) => (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.rows.map((option) => ({
|
|
398
|
+
name: option.label,
|
|
399
|
+
option
|
|
400
|
+
})), query).map((row) => row.option));
|
|
401
|
+
return ordered.filter((o) => o.label.toLowerCase().includes(query) || (o.detail?.toLowerCase().includes(query) ?? false));
|
|
372
402
|
}
|
|
373
403
|
/**
|
|
374
404
|
* Highlight index for a freshly loaded row list: the row marked as the current
|
|
@@ -377,10 +407,11 @@ window.__ModuleLoader__.load({
|
|
|
377
407
|
* made without looking confirms that value instead of the topmost row.
|
|
378
408
|
* @param options - the loaded rows.
|
|
379
409
|
* @param search - the shell's live filter text (non-empty after a retry).
|
|
410
|
+
* @param mode - the command's search policy.
|
|
380
411
|
* @returns index into the filtered rows.
|
|
381
412
|
*/
|
|
382
|
-
function currentIndex(options, search) {
|
|
383
|
-
const at = filterOptions(options, search).findIndex((option) => option.active === true);
|
|
413
|
+
function currentIndex(options, search, mode) {
|
|
414
|
+
const at = filterOptions(options, search, mode).findIndex((option) => option.active === true);
|
|
384
415
|
return at === -1 ? 0 : at;
|
|
385
416
|
}
|
|
386
417
|
/** The shell's error-strip line for a settlement failure. */
|
|
@@ -414,6 +445,7 @@ window.__ModuleLoader__.load({
|
|
|
414
445
|
* @param segment - open-time token segment snapshot for post-select consumption.
|
|
415
446
|
*/
|
|
416
447
|
open(command, spec, context, segment) {
|
|
448
|
+
const searchLabels = spec.searchLabels?.() ?? null;
|
|
417
449
|
this.binding?.abort.abort();
|
|
418
450
|
const binding = {
|
|
419
451
|
command,
|
|
@@ -426,7 +458,9 @@ window.__ModuleLoader__.load({
|
|
|
426
458
|
this.state.set({
|
|
427
459
|
...CLOSED,
|
|
428
460
|
open: true,
|
|
429
|
-
command
|
|
461
|
+
command,
|
|
462
|
+
searchLabels,
|
|
463
|
+
searchMode: spec.searchMode ?? "substring"
|
|
430
464
|
});
|
|
431
465
|
this.load(binding);
|
|
432
466
|
}
|
|
@@ -439,7 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
439
473
|
...current,
|
|
440
474
|
status: "ready",
|
|
441
475
|
options,
|
|
442
|
-
active: currentIndex(options, current.search),
|
|
476
|
+
active: currentIndex(options, current.search, current.searchMode),
|
|
443
477
|
error: null
|
|
444
478
|
});
|
|
445
479
|
}, (error) => {
|
|
@@ -489,7 +523,7 @@ window.__ModuleLoader__.load({
|
|
|
489
523
|
move(dir) {
|
|
490
524
|
const s = this.state.getSnapshot();
|
|
491
525
|
if (!s.open || s.status !== "ready" || s.submitting || s.confirming !== null) return;
|
|
492
|
-
const rows = filterOptions(s.options, s.search);
|
|
526
|
+
const rows = filterOptions(s.options, s.search, s.searchMode);
|
|
493
527
|
if (rows.length === 0) return;
|
|
494
528
|
const active = (s.active + dir + rows.length) % rows.length;
|
|
495
529
|
this.state.set({
|
|
@@ -505,7 +539,7 @@ window.__ModuleLoader__.load({
|
|
|
505
539
|
highlight(index) {
|
|
506
540
|
const s = this.state.getSnapshot();
|
|
507
541
|
if (!s.open || s.status !== "ready" || s.submitting || s.confirming !== null) return;
|
|
508
|
-
if (index < 0 || index >= filterOptions(s.options, s.search).length || index === s.active) return;
|
|
542
|
+
if (index < 0 || index >= filterOptions(s.options, s.search, s.searchMode).length || index === s.active) return;
|
|
509
543
|
this.state.set({
|
|
510
544
|
...s,
|
|
511
545
|
active: index
|
|
@@ -525,7 +559,7 @@ window.__ModuleLoader__.load({
|
|
|
525
559
|
const binding = this.binding;
|
|
526
560
|
const s = this.state.getSnapshot();
|
|
527
561
|
if (binding === null || !s.open || s.status !== "ready" || s.submitting || s.confirming !== null) return;
|
|
528
|
-
const option = filterOptions(s.options, s.search)[index];
|
|
562
|
+
const option = filterOptions(s.options, s.search, s.searchMode)[index];
|
|
529
563
|
if (option === void 0) return;
|
|
530
564
|
if (option.confirmation !== void 0) {
|
|
531
565
|
this.state.set({
|
|
@@ -1129,7 +1163,7 @@ window.__ModuleLoader__.load({
|
|
|
1129
1163
|
}
|
|
1130
1164
|
//#endregion
|
|
1131
1165
|
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-commands/src/client/PopupSelectView.module.css.mjs
|
|
1132
|
-
const css = ".mufS8W_card{z-index:100;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);min-width:min(220px,100%);max-width:100%;max-height:320px;box-shadow:var(--dsw-elevation-prominent);border:0;outline:none;flex-direction:column;padding:3px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;overflow:hidden}.mufS8W_viewport{flex-direction:column;min-height:0;display:flex;overflow-y:auto}.mufS8W_row{border-radius:var(--dsw-radius-md);cursor:pointer;color:var(--dsw-alias-label-primary);align-items:center;gap:6px;padding:5px 7px;font-size:12px;display:flex}.mufS8W_rowActive{background:var(--dsw-alias-interactive-bg-hover)}.mufS8W_label{flex:0 auto;align-items:baseline;gap:4px;min-width:0;display:flex}.mufS8W_labelText{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.mufS8W_badge{color:var(--dsw-alias-label-tertiary);letter-spacing:.2px;flex:none;align-self:flex-start;margin-top:-1px;font-size:8px;font-weight:600;line-height:10px}.mufS8W_detail{min-width:0;color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;flex:1;font-size:11px;overflow:hidden}.mufS8W_check{color:var(--dsw-alias-label-primary);flex:none;margin-left:auto;display:inline-flex}.mufS8W_check svg{width:14px;height:14px}.mufS8W_status{color:var(--dsw-alias-label-tertiary);padding:7px 8px;font-size:12px}.mufS8W_search{border
|
|
1166
|
+
const css = ".mufS8W_card{box-sizing:border-box;z-index:100;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);min-width:min(220px,100%);max-width:100%;max-height:320px;box-shadow:var(--dsw-elevation-prominent);border:0;outline:none;flex-direction:column;padding:3px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;right:0;overflow:hidden}.mufS8W_viewport{flex-direction:column;min-height:0;display:flex;overflow-y:auto}.mufS8W_row{border-radius:var(--dsw-radius-md);cursor:pointer;color:var(--dsw-alias-label-primary);align-items:center;gap:6px;padding:5px 7px;font-size:12px;display:flex}.mufS8W_rowActive{background:var(--dsw-alias-interactive-bg-hover)}.mufS8W_label{flex:0 auto;align-items:baseline;gap:4px;min-width:0;display:flex}.mufS8W_labelText{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.mufS8W_badge{color:var(--dsw-alias-label-tertiary);letter-spacing:.2px;flex:none;align-self:flex-start;margin-top:-1px;font-size:8px;font-weight:600;line-height:10px}.mufS8W_detail{min-width:0;color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;flex:1;font-size:11px;overflow:hidden}.mufS8W_check{color:var(--dsw-alias-label-primary);flex:none;margin-left:auto;display:inline-flex}.mufS8W_check svg{width:14px;height:14px}.mufS8W_status{color:var(--dsw-alias-label-tertiary);padding:7px 8px;font-size:12px}.mufS8W_search{border-radius:var(--dsw-radius-md);color:var(--dsw-alias-label-primary);background:0 0;border:.5px solid #0000;outline:none;margin:2px 2px 3px;padding:5px 7px;font-size:12px}.mufS8W_error{color:var(--dsw-alias-state-error-primary);align-items:center;gap:6px;padding:5px 7px;font-size:11px;display:flex}.mufS8W_errorText{text-overflow:ellipsis;flex:1;overflow:hidden}.mufS8W_retry{border:.5px solid var(--dsw-alias-border-inverted);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;padding:2px 7px;font-size:11px}";
|
|
1133
1167
|
const tagId = "@deepseek-ai/dsh-client-ui-commands/PopupSelectView.module.css";
|
|
1134
1168
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1135
1169
|
const tag = document.createElement("style");
|
|
@@ -1179,12 +1213,33 @@ window.__ModuleLoader__.load({
|
|
|
1179
1213
|
const state = (0, react.useSyncExternalStore)((fn) => popup.state.subscribe(fn), () => popup.state.getSnapshot());
|
|
1180
1214
|
const cardRef = (0, react.useRef)(null);
|
|
1181
1215
|
const searchRef = (0, react.useRef)(null);
|
|
1216
|
+
const viewportRef = (0, react.useRef)(null);
|
|
1217
|
+
const rows = (0, react.useMemo)(() => filterOptions(state.options, state.search, state.searchMode), [
|
|
1218
|
+
state.options,
|
|
1219
|
+
state.search,
|
|
1220
|
+
state.searchMode
|
|
1221
|
+
]);
|
|
1222
|
+
const groups = (0, react.useMemo)(() => groupOptions(rows), [rows]);
|
|
1223
|
+
(0, react.useEffect)(() => {
|
|
1224
|
+
const viewport = viewportRef.current;
|
|
1225
|
+
if (viewport === null) return;
|
|
1226
|
+
return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
|
|
1227
|
+
}, [
|
|
1228
|
+
state.open,
|
|
1229
|
+
state.status,
|
|
1230
|
+
state.confirming,
|
|
1231
|
+
groups
|
|
1232
|
+
]);
|
|
1182
1233
|
const maxHeight = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredMaxHeight)(cardRef, MAX_HEIGHT, state);
|
|
1183
1234
|
const active = state.open ? state.active : null;
|
|
1184
1235
|
(0, react.useEffect)(() => {
|
|
1185
1236
|
if (active === null) return;
|
|
1186
1237
|
cardRef.current?.querySelector("[aria-selected=\"true\"]")?.scrollIntoView({ block: "nearest" });
|
|
1187
|
-
}, [
|
|
1238
|
+
}, [
|
|
1239
|
+
active,
|
|
1240
|
+
rows,
|
|
1241
|
+
state.confirming
|
|
1242
|
+
]);
|
|
1188
1243
|
(0, react.useEffect)(() => {
|
|
1189
1244
|
if (!state.open || state.confirming !== null) return;
|
|
1190
1245
|
const onPointerDown = (ev) => {
|
|
@@ -1204,8 +1259,8 @@ window.__ModuleLoader__.load({
|
|
|
1204
1259
|
if (state.open && state.confirming === null) searchRef.current?.focus();
|
|
1205
1260
|
}, [state.open, state.confirming]);
|
|
1206
1261
|
if (!state.open) return null;
|
|
1207
|
-
const rows = filterOptions(state.options, state.search);
|
|
1208
1262
|
const confirmation = state.confirming?.confirmation;
|
|
1263
|
+
const emptyLabel = state.searchLabels === null ? t("status.empty") : state.options.length === 0 ? state.searchLabels.empty : state.searchLabels.noResults;
|
|
1209
1264
|
const onKeyDown = (ev) => {
|
|
1210
1265
|
switch (ev.key) {
|
|
1211
1266
|
case "ArrowDown":
|
|
@@ -1233,6 +1288,39 @@ window.__ModuleLoader__.load({
|
|
|
1233
1288
|
default:
|
|
1234
1289
|
}
|
|
1235
1290
|
};
|
|
1291
|
+
const renderOption = (option, index) => (0, react_jsx_runtime.jsxs)("div", {
|
|
1292
|
+
role: "option",
|
|
1293
|
+
"aria-selected": index === state.active,
|
|
1294
|
+
"aria-label": option.badge === void 0 ? void 0 : `${option.label} ${option.badge}`,
|
|
1295
|
+
className: clsx(PopupSelectView_module_css_default.row, index === state.active && PopupSelectView_module_css_default.rowActive),
|
|
1296
|
+
onClick: () => {
|
|
1297
|
+
popup.select(index);
|
|
1298
|
+
},
|
|
1299
|
+
onMouseEnter: () => {
|
|
1300
|
+
popup.highlight(index);
|
|
1301
|
+
},
|
|
1302
|
+
children: [
|
|
1303
|
+
(0, react_jsx_runtime.jsxs)("span", {
|
|
1304
|
+
className: PopupSelectView_module_css_default.label,
|
|
1305
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1306
|
+
className: PopupSelectView_module_css_default.labelText,
|
|
1307
|
+
children: option.label
|
|
1308
|
+
}), option.badge !== void 0 && (0, react_jsx_runtime.jsx)("sup", {
|
|
1309
|
+
className: PopupSelectView_module_css_default.badge,
|
|
1310
|
+
children: option.badge
|
|
1311
|
+
})]
|
|
1312
|
+
}),
|
|
1313
|
+
option.detail !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
1314
|
+
className: PopupSelectView_module_css_default.detail,
|
|
1315
|
+
children: option.detail
|
|
1316
|
+
}),
|
|
1317
|
+
option.active === true && (0, react_jsx_runtime.jsx)("span", {
|
|
1318
|
+
className: PopupSelectView_module_css_default.check,
|
|
1319
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})
|
|
1320
|
+
})
|
|
1321
|
+
]
|
|
1322
|
+
}, option.id);
|
|
1323
|
+
let optionIndex = 0;
|
|
1236
1324
|
return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.confirming === null && (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
1237
1325
|
ref: cardRef,
|
|
1238
1326
|
className: PopupSelectView_module_css_default.card,
|
|
@@ -1244,7 +1332,7 @@ window.__ModuleLoader__.load({
|
|
|
1244
1332
|
ref: searchRef,
|
|
1245
1333
|
className: PopupSelectView_module_css_default.search,
|
|
1246
1334
|
type: "text",
|
|
1247
|
-
placeholder: t("search.placeholder"),
|
|
1335
|
+
placeholder: state.searchLabels?.placeholder ?? t("search.placeholder"),
|
|
1248
1336
|
"aria-label": t("search.aria"),
|
|
1249
1337
|
value: state.search,
|
|
1250
1338
|
readOnly: state.submitting,
|
|
@@ -1277,44 +1365,17 @@ window.__ModuleLoader__.load({
|
|
|
1277
1365
|
}),
|
|
1278
1366
|
state.status === "ready" && rows.length === 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
1279
1367
|
className: PopupSelectView_module_css_default.status,
|
|
1280
|
-
children:
|
|
1368
|
+
children: emptyLabel
|
|
1281
1369
|
}),
|
|
1282
1370
|
state.status === "ready" && (0, react_jsx_runtime.jsx)("div", {
|
|
1371
|
+
ref: viewportRef,
|
|
1283
1372
|
role: "listbox",
|
|
1284
1373
|
"aria-label": t("listbox.aria", { command: String(state.command) }),
|
|
1285
1374
|
className: PopupSelectView_module_css_default.viewport,
|
|
1286
|
-
children:
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
className: clsx(PopupSelectView_module_css_default.row, index === state.active && PopupSelectView_module_css_default.rowActive),
|
|
1291
|
-
onClick: () => {
|
|
1292
|
-
popup.select(index);
|
|
1293
|
-
},
|
|
1294
|
-
onMouseEnter: () => {
|
|
1295
|
-
popup.highlight(index);
|
|
1296
|
-
},
|
|
1297
|
-
children: [
|
|
1298
|
-
(0, react_jsx_runtime.jsxs)("span", {
|
|
1299
|
-
className: PopupSelectView_module_css_default.label,
|
|
1300
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1301
|
-
className: PopupSelectView_module_css_default.labelText,
|
|
1302
|
-
children: option.label
|
|
1303
|
-
}), option.badge !== void 0 && (0, react_jsx_runtime.jsx)("sup", {
|
|
1304
|
-
className: PopupSelectView_module_css_default.badge,
|
|
1305
|
-
children: option.badge
|
|
1306
|
-
})]
|
|
1307
|
-
}),
|
|
1308
|
-
option.detail !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
1309
|
-
className: PopupSelectView_module_css_default.detail,
|
|
1310
|
-
children: option.detail
|
|
1311
|
-
}),
|
|
1312
|
-
option.active === true && (0, react_jsx_runtime.jsx)("span", {
|
|
1313
|
-
className: PopupSelectView_module_css_default.check,
|
|
1314
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})
|
|
1315
|
-
})
|
|
1316
|
-
]
|
|
1317
|
-
}, option.id))
|
|
1375
|
+
children: groups.map(({ group, rows: groupRows }) => group === void 0 ? (0, react_jsx_runtime.jsx)(react.Fragment, { children: groupRows.map((option) => renderOption(option, optionIndex++)) }, "ungrouped") : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
|
|
1376
|
+
label: group.label,
|
|
1377
|
+
children: groupRows.map((option) => renderOption(option, optionIndex++))
|
|
1378
|
+
}, `group:${group.name}`))
|
|
1318
1379
|
})
|
|
1319
1380
|
]
|
|
1320
1381
|
}), confirmation !== void 0 && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.RiskConfirmation, {
|
|
@@ -15,6 +15,13 @@ export interface SelectConfirmation {
|
|
|
15
15
|
readonly cancelLabel: string;
|
|
16
16
|
readonly confirmLabel: string;
|
|
17
17
|
}
|
|
18
|
+
/** Caller-owned group name and localized heading for popup options. */
|
|
19
|
+
export interface SelectOptionGroup {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
}
|
|
23
|
+
/** Search behavior selected by the popup owner. */
|
|
24
|
+
export type PopupSearchMode = 'substring' | 'fuzzy-label';
|
|
18
25
|
/** One option row of a popupSelect shell. */
|
|
19
26
|
export interface SelectOption {
|
|
20
27
|
readonly id: string;
|
|
@@ -22,6 +29,8 @@ export interface SelectOption {
|
|
|
22
29
|
/** Optional short marker rendered as a superscript beside the label. */
|
|
23
30
|
readonly badge?: string;
|
|
24
31
|
readonly detail?: string;
|
|
32
|
+
/** Equal group names share a sticky heading; groups retain first-occurrence order. */
|
|
33
|
+
readonly group?: SelectOptionGroup;
|
|
25
34
|
/**
|
|
26
35
|
* The row the shell's highlight parks on when the panel opens, so an accept
|
|
27
36
|
* gesture made without looking confirms the value in use. A business package
|
|
@@ -32,6 +41,14 @@ export interface SelectOption {
|
|
|
32
41
|
/** Optional in-page risk gate owned by the shared popup shell. */
|
|
33
42
|
readonly confirmation?: SelectConfirmation;
|
|
34
43
|
}
|
|
44
|
+
/** Command-owned search copy, resolved when its popup opens. */
|
|
45
|
+
export interface PopupSearchLabels {
|
|
46
|
+
readonly placeholder: string;
|
|
47
|
+
/** Empty catalog, before filtering. */
|
|
48
|
+
readonly empty: string;
|
|
49
|
+
/** Nonempty catalog with no matching rows. */
|
|
50
|
+
readonly noResults: string;
|
|
51
|
+
}
|
|
35
52
|
/**
|
|
36
53
|
* Business registration for the popupSelect command kind. Data is
|
|
37
54
|
* self-served: options/onSelect use the business package's own protocol.
|
|
@@ -40,6 +57,13 @@ export interface SelectOption {
|
|
|
40
57
|
*/
|
|
41
58
|
export interface PopupSelectSpec {
|
|
42
59
|
readonly kind: 'popupSelect';
|
|
60
|
+
/** Defaults to substring search; fuzzy-label ranks names within each group. */
|
|
61
|
+
readonly searchMode?: PopupSearchMode;
|
|
62
|
+
/**
|
|
63
|
+
* Supply localized search copy for this opening; omitted uses the shell's generic copy.
|
|
64
|
+
* @returns search placeholder and empty-state labels.
|
|
65
|
+
*/
|
|
66
|
+
searchLabels?(): PopupSearchLabels;
|
|
43
67
|
options(session: ClientSessionContext, signal: AbortSignal): Promise<readonly SelectOption[]>;
|
|
44
68
|
onSelect(option: SelectOption, session: ClientSessionContext): void | Promise<void>;
|
|
45
69
|
}
|
|
@@ -14,7 +14,7 @@ export type { CommandDescriptor, DirectoryStatus } from './directory.ts';
|
|
|
14
14
|
export { filterOptions, PopupSelectController } from './popup.ts';
|
|
15
15
|
export type { PopupSelectDeps, PopupSpec, PopupState, TokenSegment } from './popup.ts';
|
|
16
16
|
export type { PopupSelectInjected, PopupSelectViewProps } from './PopupSelectView.tsx';
|
|
17
|
-
export type { ActionSpec, CommandContribution, CommandDecoration, CommandUiContract, CommandUiSpec, PopupSelectSpec, SelectConfirmation, SelectOption, } from './contract.ts';
|
|
17
|
+
export type { ActionSpec, CommandContribution, CommandDecoration, CommandUiContract, CommandUiSpec, PopupSearchLabels, PopupSelectSpec, SelectConfirmation, SelectOption, SelectOptionGroup, PopupSearchMode, } from './contract.ts';
|
|
18
18
|
export type { CommandKey } from './locales.ts';
|
|
19
19
|
declare module '@deepseek-ai/cordis' {
|
|
20
20
|
interface Context {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Stable grouping shared by popup filtering and rendering. */
|
|
2
|
+
import type { SelectOption, SelectOptionGroup } from './contract.ts';
|
|
3
|
+
interface OptionGroup {
|
|
4
|
+
readonly group: SelectOptionGroup | undefined;
|
|
5
|
+
readonly rows: readonly SelectOption[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Group rows by their caller-owned group name, without sorting groups or rows.
|
|
9
|
+
* Ungrouped rows occupy one block at their first occurrence.
|
|
10
|
+
* @param options - Options in display order.
|
|
11
|
+
* @returns groups in first-occurrence order, with original option objects.
|
|
12
|
+
*/
|
|
13
|
+
export declare function groupOptions(options: readonly SelectOption[]): OptionGroup[];
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=option-groups.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
2
2
|
import type { TokenSpan } from '@deepseek-ai/dsh-client-ui-input-trigger/client';
|
|
3
|
-
import type { SelectOption } from './contract.ts';
|
|
3
|
+
import type { PopupSearchLabels, PopupSearchMode, PopupSelectSpec, SelectOption } from './contract.ts';
|
|
4
4
|
/**
|
|
5
5
|
* The command token segment snapshotted at shell-open time, replayed to the
|
|
6
6
|
* injected {@link PopupSelectDeps.consume} callback after a successful
|
|
@@ -21,7 +21,7 @@ export type TokenSegment = {
|
|
|
21
21
|
* session wiring passes its session projection; the controller only carries
|
|
22
22
|
* it from open() to the callbacks).
|
|
23
23
|
*/
|
|
24
|
-
export interface PopupSpec<TCtx> {
|
|
24
|
+
export interface PopupSpec<TCtx> extends Pick<PopupSelectSpec, 'searchLabels' | 'searchMode'> {
|
|
25
25
|
/** Load the option rows once per open (retry after failure reuses the same signal). */
|
|
26
26
|
options(context: TCtx, signal: AbortSignal): Promise<readonly SelectOption[]>;
|
|
27
27
|
/** Settle the picked option against the open-time context. */
|
|
@@ -51,6 +51,10 @@ export interface PopupState {
|
|
|
51
51
|
readonly options: readonly SelectOption[];
|
|
52
52
|
/** Local filter text over the loaded options. */
|
|
53
53
|
readonly search: string;
|
|
54
|
+
/** Command-owned copy for this opening; null uses generic shell labels. */
|
|
55
|
+
readonly searchLabels: PopupSearchLabels | null;
|
|
56
|
+
/** Search policy resolved from the command spec at opening. */
|
|
57
|
+
readonly searchMode: PopupSearchMode;
|
|
54
58
|
/**
|
|
55
59
|
* Highlight index into the filtered row list: 0 until options land; afterwards
|
|
56
60
|
* the row the loaded list marks as the current value
|
|
@@ -68,13 +72,14 @@ export interface PopupState {
|
|
|
68
72
|
readonly error: string | null;
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
71
|
-
* Filter
|
|
72
|
-
*
|
|
75
|
+
* Filter rows using substring matching, or rank labels fuzzily within each group.
|
|
76
|
+
* Blank search keeps every row; fuzzy matching preserves group order.
|
|
73
77
|
* @param options - the loaded rows.
|
|
74
78
|
* @param search - the shell's search text.
|
|
75
|
-
* @
|
|
79
|
+
* @param mode - the command's policy; defaults to substring over label and detail.
|
|
80
|
+
* @returns the original option objects in the order shown and used for selection.
|
|
76
81
|
*/
|
|
77
|
-
export declare function filterOptions(options: readonly SelectOption[], search: string): readonly SelectOption[];
|
|
82
|
+
export declare function filterOptions(options: readonly SelectOption[], search: string, mode?: PopupSearchMode): readonly SelectOption[];
|
|
78
83
|
/**
|
|
79
84
|
* Headless controller of one session's popupSelect shell. Late settlements
|
|
80
85
|
* lose their write rights through binding identity: dismiss/dispose/reopen
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-commands",
|
|
3
3
|
"description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
|
|
4
|
-
"version": "0.2.0-rc.
|
|
4
|
+
"version": "0.2.0-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -44,22 +44,22 @@
|
|
|
44
44
|
"@types/react": "~18.3.1",
|
|
45
45
|
"react": "^18.2.0",
|
|
46
46
|
"clsx": "^2.0.0",
|
|
47
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.
|
|
48
|
-
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.
|
|
49
|
-
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.
|
|
50
|
-
"@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-
|
|
52
|
-
"@deepseek-ai/dsh-client-ui-
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-commands": "0.2.0-rc.1",
|
|
56
|
-
"@deepseek-ai/dsh-session": "0.2.0-rc.1",
|
|
57
|
-
"@deepseek-ai/dsh-util-values": "0.2.0-rc.1",
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.1",
|
|
60
|
-
"@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-client-test-runtime": "0.2.0-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.0-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-commands": "0.2.0-rc.2",
|
|
61
55
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2"
|
|
63
63
|
},
|
|
64
64
|
"files": [
|
|
65
65
|
"lib/index.js",
|