@deepseek-ai/dsh-client-ui-commands 0.1.0-rc.7 → 0.1.0-rc.8

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: 67110ffd8c1ad11e56ca9293a9064c66dd08c81d
6
- README.zh.md: 40fe21850dd289d2a5c91bd88d4f22c087731b80
5
+ README.md: 2140495a44110d5e4b33e4cc8f539959752ac185
6
+ README.zh.md: afa47cd18505b9afbd3e867d131e9796db598895
package/README.md CHANGED
@@ -8,6 +8,8 @@ Client command API (`ctx.commandUi`): the session-keyed command-directory cache,
8
8
 
9
9
  `CommandDirectory` (`src/client/directory.ts`) is the one wire-derived cache, keyed by session. Ordinary sessions fetch through `command.list({sessionId})`, and the source's scope-birth `warm` hook prewarms the session's entry. Catalog-addressed continuable children resolve an empty command directory locally: `command.list` is Agent-bound, so prewarming it would activate a child merely to view persisted history. Entries are soft-invalidated by the forwarded `commands/change` owner event (old snapshots serve while the repull flies) and by forwarded `agent-preset/selected` for that one session (recomposing an agent registers nothing, so the registry-wide signal never fires for it), hard-invalidated by `connection/reset`, and epoch-guarded so a superseded pull can never overwrite a newer one. `matchSpace` answers synchronously from this cache only; `matchEnter` strong-waits it on the SubmitAttempt signal and rejects on warmup failure — a `/` line is never silently downgraded to a plain prompt.
10
10
 
11
+ `matchEnter` also enforces the submission envelope: when the composer submits with image attachments, only a host command declaring `input.images` proceeds (its claim carries `images: true` and its submit forwards the serialized payloads to `command.execute`); every other command route — contribution popup, decorated popup, non-declaring claim, bare detached execute — throws the localized `notice.imagesUnsupported` refusal, which the input machine publishes as one error notice and the composer renders as a transient Toast banner with the draft and images retained. An image-carrying submit whose host handler answers an error result maps to an error outcome so the composer keeps the images; imageless submits keep the plain success mapping because the durable flow node owns the outcome rendering.
12
+
11
13
  After `command.execute` returns a matched command result, this browser emits local `command/executed(sessionId, name, result)`. Other clients receive the durable command nodes through the Host event stream but never this acknowledgment, so a browser-only side effect can select successful results from the client that submitted the command without treating Session replay as an action request. Listener failures are logged and contained one by one; they cannot change the already-admitted command result or prevent later listeners from running.
12
14
 
13
15
  Menu queries fuzzy-match ordered, case-insensitive subsequences of command names. Prefixes rank first; separator boundaries, adjacent characters, and shorter gaps rank the remaining matches, with directory and contribution order breaking ties. This affects discovery only: space and Enter still require an exact command name. Rationale: [Web slash-command fuzzy discovery](../../../.agents/notes/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.md).
package/README.zh.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  `CommandDirectory`(`src/client/directory.ts`)是唯一的 wire 派生缓存,以会话为 key。普通会话通过 `command.list({sessionId})` 拉取,source 的 scope 出生 `warm` 钩子会预热该会话的缓存项。由目录寻址的可继续子代理会在客户端解析为空命令目录:`command.list` 绑定 Agent,若预热它,就会仅因查看持久化历史而激活子代理。缓存项由转发的 owner 事件 `commands/change` 软失效(重拉在途期间旧快照继续服务),也由转发的 `agent-preset/selected` 对该会话单独软失效(重组 agent 不产生任何注册,注册表级信号不会为它触发),由 `connection/reset` 硬失效,并以 epoch 把关,被取代的旧拉取永远无法覆盖更新的结果。`matchSpace` 只凭该缓存同步应答;`matchEnter` 在 SubmitAttempt 信号上强等缓存,预热失败即拒绝——`/` 开头的一行绝不会被静默降级为普通提示词。
10
10
 
11
+ `matchEnter` 还强制执行提交信封:composer 携带图片附件提交时,只有声明了 `input.images` 的宿主命令继续(其 claim 携带 `images: true`,其 submit 把序列化载荷转交 `command.execute`);其余每条命令路径——contribution 弹窗、decoration 弹窗、未声明的 claim、bare 分离执行——都会抛出本地化的 `notice.imagesUnsupported` 拒绝,输入状态机发布一条错误通知,composer 以瞬态 Toast 横幅呈现它,草稿与图片原样保留。带图提交若宿主处理器返回错误结果,则映射为错误 outcome,composer 保留图片;不带图的提交维持原有的一律成功映射,因为结果呈现由持久化 flow 节点负责。
12
+
11
13
  `command.execute` 返回已匹配的命令结果后,当前浏览器会发布本地 `command/executed(sessionId, name, result)`。其他客户端只会通过 Host 事件流收到持久命令节点,不会收到这条确认,因此浏览器专属副作用可以筛选由实际提交命令的客户端收到的成功结果,而不会把 Session 回放当成操作请求。监听器失败会逐项记录并隔离,不会改变已经准入的命令结果,也不会阻止后续监听器运行。
12
14
 
13
15
  菜单查询会按顺序且不区分大小写地模糊匹配命令名的子序列。前缀排名最高;其余匹配项按分隔符边界优先、相邻字符优先、间隔越短越优先的规则排序,若仍同分,则以目录顺序和贡献项顺序打破平局。此行为只影响命令发现:space 和 Enter 仍要求命令名精确匹配。原理:[Web 斜杠命令模糊发现](../../../.agents/notes/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.md)。
package/lib/client.js CHANGED
@@ -500,12 +500,17 @@ window.__ModuleLoader__.load({
500
500
  decorations: /* @__PURE__ */ new Map(),
501
501
  popups: /* @__PURE__ */ new Map()
502
502
  };
503
+ /** `command`-namespace translator (composer refusal notices). */
504
+ t;
503
505
  /**
504
506
  * @param ctx - owning root context (plugin fiber; the service registers
505
507
  * itself as `command` and follows that fiber's lifetime).
506
508
  */
507
509
  constructor(ctx) {
508
510
  super(ctx, "commandUi");
511
+ const locale = ctx.get("locale");
512
+ if (locale === void 0) throw new Error("ui-commands: locale service unavailable");
513
+ this.t = locale.bind("command");
509
514
  this.directory = new CommandDirectory(async (sessionId) => {
510
515
  if (this.sessions().subagentAddress(sessionId) !== void 0) return [];
511
516
  const result = await ctx.remote.commands.list(sessionId);
@@ -520,7 +525,7 @@ window.__ModuleLoader__.load({
520
525
  candidates: (session, req) => this.candidates(session, req),
521
526
  onPick: (pick) => this.dispatch(pick),
522
527
  matchSpace: (session, token) => this.matchSpace(session, token),
523
- matchEnter: (session, line, signal) => this.matchEnter(session, line, signal),
528
+ matchEnter: (session, line, signal, envelope) => this.matchEnter(session, line, signal, envelope),
524
529
  warm: (session) => {
525
530
  this.directory.warm(session.sessionId);
526
531
  }
@@ -687,8 +692,14 @@ window.__ModuleLoader__.load({
687
692
  * warmup failure rejects — never a silent downgrade). Contributions and
688
693
  * bare host commands act on the bare token only; leadingInput claims
689
694
  * args-tolerant.
695
+ *
696
+ * Envelope policy: an enter submission carrying images resolves only
697
+ * through a command declaring image acceptance. Every other command route —
698
+ * popup, non-accepting claim, bare detached execute — throws the refusal
699
+ * so the machine surfaces one composer notice and the draft and images
700
+ * stay in place; nothing executes and nothing is dropped.
690
701
  */
691
- async matchEnter(session, line, signal) {
702
+ async matchEnter(session, line, signal, envelope) {
692
703
  const trimmed = line.trim();
693
704
  if (!trimmed.startsWith("/")) return void 0;
694
705
  const ws = trimmed.search(/\s/);
@@ -696,9 +707,13 @@ window.__ModuleLoader__.load({
696
707
  const bare = ws === -1;
697
708
  const name = token.slice(1);
698
709
  if (name === "") return void 0;
710
+ const refuseImages = () => {
711
+ throw new Error(this.t("notice.imagesUnsupported", { command: name }));
712
+ };
699
713
  const contribution = this.live.contributions.get(name);
700
714
  if (contribution !== void 0 && contribution.available(session)) {
701
715
  if (!bare) return void 0;
716
+ if (envelope.images > 0) refuseImages();
702
717
  this.openPopup(name, contribution.ui, session, {
703
718
  via: "enter",
704
719
  token
@@ -711,6 +726,7 @@ window.__ModuleLoader__.load({
711
726
  if (bare) {
712
727
  const decoration = this.live.decorations.get(name);
713
728
  if (decoration !== void 0 && decoration.available(session)) {
729
+ if (envelope.images > 0) refuseImages();
714
730
  this.openPopup(name, decoration.ui, session, {
715
731
  via: "enter",
716
732
  token
@@ -718,8 +734,12 @@ window.__ModuleLoader__.load({
718
734
  return "handled";
719
735
  }
720
736
  }
721
- if (desc.input !== void 0) return { claim: this.leadingClaim(desc, session) };
737
+ if (desc.input !== void 0) {
738
+ if (envelope.images > 0 && desc.input.images !== true) refuseImages();
739
+ return { claim: this.leadingClaim(desc, session) };
740
+ }
722
741
  if (!bare) return void 0;
742
+ if (envelope.images > 0) refuseImages();
723
743
  this.consumeVia(session.sessionId, {
724
744
  via: "enter",
725
745
  token
@@ -739,7 +759,8 @@ window.__ModuleLoader__.load({
739
759
  return {
740
760
  token,
741
761
  ...desc.input !== void 0 ? { hint: desc.input.hint } : {},
742
- submit: (args, _actx) => this.execute(session, token + args)
762
+ ...desc.input?.images === true ? { images: true } : {},
763
+ submit: (args, _actx, images) => this.execute(session, token + args, images)
743
764
  };
744
765
  }
745
766
  /**
@@ -749,16 +770,22 @@ window.__ModuleLoader__.load({
749
770
  * plain success regardless of its handler outcome, because the host
750
771
  * executor durably logged the lifecycle (`command/run`/`command/done`) and
751
772
  * the outcome renders as a persistent flow node — the composer never
752
- * echoes it. Transport failures throw.
773
+ * echoes it. A handler error result reports an error outcome so the
774
+ * composer keeps the submission (draft and images) for correction.
775
+ * Transport failures throw.
753
776
  */
754
- async execute(session, line) {
755
- const result = await this.ctx.remote.commands.execute(session.sessionId, line);
777
+ async execute(session, line, images = []) {
778
+ const result = await this.ctx.remote.commands.execute(session.sessionId, line, images);
756
779
  if (!result.ok) throw new Error(`command.execute failed: ${result.error.code}: ${result.error.message}`);
757
780
  if (result.value === void 0) return {
758
781
  kind: "error",
759
782
  text: `unknown or malformed command: ${line}`
760
783
  };
761
784
  this.notifyExecuted(session.sessionId, submittedCommandName(line), result.value.result);
785
+ if (images.length > 0 && result.value.result.kind === "error") return {
786
+ kind: "error",
787
+ text: result.value.result.text
788
+ };
762
789
  return { kind: "success" };
763
790
  }
764
791
  /** Publish the local acknowledgment without letting an observer change command admission. */
@@ -855,18 +882,18 @@ window.__ModuleLoader__.load({
855
882
  document.head.appendChild(tag);
856
883
  }
857
884
  var PopupSelectView_module_css_default = {
858
- "rowActive": "mufS8W_rowActive",
859
- "label": "mufS8W_label",
860
- "search": "mufS8W_search",
885
+ "card": "mufS8W_card",
886
+ "check": "mufS8W_check",
887
+ "detail": "mufS8W_detail",
861
888
  "error": "mufS8W_error",
862
889
  "errorText": "mufS8W_errorText",
863
- "check": "mufS8W_check",
890
+ "label": "mufS8W_label",
864
891
  "retry": "mufS8W_retry",
865
892
  "row": "mufS8W_row",
866
- "card": "mufS8W_card",
867
- "viewport": "mufS8W_viewport",
868
- "detail": "mufS8W_detail",
869
- "status": "mufS8W_status"
893
+ "rowActive": "mufS8W_rowActive",
894
+ "search": "mufS8W_search",
895
+ "status": "mufS8W_status",
896
+ "viewport": "mufS8W_viewport"
870
897
  };
871
898
  //#endregion
872
899
  //#region lib/types/client/PopupSelectView.js
@@ -1047,7 +1074,8 @@ window.__ModuleLoader__.load({
1047
1074
  "status.applying": "正在应用…",
1048
1075
  "status.empty": "无选项",
1049
1076
  "overlay.aria": "/{command} 选项",
1050
- "listbox.aria": "/{command} 匹配项"
1077
+ "listbox.aria": "/{command} 匹配项",
1078
+ "notice.imagesUnsupported": "/{command} 不接受图片附件,请先移除图片"
1051
1079
  };
1052
1080
  /** English dictionary, checked complete against the zh key set. */
1053
1081
  const en = {
@@ -1057,7 +1085,8 @@ window.__ModuleLoader__.load({
1057
1085
  "status.applying": "Applying…",
1058
1086
  "status.empty": "No options",
1059
1087
  "overlay.aria": "/{command} options",
1060
- "listbox.aria": "/{command} matches"
1088
+ "listbox.aria": "/{command} matches",
1089
+ "notice.imagesUnsupported": "/{command} does not accept image attachments; remove them first"
1061
1090
  };
1062
1091
  //#endregion
1063
1092
  //#region lib/types/client/index.js
@@ -8,6 +8,7 @@ export declare const zh: {
8
8
  'status.empty': string;
9
9
  'overlay.aria': string;
10
10
  'listbox.aria': string;
11
+ 'notice.imagesUnsupported': string;
11
12
  };
12
13
  /** The command namespace key union. */
13
14
  export type CommandKey = keyof typeof zh;
@@ -20,5 +21,6 @@ export declare const en: {
20
21
  'status.empty': string;
21
22
  'overlay.aria': string;
22
23
  'listbox.aria': string;
24
+ 'notice.imagesUnsupported': string;
23
25
  };
24
26
  //# sourceMappingURL=locales.d.ts.map
@@ -33,6 +33,8 @@ export declare class CommandUiRuntime extends Service implements CommandUiContra
33
33
  static inject: string[];
34
34
  private readonly directory;
35
35
  private readonly live;
36
+ /** `command`-namespace translator (composer refusal notices). */
37
+ private readonly t;
36
38
  /**
37
39
  * @param ctx - owning root context (plugin fiber; the service registers
38
40
  * itself as `command` and follows that fiber's lifetime).
@@ -81,6 +83,12 @@ export declare class CommandUiRuntime extends Service implements CommandUiContra
81
83
  * warmup failure rejects — never a silent downgrade). Contributions and
82
84
  * bare host commands act on the bare token only; leadingInput claims
83
85
  * args-tolerant.
86
+ *
87
+ * Envelope policy: an enter submission carrying images resolves only
88
+ * through a command declaring image acceptance. Every other command route —
89
+ * popup, non-accepting claim, bare detached execute — throws the refusal
90
+ * so the machine surfaces one composer notice and the draft and images
91
+ * stay in place; nothing executes and nothing is dropped.
84
92
  */
85
93
  private matchEnter;
86
94
  /** Open the session's popup for one contribution or decoration (menu pick / bare enter). */
@@ -94,7 +102,9 @@ export declare class CommandUiRuntime extends Service implements CommandUiContra
94
102
  * plain success regardless of its handler outcome, because the host
95
103
  * executor durably logged the lifecycle (`command/run`/`command/done`) and
96
104
  * the outcome renders as a persistent flow node — the composer never
97
- * echoes it. Transport failures throw.
105
+ * echoes it. A handler error result reports an error outcome so the
106
+ * composer keeps the submission (draft and images) for correction.
107
+ * Transport failures throw.
98
108
  */
99
109
  private execute;
100
110
  /** Publish the local acknowledgment without letting an observer change command admission. */
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.0-rc.7",
4
+ "version": "0.1.0-rc.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -46,33 +46,30 @@
46
46
  "clsx": "^2.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "react": "^18.2.0",
50
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
51
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
52
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
53
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
54
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
55
- "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.7",
56
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
57
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
58
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
59
- "@deepseek-ai/cordis": "^4.0.1"
49
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
50
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
51
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
52
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
53
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8",
54
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
55
+ "@deepseek-ai/cordis": "^4.0.1",
56
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8"
60
57
  },
61
58
  "devDependencies": {
62
59
  "@types/react": "~18.3.1",
63
60
  "react": "^18.2.0",
64
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
65
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
66
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
67
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
68
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.0-rc.7",
69
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
70
- "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.7",
71
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
72
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
73
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
74
- "@deepseek-ai/cordis": "^4.0.1",
75
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7"
61
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
62
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8",
63
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
64
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.0-rc.8",
65
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
66
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
67
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8",
68
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.0-rc.8",
69
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
70
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
72
+ "@deepseek-ai/cordis": "^4.0.1"
76
73
  },
77
74
  "files": [
78
75
  "lib/index.js",