dsh-quick-actions 0.1.0 → 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/lib/client.js CHANGED
@@ -182,6 +182,29 @@ window.__ModuleLoader__.load({
182
182
  }
183
183
  };
184
184
  }
185
+ const FNV_OFFSET_BASIS = 144066263297769815596495629667062367629n;
186
+ const FNV_PRIME = 309485009821345068724781371n;
187
+ const FNV_MASK = (1n << 128n) - 1n;
188
+ function fnv1a128(value) {
189
+ let hash = FNV_OFFSET_BASIS;
190
+ for (const byte of new TextEncoder().encode(value)) hash = (hash ^ BigInt(byte)) * FNV_PRIME & FNV_MASK;
191
+ return hash.toString(16).padStart(32, "0");
192
+ }
193
+ /**
194
+ * Hash the whole catalog projection, field by field and in order, so that any
195
+ * change a client could observe — including a pure reordering — moves the
196
+ * revision, and an unchanged catalog never does (spec 6.2).
197
+ */
198
+ function revisionOf(presets) {
199
+ return fnv1a128(JSON.stringify([1, presets.map((preset) => [
200
+ preset.id,
201
+ preset.kind,
202
+ preset.label,
203
+ preset.text,
204
+ preset.icon ?? null,
205
+ preset.confirm
206
+ ])]));
207
+ }
185
208
  function fieldIssue(field, reason) {
186
209
  return {
187
210
  field,
@@ -221,55 +244,135 @@ window.__ModuleLoader__.load({
221
244
  } };
222
245
  }
223
246
  /**
224
- * Read one published catalog snapshot back (spec 17.2). The Host is the catalog's
225
- * single validation authority, so this is a defensive decode of an already
226
- * confirmed snapshot rather than a second authority — but it is a decode, not a
227
- * cast: a snapshot this release cannot read in full yields no catalog at all.
228
- *
229
- * All-or-nothing on purpose. Dropping the entries it cannot read would show the
230
- * user a silently truncated action list, which is exactly what spec 5.1 forbids
231
- * the Host to do; the consumer reports a catalog error instead (spec 10).
232
- *
233
- * The Host's `revision` is carried through rather than recomputed. It is the
234
- * catalog's published identity, and a Client that recomputed it would be
235
- * asserting an authority it does not have.
236
- */
237
- function decodeCatalogSnapshot(raw) {
238
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
239
- const { schemaVersion, revision, presets } = raw;
240
- if (schemaVersion !== 1) return void 0;
241
- if (typeof revision !== "string" || revision === "") return void 0;
242
- if (!Array.isArray(presets)) return void 0;
243
- const read = readPublishedPresets(presets);
244
- if (read === void 0) return void 0;
245
- return {
246
- schemaVersion: 1,
247
- revision,
248
- presets: read
249
- };
250
- }
251
- /**
252
- * Validate an already-published preset list against the same field rules the
253
- * Host applied. Nothing is reported field by field: the Host is the authority
254
- * that names an author's mistake, and a Client can only refuse the snapshot.
247
+ * Merge and validate the built-in manifest with the Host composition's presets.
248
+ * Nothing is repaired or dropped: the first release refuses to load a catalog an
249
+ * author got wrong, so the mistake surfaces at startup rather than at send time.
255
250
  */
256
- function readPublishedPresets(entries) {
257
- if (entries.length > 50) return void 0;
251
+ function buildPresetCatalog(input) {
252
+ const issues = [];
258
253
  const presets = [];
259
254
  const seen = /* @__PURE__ */ new Set();
260
- for (const entry of entries) {
261
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return void 0;
255
+ const sources = [["builtin", input.builtins], ["config", input.configured]];
256
+ for (const [source, entries] of sources) for (const [index, entry] of entries.entries()) {
257
+ const at = (issue, id) => ({
258
+ scope: "preset",
259
+ source,
260
+ index,
261
+ id,
262
+ field: issue.field,
263
+ reason: issue.reason
264
+ });
265
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
266
+ issues.push(at(fieldIssue("id", "invalid-type"), void 0));
267
+ continue;
268
+ }
262
269
  const record = entry;
263
270
  const identity = readId(record);
264
- if ("issue" in identity || seen.has(identity.id)) return void 0;
271
+ if ("issue" in identity) {
272
+ issues.push(at(identity.issue, void 0));
273
+ continue;
274
+ }
275
+ if (seen.has(identity.id)) {
276
+ issues.push(at(fieldIssue("id", "duplicate"), identity.id));
277
+ continue;
278
+ }
265
279
  const read = readPreset(record, identity.id);
266
- if ("issue" in read) return void 0;
280
+ if ("issue" in read) {
281
+ issues.push(at(read.issue, identity.id));
282
+ continue;
283
+ }
267
284
  seen.add(identity.id);
268
285
  presets.push(read.preset);
269
286
  }
270
- return presets;
287
+ const count = input.builtins.length + input.configured.length;
288
+ if (count > 50) issues.push({
289
+ scope: "catalog",
290
+ reason: "too-many",
291
+ count,
292
+ limit: 50
293
+ });
294
+ if (issues.length > 0) return {
295
+ ok: false,
296
+ issues
297
+ };
298
+ return {
299
+ ok: true,
300
+ catalog: {
301
+ schemaVersion: 1,
302
+ revision: revisionOf(presets),
303
+ presets
304
+ }
305
+ };
271
306
  }
272
307
  //#endregion
308
+ //#region src/model/presets.ts
309
+ /**
310
+ * The package's built-in Preset Quick Action manifest — component 1 of the
311
+ * Preset Catalog (spec 5.1), declared ahead of the Host composition's
312
+ * `Config.presets`.
313
+ *
314
+ * It lives in the shared model because both faces merge it: the Host validates
315
+ * the catalog at load, and the Client rebuilds the same catalog from this list
316
+ * plus the `presets` form field it reads (spec 22.3). One list, so the two can
317
+ * never disagree about what the built-ins are.
318
+ *
319
+ * These are author-facing product copy in one language; changing a label or a
320
+ * text is a product decision, not an implementation one.
321
+ *
322
+ * Two rules bind any edit here:
323
+ *
324
+ * - A Preset Action ID is permanent. Label, icon and text may change under the
325
+ * same id, but `kind` and `confirm` are the immutable safety behaviour
326
+ * signature — changing either needs a new id (spec 5.1). Editing a text so it
327
+ * crosses the Command Send Action boundary counts as crossing that signature.
328
+ * - Adding an entry is additive: a new id appends to the end of every existing
329
+ * user's order and rewrites no stored data (spec 5.3).
330
+ *
331
+ * `summarize` and `explain` carry new ids rather than reusing
332
+ * `summarize-thread` and `explain-last-change`: those two shipped on the
333
+ * default `confirm: true`, and turning confirmation off is exactly the
334
+ * signature change the first rule reserves a new id for. The old ids simply
335
+ * leave the catalog, which the model already handles — a stored preference for
336
+ * an id no longer in the catalog is kept as a tombstone, neither shown nor
337
+ * counted (spec 5.3).
338
+ */
339
+ const BUILT_IN_PRESETS = Object.freeze([
340
+ Object.freeze({
341
+ id: "approve",
342
+ label: "确认",
343
+ text: "确认,按你刚才说的做。",
344
+ icon: "✅",
345
+ confirm: false
346
+ }),
347
+ Object.freeze({
348
+ id: "continue",
349
+ label: "继续",
350
+ text: "继续。",
351
+ icon: "▶️",
352
+ confirm: false
353
+ }),
354
+ Object.freeze({
355
+ id: "summarize",
356
+ label: "总结",
357
+ text: "总结当前对话:已确定的结论、仍未决的问题、下一步要做的事。",
358
+ icon: "📝",
359
+ confirm: false
360
+ }),
361
+ Object.freeze({
362
+ id: "explain",
363
+ label: "解释",
364
+ text: "解释你刚才的改动:为什么这样做,考虑过哪些替代方案,取舍是什么。",
365
+ icon: "🔍",
366
+ confirm: false
367
+ }),
368
+ Object.freeze({
369
+ id: "compact-context",
370
+ label: "压缩",
371
+ text: "/compact",
372
+ icon: "🧹"
373
+ })
374
+ ]);
375
+ //#endregion
273
376
  //#region src/model/settings.ts
274
377
  /**
275
378
  * Settings V1 decoding (spec 4.2, 6.3). Decoding is defensive and lossless: it
@@ -278,17 +381,27 @@ window.__ModuleLoader__.load({
278
381
  * Catalog-aware canonicalization lives in `normalize.ts`.
279
382
  */
280
383
  /**
281
- * The one namespace holding user data (spec 4.2); renaming it orphans every
282
- * stored section. It lives in the shared model because both faces address it:
283
- * the Host registers it, the Client binds the same name.
384
+ * The one namespace holding user data (spec 4.2, 22); renaming it orphans every
385
+ * stored section. Since DSH 0.1.7 a Settings namespace *is* a profile entry id,
386
+ * so this is also the Cordis loader entry id the bundle patch inserts — the two
387
+ * can no longer drift apart. It lives in the shared model because both faces
388
+ * address it: the Host describes and fences its canonical rewrite against it,
389
+ * the Client reads and writes the same form.
284
390
  */
285
391
  const QUICK_ACTIONS_SETTINGS_NAMESPACE = "composer-quick-actions";
286
392
  /**
287
- * The read-only namespace carrying the Preset Catalog to Clients (spec 17.2).
288
- * The plugin never writes its user layer, so it holds no persisted section —
289
- * `composer-quick-actions` remains the only persisted namespace (spec 4.2).
393
+ * The top-level fields of the stored section, in canonical order (spec 4.2).
394
+ * Each is a separate volatile Config field (spec 22.2): the DSH legacy import
395
+ * addresses a section by these exact top-level keys, and every write sets them
396
+ * field by field so a write never reaches the author's `presets`.
290
397
  */
291
- const QUICK_ACTIONS_CATALOG_NAMESPACE = "composer-quick-actions-catalog";
398
+ const QUICK_ACTION_STATE_FIELDS = Object.freeze([
399
+ "schemaVersion",
400
+ "layout",
401
+ "userActionsById",
402
+ "actionOrder",
403
+ "presetStateById"
404
+ ]);
292
405
  /** The state a fresh install starts from (spec 4.2). */
293
406
  const DEFAULT_QUICK_ACTION_SETTINGS = Object.freeze({
294
407
  schemaVersion: 1,
@@ -764,10 +877,10 @@ window.__ModuleLoader__.load({
764
877
  /**
765
878
  * The Client's single owner of authoritative Quick Action state (spec 7.1).
766
879
  *
767
- * It binds the two Settings namespaces the Host registers — the read-only
768
- * catalog namespace, read off its composition `base` layer, and the user-state
769
- * namespace — derives the projection every surface renders, and serializes
770
- * revision-fenced writes back through the same scope.
880
+ * It reads the plugin entry's one Settings form (spec 22) — the user-state
881
+ * fields and the author's `presets`, from which it rebuilds the Preset Catalog
882
+ * with the shared model — derives the projection every surface renders, and
883
+ * serializes revision-fenced writes back through the same form.
771
884
  *
772
885
  * What it deliberately does not own:
773
886
  *
@@ -779,10 +892,21 @@ window.__ModuleLoader__.load({
779
892
  * `InputActions` object reaches this module — long-lived global state must not
780
893
  * pin a session's runtime objects (spec 7.1). Draft occupancy, confirmation and
781
894
  * send single-flight belong to the per-session execution layer (spec 7.2).
782
- * - **Its own copy of the settings document.** Both bindings derive from the one
895
+ * - **Its own copy of the settings document.** The form derives from the one
783
896
  * browser-side describe mirror, so the catalog costs no read of its own and
784
- * refreshes with that mirror after a reconnect (spec 17.3).
897
+ * refreshes with that mirror after a reconnect (spec 17.3, 22.3).
898
+ */
899
+ /**
900
+ * Whether a user retry can clear this catalog error (spec 10).
901
+ *
902
+ * Only a failed first read can: the mirror's public face re-reads only while it
903
+ * holds no document (`ensure()`, spec 22.4). The other two arrive with a held
904
+ * document and clear on their own when the Host's `settings/document-updated`
905
+ * broadcast or a reconnect refreshes it, so a retry there would do nothing.
785
906
  */
907
+ function isRetryableCatalogError(reason) {
908
+ return reason === "unreadable";
909
+ }
786
910
  /**
787
911
  * How many Custom Action IDs a create or clone tries before giving up.
788
912
  * Normalization must never renumber a stored id (spec 4.1), so the planner
@@ -790,44 +914,27 @@ window.__ModuleLoader__.load({
790
914
  * anything; exhausting the attempts surfaces the refusal instead of hiding it.
791
915
  */
792
916
  const ID_MINT_ATTEMPTS = 4;
793
- /** The five top-level fields of the persisted section (spec 4.2). */
917
+ /**
918
+ * One write per user-state field (spec 4.2), never touching `presets`: the
919
+ * author's list shares the entry's config, and this face never writes it.
920
+ */
794
921
  function sectionOps(next) {
795
- return [
796
- {
797
- op: "set",
798
- path: ["schemaVersion"],
799
- value: next.schemaVersion
800
- },
801
- {
802
- op: "set",
803
- path: ["layout"],
804
- value: next.layout
805
- },
806
- {
807
- op: "set",
808
- path: ["userActionsById"],
809
- value: next.userActionsById
810
- },
811
- {
812
- op: "set",
813
- path: ["actionOrder"],
814
- value: next.actionOrder
815
- },
816
- {
817
- op: "set",
818
- path: ["presetStateById"],
819
- value: next.presetStateById
820
- }
821
- ];
922
+ return QUICK_ACTION_STATE_FIELDS.map((field) => ({
923
+ op: "set",
924
+ path: [field],
925
+ value: next[field]
926
+ }));
822
927
  }
823
928
  /**
824
- * Read the catalog off a bound namespace.
929
+ * Rebuild the catalog from the form (spec 22.3): the package's built-ins, then
930
+ * the author's `presets` field, merged by the same shared model the Host
931
+ * validated them with at load.
825
932
  *
826
- * `base` is read, never `value`: `base` is the author's own layer, so a user who
827
- * hand-writes a section of that name into the settings document still sees the
828
- * Host's catalog (spec 17.2).
933
+ * The resolved `value` is read, not `base`: author presets are declared in the
934
+ * profile's own patch layer, which is exactly the layer `base` leaves out. A
935
+ * list the model refuses is an error, never a partial catalog (spec 5.1).
829
936
  */
830
- function readCatalog(snapshot, document) {
937
+ function readCatalog(snapshot, document, builtins) {
831
938
  if (snapshot.status === "loading") {
832
939
  if (document.view !== void 0) return {
833
940
  status: "error",
@@ -838,18 +945,25 @@ window.__ModuleLoader__.load({
838
945
  reason: "unreadable"
839
946
  };
840
947
  }
841
- if (snapshot.status === "unavailable" || snapshot.base === void 0) return {
948
+ if (snapshot.status === "unavailable") return {
842
949
  status: "error",
843
950
  reason: "unavailable"
844
951
  };
845
- const catalog = decodeCatalogSnapshot(snapshot.base);
846
- if (catalog === void 0) return {
952
+ const configured = snapshot.value?.presets ?? [];
953
+ if (!Array.isArray(configured)) return {
847
954
  status: "error",
848
955
  reason: "undecodable"
849
956
  };
850
- return {
957
+ const result = buildPresetCatalog({
958
+ builtins,
959
+ configured
960
+ });
961
+ return result.ok ? {
851
962
  status: "ready",
852
- catalog
963
+ catalog: result.catalog
964
+ } : {
965
+ status: "error",
966
+ reason: "undecodable"
853
967
  };
854
968
  }
855
969
  /**
@@ -891,21 +1005,19 @@ window.__ModuleLoader__.load({
891
1005
  };
892
1006
  }
893
1007
  /**
894
- * Create the controller. Both bindings are made on the calling fiber, so the
895
- * scope disposers the binder registers are withdrawn with it; `dispose()`
896
- * releases what this module owns on top of that.
1008
+ * Create the controller. The form is owned by the settings provider and shared
1009
+ * by every reader of this entry, so there is nothing to release for it;
1010
+ * `dispose()` withdraws this module's own subscriptions.
1011
+ *
1012
+ * Both the catalog and the user state come off the same form. It is decoded
1013
+ * here, by the shared model, rather than trusted as the transport validated
1014
+ * it: the Client's reading of a confirmed snapshot is the model's.
897
1015
  */
898
1016
  function createQuickActionsController(options) {
899
1017
  const mintId = options.mintCustomActionId ?? (() => crypto.randomUUID());
900
- const catalogScope = options.settingsScope.bind({
901
- namespace: QUICK_ACTIONS_CATALOG_NAMESPACE,
902
- decode: (value) => value ?? null
903
- });
904
- const settingsScope = options.settingsScope.bind({
905
- namespace: QUICK_ACTIONS_SETTINGS_NAMESPACE,
906
- decode: (value) => decodeQuickActionSettings(value)
907
- });
908
- const mirror = options.settingsScope.describe();
1018
+ const builtins = options.builtinPresets ?? BUILT_IN_PRESETS;
1019
+ const form = options.configForms.get(QUICK_ACTIONS_SETTINGS_NAMESPACE);
1020
+ const mirror = options.configForms.describe();
909
1021
  const listeners = /* @__PURE__ */ new Set();
910
1022
  let disposed = false;
911
1023
  let manager = { open: false };
@@ -916,8 +1028,9 @@ window.__ModuleLoader__.load({
916
1028
  /** Serializes this controller's own writes so each plan reads the previous one back. */
917
1029
  let tail = Promise.resolve();
918
1030
  function derive() {
919
- const catalog = readCatalog(catalogScope.getSnapshot(), mirror.getSnapshot());
920
- const settings = readSettings(settingsScope.getSnapshot());
1031
+ const snapshot = form.getSnapshot();
1032
+ const catalog = readCatalog(snapshot, mirror.getSnapshot(), builtins);
1033
+ const settings = readSettings(snapshot);
921
1034
  const projection = catalog.status === "ready" ? projectQuickActions(settings.status === "ready" ? settings.settings : DEFAULT_QUICK_ACTION_SETTINGS, catalog.catalog) : void 0;
922
1035
  const stale = connectionState !== void 0 && connectionState !== "connected";
923
1036
  return {
@@ -939,11 +1052,7 @@ window.__ModuleLoader__.load({
939
1052
  state = next;
940
1053
  for (const listener of Array.from(listeners)) listener();
941
1054
  }
942
- const stopSources = [
943
- mirror.subscribe(publish),
944
- catalogScope.subscribe(publish),
945
- settingsScope.subscribe(publish)
946
- ];
1055
+ const stopSources = [mirror.subscribe(publish), form.subscribe(publish)];
947
1056
  const stopConnection = options.connection.state.subscribe(() => {
948
1057
  connectionState = options.connection.state.getSnapshot();
949
1058
  publish();
@@ -973,7 +1082,7 @@ window.__ModuleLoader__.load({
973
1082
  * the writer that won.
974
1083
  */
975
1084
  function classify(catalog, next, fence) {
976
- const snapshot = settingsScope.getSnapshot();
1085
+ const snapshot = form.getSnapshot();
977
1086
  const settings = readSettings(snapshot);
978
1087
  if (settings.status === "ready" && deepEqualJson(normalizeQuickActionSettings(settings.settings, catalog), next)) return {
979
1088
  ok: true,
@@ -1019,10 +1128,10 @@ window.__ModuleLoader__.load({
1019
1128
  ok: true,
1020
1129
  changed: false
1021
1130
  });
1022
- await settingsScope.mutate(sectionOps(outcome.plan.next), gate.revision);
1131
+ await form.mutate(sectionOps(outcome.plan.next), gate.revision);
1023
1132
  return settle(classify(gate.catalog, outcome.plan.next, gate.revision));
1024
1133
  } catch (error) {
1025
- await mirror.load().catch(() => void 0);
1134
+ await mirror.ensure().catch(() => void 0);
1026
1135
  return settle({
1027
1136
  ok: false,
1028
1137
  failure: {
@@ -1048,7 +1157,7 @@ window.__ModuleLoader__.load({
1048
1157
  },
1049
1158
  refresh: async () => {
1050
1159
  if (disposed) return;
1051
- await mirror.load();
1160
+ await mirror.ensure();
1052
1161
  },
1053
1162
  openManager() {
1054
1163
  manager = { open: true };
@@ -1576,6 +1685,11 @@ window.__ModuleLoader__.load({
1576
1685
  * no layout — every width reads as zero. That answers "all of them": showing the
1577
1686
  * full row and letting the browser's own overflow handle it for one frame is
1578
1687
  * better than flashing a "more" button that the next frame withdraws.
1688
+ *
1689
+ * A control fits when it overruns the row by less than `FIT_TOLERANCE`. Whenever
1690
+ * the bar is content-sized (spec 22.9) the row it measures is exactly as wide as
1691
+ * the controls it shows, and fractional layout can report that a hair short;
1692
+ * folding a face over the difference would narrow the row and start a cascade.
1579
1693
  */
1580
1694
  function fitActionCount(input) {
1581
1695
  const { available, widths, reserved, gap } = input;
@@ -1585,7 +1699,7 @@ window.__ModuleLoader__.load({
1585
1699
  let shown = 0;
1586
1700
  for (const width of widths) {
1587
1701
  const next = used + width + (shown === 0 && reserved === 0 ? 0 : gap);
1588
- if (next > available) break;
1702
+ if (next > available + .5) break;
1589
1703
  used = next;
1590
1704
  shown += 1;
1591
1705
  }
@@ -1781,6 +1895,31 @@ window.__ModuleLoader__.load({
1781
1895
  * per-Session engine, which owns the single flight and the final re-verification
1782
1896
  * (spec 9.3, 9.5). The panel never mints a second lock.
1783
1897
  */
1898
+ /**
1899
+ * The search field's glyph: the official `IconSearchOutlineRegular` artwork,
1900
+ * drawn here rather than imported. DSH renamed its icon exports between lines
1901
+ * (`IconSearchOutline16` is gone as of 0.1.7) and the shell's seed table serves
1902
+ * only the running line's names, so an imported icon breaks the whole panel the
1903
+ * next time a name moves (spec 22.5). Stroke follows `currentColor`, so it
1904
+ * themes with the field like the official one.
1905
+ */
1906
+ function SearchIcon() {
1907
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1908
+ width: 16,
1909
+ height: 16,
1910
+ viewBox: "0 0 16 16",
1911
+ fill: "none",
1912
+ "aria-hidden": "true",
1913
+ strokeWidth: 1,
1914
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1915
+ d: "M6.58727 11.8586C9.55061 11.8586 11.9529 9.45637 11.9529 6.49304C11.9529 3.5297 9.55061 1.12744 6.58727 1.12744C3.62394 1.12744 1.22168 3.5297 1.22168 6.49304C1.22168 9.45637 3.62394 11.8586 6.58727 11.8586Z",
1916
+ stroke: "currentColor"
1917
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1918
+ d: "M10.2991 10.3933L14.7783 14.8725",
1919
+ stroke: "currentColor"
1920
+ })]
1921
+ });
1922
+ }
1784
1923
  function ActionPanel(props) {
1785
1924
  const { actions, session, t, labelledBy, onActivate, onClose } = props;
1786
1925
  const [query, setQuery] = (0, react.useState)("");
@@ -1822,7 +1961,7 @@ window.__ModuleLoader__.load({
1822
1961
  className: "dsh-cqa-field-label",
1823
1962
  children: t("panel.search")
1824
1963
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1825
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
1964
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchIcon, {}),
1826
1965
  type: "search",
1827
1966
  value: query,
1828
1967
  "data-quick-actions-search": "",
@@ -1953,10 +2092,13 @@ window.__ModuleLoader__.load({
1953
2092
  });
1954
2093
  }
1955
2094
  /**
1956
- * Measure one element's inner width, republishing on every resize. Used twice:
1957
- * on the whole row for the density rule, and on the action region alone for the
1958
- * bar's overflow split — the region is `flex: 1 1 auto`, so its width is already
1959
- * what is left after the controls that must stay visible.
2095
+ * Measure one element's width, republishing on every resize. Used twice: on the
2096
+ * whole row for the density rule, and on the action region alone for the bar's
2097
+ * overflow split — the region is `flex: 1 1 auto`, so its width is already what
2098
+ * is left after the controls that must stay visible.
2099
+ *
2100
+ * Fractional, like the control widths it is compared with: `clientWidth` rounds,
2101
+ * and a region reported a pixel short of the faces it holds folds one of them.
1960
2102
  */
1961
2103
  function useElementWidth() {
1962
2104
  const ref = (0, react.useRef)(null);
@@ -1965,10 +2107,10 @@ window.__ModuleLoader__.load({
1965
2107
  const element = ref.current;
1966
2108
  if (element === null || typeof ResizeObserver === "undefined") return;
1967
2109
  const observer = new ResizeObserver(() => {
1968
- setWidth(element.clientWidth);
2110
+ setWidth(element.getBoundingClientRect().width);
1969
2111
  });
1970
2112
  observer.observe(element);
1971
- setWidth(element.clientWidth);
2113
+ setWidth(element.getBoundingClientRect().width);
1972
2114
  return () => {
1973
2115
  observer.disconnect();
1974
2116
  };
@@ -1989,7 +2131,7 @@ window.__ModuleLoader__.load({
1989
2131
  let changed = false;
1990
2132
  for (const node of Array.from(fitRef.current?.querySelectorAll("[data-quick-action]") ?? [])) {
1991
2133
  const key = node.dataset["quickAction"];
1992
- const value = node.offsetWidth;
2134
+ const value = node.getBoundingClientRect().width;
1993
2135
  if (key === void 0 || value <= 0 || widths.current.get(key) === value) continue;
1994
2136
  widths.current.set(key, value);
1995
2137
  changed = true;
@@ -2060,6 +2202,7 @@ window.__ModuleLoader__.load({
2060
2202
  className: layout === "bar" ? "dsh-cqa-bar" : layout === "launcher" ? "dsh-cqa-launcher" : "dsh-cqa-ribbon",
2061
2203
  "data-quick-actions-layout": layout,
2062
2204
  "data-quick-actions-density": density,
2205
+ "data-quick-actions-overflow": layout === "bar" && overflow.length > 0 ? "" : void 0,
2063
2206
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2064
2207
  className: "dsh-cqa-row",
2065
2208
  ref: rowRef,
@@ -2190,8 +2333,8 @@ window.__ModuleLoader__.load({
2190
2333
  "feedback.dismiss": "知道了",
2191
2334
  "catalog.loading": "正在读取快捷动作…",
2192
2335
  "catalog.unreadable": "无法读取设置,快捷动作暂不可用",
2193
- "catalog.unavailable": "未找到预置目录",
2194
- "catalog.undecodable": "预置目录版本过新,无法读取",
2336
+ "catalog.unavailable": "DSH 未向本页面提供快捷动作设置,快捷动作暂不可用",
2337
+ "catalog.undecodable": "预置目录配置无效,快捷动作暂不可用",
2195
2338
  "catalog.retry": "重试",
2196
2339
  "crash.title": "快捷动作出错了",
2197
2340
  "crash.retry": "重新加载",
@@ -2290,8 +2433,8 @@ window.__ModuleLoader__.load({
2290
2433
  "feedback.dismiss": "Dismiss",
2291
2434
  "catalog.loading": "Loading Quick Actions…",
2292
2435
  "catalog.unreadable": "Settings could not be read, so Quick Actions are unavailable",
2293
- "catalog.unavailable": "No Preset Catalog was published",
2294
- "catalog.undecodable": "The Preset Catalog is newer than this release can read",
2436
+ "catalog.unavailable": "DSH is not serving Quick Actions settings to this page, so Quick Actions are unavailable",
2437
+ "catalog.undecodable": "The Preset Catalog configuration is invalid, so Quick Actions are unavailable",
2295
2438
  "catalog.retry": "Retry",
2296
2439
  "crash.title": "Quick Actions hit an error",
2297
2440
  "crash.retry": "Reload",
@@ -2931,14 +3074,14 @@ window.__ModuleLoader__.load({
2931
3074
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2932
3075
  className: "dsh-cqa-note-text",
2933
3076
  children: t(client.catalog.status === "error" ? `catalog.${client.catalog.reason}` : "catalog.loading")
2934
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3077
+ }), client.catalog.status === "error" && isRetryableCatalogError(client.catalog.reason) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2935
3078
  type: "button",
2936
3079
  className: "dsh-cqa-link",
2937
3080
  onClick: () => {
2938
3081
  controller.refresh().catch(() => void 0);
2939
3082
  },
2940
3083
  children: t("catalog.retry")
2941
- })]
3084
+ }) : null]
2942
3085
  }) : null,
2943
3086
  readOnlyReason === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2944
3087
  className: "dsh-cqa-note",
@@ -3234,12 +3377,12 @@ window.__ModuleLoader__.load({
3234
3377
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3235
3378
  className: "dsh-cqa-note-text",
3236
3379
  children: t(`catalog.${client.catalog.reason}`)
3237
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3380
+ }), isRetryableCatalogError(client.catalog.reason) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3238
3381
  type: "button",
3239
3382
  className: "dsh-cqa-link",
3240
3383
  onClick: onRetry,
3241
3384
  children: t("catalog.retry")
3242
- })]
3385
+ }) : null]
3243
3386
  });
3244
3387
  }
3245
3388
  /** Build both Slot entry components over one set of fiber-owned dependencies. */
@@ -3329,20 +3472,24 @@ window.__ModuleLoader__.load({
3329
3472
  * Colours are DSH alias theme tokens only — nothing here defines a palette, and
3330
3473
  * nothing overrides a global theme (spec 8.4).
3331
3474
  *
3332
- * ## The width rule (spec 8.2)
3333
- *
3334
- * The two layouts sit in different boxes, so they reach the same outer edges by
3335
- * different arithmetic:
3336
- *
3337
- * - `.dsh-cqa-ribbon` renders in the composer stack, outside the InputBar, which
3338
- * is the element that owns `--dsh-composer-side-clearance` as padding. It
3339
- * therefore subtracts that clearance from both sides itself.
3340
- * - `.dsh-cqa-bar` renders as the InputBar's last child, inside that padding, so
3341
- * it is simply `100%` wide.
3342
- *
3343
- * Both are then capped at `--dsh-composer-card-max-width` — the same cap the
3344
- * composer card uses — and centred, so their left and right edges coincide with
3345
- * the input box exactly rather than approximately.
3475
+ * ## The width rule (spec 8.2, 22.9)
3476
+ *
3477
+ * `.dsh-cqa-ribbon` (and the launcher, in the same box) renders in the composer
3478
+ * stack, outside the InputBar, which is the element that owns
3479
+ * `--dsh-composer-side-clearance` as padding. It therefore subtracts that
3480
+ * clearance from both sides itself, is capped at `--dsh-composer-card-max-width`
3481
+ * — the same cap the composer card uses — and is centred, so its left and right
3482
+ * edges coincide with the input box exactly rather than approximately.
3483
+ *
3484
+ * `.dsh-cqa-bar` makes no such claim. Since DSH 0.1.6-alpha.2 the composer dock is
3485
+ * a row under the card that DSH centres and sizes to its content, shared with
3486
+ * DSH's own stats pills and context meter, so the bar is a content-sized member of
3487
+ * that row (spec 22.9). It only has to stay inside it: `min-width: 0` lets it
3488
+ * shrink when the row reaches the composer's width, which is what hands the
3489
+ * overflow split a smaller region to fold into "more". Once it folds it claims
3490
+ * the card's width (`[data-quick-actions-overflow]`), capped by the row's own
3491
+ * `max-width: 100%`: content-sized, the region the split measures would only be
3492
+ * as wide as what it already shows, and a wider window could never unfold it.
3346
3493
  */
3347
3494
  const TAG_ID = "dsh-quick-actions/surfaces.css";
3348
3495
  const QUICK_ACTIONS_CSS = `
@@ -3355,9 +3502,12 @@ window.__ModuleLoader__.load({
3355
3502
  }
3356
3503
  .dsh-cqa-bar {
3357
3504
  box-sizing: border-box;
3358
- width: 100%;
3505
+ min-width: 0;
3359
3506
  max-width: var(--dsh-composer-card-max-width);
3360
- margin: 0 auto;
3507
+ }
3508
+ /* Folding: claim the card's width, for the dock row's own max-width to cap. */
3509
+ .dsh-cqa-bar[data-quick-actions-overflow] {
3510
+ width: var(--dsh-composer-card-max-width);
3361
3511
  }
3362
3512
 
3363
3513
  .dsh-cqa-row {
@@ -3839,7 +3989,7 @@ window.__ModuleLoader__.load({
3839
3989
  */
3840
3990
  const inject = [
3841
3991
  "slots",
3842
- "settingsScope",
3992
+ "configForms",
3843
3993
  "connection",
3844
3994
  "locale"
3845
3995
  ];
@@ -3855,7 +4005,7 @@ window.__ModuleLoader__.load({
3855
4005
  */
3856
4006
  function apply(ctx) {
3857
4007
  const controller = createQuickActionsController({
3858
- settingsScope: ctx.settingsScope,
4008
+ configForms: ctx.configForms,
3859
4009
  connection: ctx.connection
3860
4010
  });
3861
4011
  const sessions = createQuickActionSessionRegistry();