@deepseek-ai/dsh-client-ui-commands 0.1.6-alpha.1 → 0.1.6-alpha.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 CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-commands/README.md
5
- README.md: f802179e08009cd4db8a3b786e52e81b26bb21da
6
- README.zh.md: 1dee58ab42d67a62099d57d4e631e440d3349f21
5
+ README.md: f8cba29179f9960042a4a9e67e86496591a76d08
6
+ README.zh.md: 1cea3ae7aea857f2b2d219abf0a7192c2f222792
package/README.md CHANGED
@@ -25,7 +25,7 @@ Typing a `/` command opens a registered popup, a client action, a host command's
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
- Mount this plugin alongside `ui-input-trigger` and `ui-conversation`; the `/` source then appears in the trigger menu, and business packages register their command surfaces through `ctx.commandUi`. Typing `/model` opens the registered popup; a host command with an argument claim opens its input or executes directly. The composer's `+` button and a typed `/` open the same menu: an Add section (File, Goal, Plan, Feedback) and a Commands section (Compact, Permission, Model, Export) in usage order, each row with a glyph, a localized title and description, and the command name as an alias where the localized title differs from it.
28
+ Mount this plugin alongside `ui-input-trigger` and `ui-conversation`; the `/` source then appears in the trigger menu, and business packages register their command surfaces through `ctx.commandUi`. Typing `/model` opens the registered popup; a host command with an argument claim opens its input or executes directly. The popup holds composer focus: typing filters the loaded rows locally, `↑`/`↓` walk them, Enter and `Tab` accept the highlighted row, and Escape and `Shift+Tab` return to the composer. The highlight opens on the row the options mark as the session's current value, so accepting on a freshly opened panel confirms it. The composer's `+` button and a typed `/` open the same menu: an Add section (File, Goal, Plan, Feedback) and a Commands section (Compact, Permission, Model, Export) in usage order, each row with a glyph, a localized title and description, and the command name as an alias where the localized title differs from it.
29
29
 
30
30
  ### Kinds and decorations
31
31
 
package/README.zh.md CHANGED
@@ -25,7 +25,7 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 与 `ui-input-trigger` 及 `ui-conversation` 一起挂载本插件;`/` source 随即出现在触发菜单中,业务包经 `ctx.commandUi` 注册自己的命令表面。键入 `/model` 打开已注册的弹窗;带参数声明的宿主命令打开其输入或直接执行。composer 的 `+` 按钮与键入的 `/` 打开同一个菜单:「添加」小节(文件、目标、计划、反馈)与「指令」小节(压缩、权限、模型、下载日志)按使用频次排列,每行带图标、本地化的标题与说明,本地化标题与命令名不同时还显示命令名作为别名。
28
+ 与 `ui-input-trigger` 及 `ui-conversation` 一起挂载本插件;`/` source 随即出现在触发菜单中,业务包经 `ctx.commandUi` 注册自己的命令表面。键入 `/model` 打开已注册的弹窗;带参数声明的宿主命令打开其输入或直接执行。弹窗持有 composer 焦点:键入即在已加载的行上本地筛选,`↑`/`↓` 在行间移动,回车与 `Tab` 接受高亮行,Escape 与 `Shift+Tab` 把焦点还给 composer。高亮落在选项标记为会话当前值的行上,因此在刚打开的弹窗上接受即确认当前值。composer 的 `+` 按钮与键入的 `/` 打开同一个菜单:「添加」小节(文件、目标、计划、反馈)与「指令」小节(压缩、权限、模型、下载日志)按使用频次排列,每行带图标、本地化的标题与说明,本地化标题与命令名不同时还显示命令名作为别名。
29
29
 
30
30
  ### 种类与装饰
31
31
 
package/lib/client.js CHANGED
@@ -9,6 +9,69 @@ window.__ModuleLoader__.load({
9
9
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  let react = require("react");
12
+ //#region ../../util/values/src/index.ts
13
+ /**
14
+ * Weak-key lookup with a strongly retained iterable set of associated values.
15
+ *
16
+ * Each value must belong to only one key. The container performs no automatic
17
+ * cleanup; owners delete associations or clear the container at lifecycle end.
18
+ */
19
+ var WeakMapWithValues = class {
20
+ keys = /* @__PURE__ */ new WeakMap();
21
+ valueSet = /* @__PURE__ */ new Set();
22
+ /** Live strongly retained values in insertion order. */
23
+ values = this.valueSet;
24
+ /**
25
+ * Read the value associated with a key.
26
+ * @param key - weakly held lookup key.
27
+ * @returns the associated value, or absence.
28
+ */
29
+ get(key) {
30
+ return this.keys.get(key);
31
+ }
32
+ /**
33
+ * Test whether a key has an association.
34
+ * @param key - weakly held lookup key.
35
+ * @returns whether the key is present.
36
+ */
37
+ has(key) {
38
+ return this.keys.has(key);
39
+ }
40
+ /**
41
+ * Associate one key with one caller-unique value.
42
+ * @param key - weakly held lookup key.
43
+ * @param value - strongly retained value that belongs to no other key.
44
+ * @returns this container.
45
+ */
46
+ set(key, value) {
47
+ if (this.keys.has(key)) {
48
+ const previous = this.keys.get(key);
49
+ if (previous === value) return this;
50
+ this.valueSet.delete(previous);
51
+ }
52
+ this.keys.set(key, value);
53
+ this.valueSet.add(value);
54
+ return this;
55
+ }
56
+ /**
57
+ * Remove one association and its strongly retained value.
58
+ * @param key - weakly held lookup key.
59
+ * @returns whether an association was removed.
60
+ */
61
+ delete(key) {
62
+ if (!this.keys.has(key)) return false;
63
+ const value = this.keys.get(key);
64
+ const deleted = this.keys.delete(key);
65
+ this.valueSet.delete(value);
66
+ return deleted;
67
+ }
68
+ /** Remove every association and strongly retained value. */
69
+ clear() {
70
+ this.keys = /* @__PURE__ */ new WeakMap();
71
+ this.valueSet.clear();
72
+ }
73
+ };
74
+ //#endregion
12
75
  //#region lib/types/client/locales.js
13
76
  /**
14
77
  * `command` namespace dictionaries: the composer menu's section headings,
@@ -307,6 +370,19 @@ window.__ModuleLoader__.load({
307
370
  if (query === "") return options;
308
371
  return options.filter((o) => o.label.toLowerCase().includes(query) || (o.detail?.toLowerCase().includes(query) ?? false));
309
372
  }
373
+ /**
374
+ * Highlight index for a freshly loaded row list: the row marked as the current
375
+ * value when the live search still shows it, else the top row. Opening parks
376
+ * the highlight on the value the session already uses, so an accept gesture
377
+ * made without looking confirms that value instead of the topmost row.
378
+ * @param options - the loaded rows.
379
+ * @param search - the shell's live filter text (non-empty after a retry).
380
+ * @returns index into the filtered rows.
381
+ */
382
+ function currentIndex(options, search) {
383
+ const at = filterOptions(options, search).findIndex((option) => option.active === true);
384
+ return at === -1 ? 0 : at;
385
+ }
310
386
  /** The shell's error-strip line for a settlement failure. */
311
387
  function errorText(error) {
312
388
  return error instanceof Error ? error.message : String(error);
@@ -358,11 +434,12 @@ window.__ModuleLoader__.load({
358
434
  load(binding) {
359
435
  binding.spec.options(binding.context, binding.abort.signal).then((options) => {
360
436
  if (this.binding !== binding) return;
437
+ const current = this.state.getSnapshot();
361
438
  this.state.set({
362
- ...this.state.getSnapshot(),
439
+ ...current,
363
440
  status: "ready",
364
441
  options,
365
- active: 0,
442
+ active: currentIndex(options, current.search),
366
443
  error: null
367
444
  });
368
445
  }, (error) => {
@@ -391,7 +468,8 @@ window.__ModuleLoader__.load({
391
468
  }
392
469
  /**
393
470
  * Replace the local search text (pure local filter — the provider is never
394
- * re-queried) and rebase the highlight onto the new filtered list.
471
+ * re-queried) and rebase the highlight to the top of the new filtered list:
472
+ * typing searches for something other than the current value.
395
473
  * @param search - the shell search input's text.
396
474
  */
397
475
  setSearch(search) {
@@ -645,7 +723,7 @@ window.__ModuleLoader__.load({
645
723
  live = {
646
724
  contributions: /* @__PURE__ */ new Map(),
647
725
  decorations: /* @__PURE__ */ new Map(),
648
- popups: /* @__PURE__ */ new Map()
726
+ popups: new WeakMapWithValues()
649
727
  };
650
728
  /** `command`-namespace translator (composer refusal notices). */
651
729
  t;
@@ -731,24 +809,27 @@ window.__ModuleLoader__.load({
731
809
  * @param name - command name without the leading slash.
732
810
  */
733
811
  dismiss(name) {
734
- for (const popup of this.live.popups.values()) if (popup.state.getSnapshot().command === name) popup.dismiss();
812
+ for (const popup of this.live.popups.values) if (popup.state.getSnapshot().command === name) popup.dismiss({ focusComposer: true });
735
813
  }
736
814
  /**
737
815
  * Resolve the per-session popup controller (lazy; dies with the session
738
816
  * scope). The controller's consume callback dispatches the scoped
739
817
  * consume-token event back to this session; focusComposer reaches the
740
- * composer through the overlay slot currency.
818
+ * session's composer through the conversation input face.
741
819
  * @param actx - session-scope ctx.
742
820
  * @returns the resident controller.
821
+ * @throws when the Context no longer belongs to a retained Session generation.
743
822
  */
744
823
  popupFor(actx) {
745
- const id = this.sessions().scopeOf(actx);
746
- if (id === void 0) throw new Error("command.popupFor requires a session scope");
824
+ const sessions = this.sessions();
825
+ const session = sessions.sessionOf(actx);
826
+ const binding = session === void 0 ? void 0 : sessions.binding(session.sessionId);
827
+ if (binding === void 0 || binding.session !== session) throw new Error("command.popupFor requires a retained Session scope");
747
828
  const { popups } = this.live;
748
- const existing = popups.get(id);
829
+ const existing = popups.get(binding);
749
830
  if (existing !== void 0) return existing;
750
831
  const controller = new PopupSelectController({
751
- consume: (segment) => actx.bail(actx, "slash/input-consume-token", { guard: segment.via === "menu" ? {
832
+ consume: (segment) => binding.ctx.bail(binding.ctx, "slash/input-consume-token", { guard: segment.via === "menu" ? {
752
833
  kind: "span",
753
834
  span: segment.span
754
835
  } : {
@@ -756,31 +837,16 @@ window.__ModuleLoader__.load({
756
837
  token: segment.token
757
838
  } }) === true,
758
839
  focusComposer: () => {
759
- this.focusHooks.get(id)?.();
840
+ binding.ctx.get("conversation")?.input.for(binding.ctx).focus();
760
841
  }
761
842
  });
762
- popups.set(id, controller);
763
- actx.effect(() => () => {
843
+ popups.set(binding, controller);
844
+ binding.ctx.effect(() => () => {
764
845
  controller.dispose();
765
- popups.delete(id);
766
- this.focusHooks.delete(id);
846
+ popups.delete(binding);
767
847
  }, "command: session popup");
768
848
  return controller;
769
849
  }
770
- /** Composer focus hooks by session (the overlay wiring binds the textarea focus here). */
771
- focusHooks = /* @__PURE__ */ new Map();
772
- /**
773
- * Bind one session's composer-focus hook (overlay slot wiring; unbind on unmount).
774
- * @param id - session id.
775
- * @param focus - textarea focus callback.
776
- * @returns the unbind disposer.
777
- */
778
- bindComposerFocus(id, focus) {
779
- this.focusHooks.set(id, focus);
780
- return () => {
781
- if (this.focusHooks.get(id) === focus) this.focusHooks.delete(id);
782
- };
783
- }
784
850
  /**
785
851
  * Menu candidates: host catalog + contribution availability, built-in rows
786
852
  * localized, then position filtering; sections for an empty query, the
@@ -1087,11 +1153,12 @@ window.__ModuleLoader__.load({
1087
1153
  * store into the conversation.input.overlay anchor. Unlike the slash menu
1088
1154
  * (combobox — textarea keeps focus), this shell HOLDS focus while open: the
1089
1155
  * inner search input takes focus, plain typing filters the loaded options
1090
- * locally, Enter/↑↓ drive the filtered highlight (scrolled into view), Escape
1091
- * dismisses back to the composer, and ←→ keep the search input's native
1092
- * caret. Any pointer interaction outside the box dismisses (the click's own
1093
- * target takes focus). Closed state renders null; the overlay slot stays
1094
- * mounted. The card height clamps to the space above the composer.
1156
+ * locally, Enter and Tab accept the filtered highlight, ↑↓ walk it (wrapping,
1157
+ * scrolled into view), and Escape and Shift+Tab dismiss back to the composer.
1158
+ * ←→ keep the search input's native caret. Any pointer interaction outside the
1159
+ * box dismisses (the click's own target takes focus). Closed state renders
1160
+ * null; the overlay slot stays mounted. The card height clamps to the space
1161
+ * above the composer.
1095
1162
  */
1096
1163
  /** Design cap on the card height (same MenuDropdown family as the slash menu). */
1097
1164
  const MAX_HEIGHT = 320;
@@ -1145,6 +1212,12 @@ window.__ModuleLoader__.load({
1145
1212
  ev.preventDefault();
1146
1213
  popup.select(state.active);
1147
1214
  return;
1215
+ case "Tab":
1216
+ if (!ev.shiftKey && (state.status !== "ready" || rows.length === 0)) return;
1217
+ ev.preventDefault();
1218
+ if (ev.shiftKey) popup.dismiss({ focusComposer: true });
1219
+ else popup.select(state.active);
1220
+ return;
1148
1221
  case "Escape":
1149
1222
  ev.preventDefault();
1150
1223
  popup.dismiss({ focusComposer: true });
@@ -22,6 +22,12 @@ export interface SelectOption {
22
22
  /** Optional short marker rendered as a superscript beside the label. */
23
23
  readonly badge?: string;
24
24
  readonly detail?: string;
25
+ /**
26
+ * The row the shell's highlight parks on when the panel opens, so an accept
27
+ * gesture made without looking confirms the value in use. A business package
28
+ * that marks a row `active` for presentation alone would make that row the
29
+ * default pick.
30
+ */
25
31
  readonly active?: boolean;
26
32
  /** Optional in-page risk gate owned by the shared popup shell. */
27
33
  readonly confirmation?: SelectConfirmation;
@@ -51,7 +51,12 @@ export interface PopupState {
51
51
  readonly options: readonly SelectOption[];
52
52
  /** Local filter text over the loaded options. */
53
53
  readonly search: string;
54
- /** Highlight index into the filtered row list (0 when empty/pending). */
54
+ /**
55
+ * Highlight index into the filtered row list: 0 until options land; afterwards
56
+ * the row the loaded list marks as the current value
57
+ * ({@link SelectOption.active}), else 0. A search rebases it to the top of the
58
+ * filtered rows.
59
+ */
55
60
  readonly active: number;
56
61
  /** A select() settlement is in flight: further select/search/highlight no-op until it settles. */
57
62
  readonly submitting: boolean;
@@ -101,7 +106,8 @@ export declare class PopupSelectController<TCtx = unknown> {
101
106
  retry(): void;
102
107
  /**
103
108
  * Replace the local search text (pure local filter — the provider is never
104
- * re-queried) and rebase the highlight onto the new filtered list.
109
+ * re-queried) and rebase the highlight to the top of the new filtered list:
110
+ * typing searches for something other than the current value.
105
111
  * @param search - the shell search input's text.
106
112
  */
107
113
  setSearch(search: string): void;
@@ -68,20 +68,12 @@ export declare class CommandUiRuntime extends Service implements CommandUiContra
68
68
  * Resolve the per-session popup controller (lazy; dies with the session
69
69
  * scope). The controller's consume callback dispatches the scoped
70
70
  * consume-token event back to this session; focusComposer reaches the
71
- * composer through the overlay slot currency.
71
+ * session's composer through the conversation input face.
72
72
  * @param actx - session-scope ctx.
73
73
  * @returns the resident controller.
74
+ * @throws when the Context no longer belongs to a retained Session generation.
74
75
  */
75
76
  popupFor(actx: ClientContext): PopupSelectController<ClientSessionContext>;
76
- /** Composer focus hooks by session (the overlay wiring binds the textarea focus here). */
77
- private readonly focusHooks;
78
- /**
79
- * Bind one session's composer-focus hook (overlay slot wiring; unbind on unmount).
80
- * @param id - session id.
81
- * @param focus - textarea focus callback.
82
- * @returns the unbind disposer.
83
- */
84
- bindComposerFocus(id: SessionId, focus: () => void): () => void;
85
77
  /**
86
78
  * Menu candidates: host catalog + contribution availability, built-in rows
87
79
  * localized, then position filtering; sections for an empty query, the
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.1.6-alpha.1",
4
+ "version": "0.1.6-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -44,21 +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.1.6-alpha.1",
48
- "@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
49
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.6-alpha.1",
50
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.1",
51
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
52
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
53
- "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
54
- "@deepseek-ai/dsh-commands": "^0.1.6-alpha.1",
47
+ "@deepseek-ai/dsh-api-remotes": "^0.1.6-alpha.2",
48
+ "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.2",
49
+ "@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.2",
50
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.6-alpha.2",
51
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.2",
52
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.2",
53
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.6-alpha.2",
54
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.2",
55
+ "@deepseek-ai/dsh-commands": "^0.1.6-alpha.2",
55
56
  "@deepseek-ai/cordis": "^4.0.2",
56
- "@deepseek-ai/dsh-api-session-controller": "^0.1.6-alpha.1",
57
- "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1",
58
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
59
- "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.6-alpha.1",
60
- "@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.1",
61
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1"
57
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.6-alpha.2",
58
+ "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.2",
59
+ "@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
60
+ "@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
61
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.2",
62
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.2"
62
63
  },
63
64
  "files": [
64
65
  "lib/index.js",