dsh-quick-actions 0.1.0-rc.3

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +199 -0
  3. package/README.md +199 -0
  4. package/cordis.patch.yml +29 -0
  5. package/lib/client.js +3901 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +737 -0
  8. package/lib/types/client/controller.d.ts +207 -0
  9. package/lib/types/client/dsh.d.ts +171 -0
  10. package/lib/types/client/index.d.ts +43 -0
  11. package/lib/types/client/manager/ActionForm.d.ts +20 -0
  12. package/lib/types/client/manager/ActionPanel.d.ts +15 -0
  13. package/lib/types/client/manager/ManagedRow.d.ts +40 -0
  14. package/lib/types/client/manager/ManagerPanel.d.ts +9 -0
  15. package/lib/types/client/manager/press.d.ts +38 -0
  16. package/lib/types/client/manager/search.d.ts +56 -0
  17. package/lib/types/client/manager/status.d.ts +23 -0
  18. package/lib/types/client/modal.d.ts +55 -0
  19. package/lib/types/client/session/ConfirmPanel.d.ts +23 -0
  20. package/lib/types/client/session/availability.d.ts +19 -0
  21. package/lib/types/client/session/execution.d.ts +86 -0
  22. package/lib/types/client/session/guards.d.ts +59 -0
  23. package/lib/types/client/surfaces/ActionFace.d.ts +21 -0
  24. package/lib/types/client/surfaces/ErrorBoundary.d.ts +31 -0
  25. package/lib/types/client/surfaces/QuickActionsSurface.d.ts +17 -0
  26. package/lib/types/client/surfaces/entries.d.ts +27 -0
  27. package/lib/types/client/surfaces/layout.d.ts +49 -0
  28. package/lib/types/client/surfaces/residency.d.ts +65 -0
  29. package/lib/types/host/config.d.ts +18 -0
  30. package/lib/types/host/index.d.ts +38 -0
  31. package/lib/types/host/presets.d.ts +26 -0
  32. package/lib/types/host/settings.d.ts +113 -0
  33. package/lib/types/index.d.ts +35 -0
  34. package/lib/types/locales/index.d.ts +34 -0
  35. package/lib/types/model/catalog.d.ts +63 -0
  36. package/lib/types/model/index.d.ts +13 -0
  37. package/lib/types/model/json.d.ts +11 -0
  38. package/lib/types/model/mutations.d.ts +98 -0
  39. package/lib/types/model/normalize.d.ts +15 -0
  40. package/lib/types/model/projection.d.ts +49 -0
  41. package/lib/types/model/settings.d.ts +48 -0
  42. package/lib/types/model/text.d.ts +28 -0
  43. package/lib/types/model/types.d.ts +89 -0
  44. package/lib/types/model/validation.d.ts +40 -0
  45. package/lib/types/styles/index.d.ts +39 -0
  46. package/lib/types/types.d.ts +11 -0
  47. package/lib/types.js +1 -0
  48. package/package.json +83 -0
package/lib/client.js ADDED
@@ -0,0 +1,3901 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-quick-actions",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ let react_dom = require("react-dom");
11
+ //#region src/model/types.ts
12
+ /** Every layout value, in the order the management panel offers them. */
13
+ const QUICK_ACTION_LAYOUTS = [
14
+ "ribbon",
15
+ "bar",
16
+ "launcher"
17
+ ];
18
+ /**
19
+ * The identity of a reference as one comparable string. Source and id together
20
+ * are the identity: the same id under a different source is a different action.
21
+ */
22
+ function quickActionRefKey(ref) {
23
+ return `${ref.source}:${ref.id}`;
24
+ }
25
+ //#endregion
26
+ //#region src/model/json.ts
27
+ /**
28
+ * JSON structural comparison shared by the mutation planner and the Host's
29
+ * canonical rewrite.
30
+ *
31
+ * Structural, not canonical-JSON: object key order carries no meaning here.
32
+ * A stored section round-tripped through YAML, and a tombstone written by a
33
+ * higher version, both come back with whatever key order their writer chose —
34
+ * comparing serialized text would report a difference that is not one, and the
35
+ * rewrite would then write on every start instead of being idempotent.
36
+ */
37
+ function deepEqualJson(left, right) {
38
+ if (left === right) return true;
39
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
40
+ if (Array.isArray(left) || Array.isArray(right)) {
41
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
42
+ return left.every((item, index) => deepEqualJson(item, right[index]));
43
+ }
44
+ const leftKeys = Object.keys(left);
45
+ const rightKeys = Object.keys(right);
46
+ if (leftKeys.length !== rightKeys.length) return false;
47
+ return leftKeys.every((key) => Object.hasOwn(right, key) && deepEqualJson(left[key], right[key]));
48
+ }
49
+ //#endregion
50
+ //#region src/model/text.ts
51
+ /**
52
+ * Unicode primitives shared by every Quick Action entry point (spec 4.3).
53
+ * Config loading, the management form, migration and Settings mutations must all
54
+ * measure and trim text through these functions so one rule cannot drift from another.
55
+ */
56
+ /**
57
+ * Reference placeholder code points DSH reserves and strips inside `setDraft`:
58
+ * the reference marker block plus the legacy object replacement character.
59
+ * Static text carrying one of them would submit something other than what the
60
+ * user configured, so the model rejects it as a field error (spec 4.3).
61
+ *
62
+ * Source: `REFERENCE_PLACEHOLDER_RE` in `@deepseek-ai/dsh-client-ui-conversation`
63
+ * (`lib/client.js`, input machine), applied by `setDraft` before it rebuilds the
64
+ * draft. Verified identical in 0.1.1-rc.2, 0.1.2-rc.1 and 0.1.5-rc.1 — the release
65
+ * that renamed `imageIds` left this range untouched (ticket 29). Its tail is
66
+ * published as a named constant, which 0.1.5-rc.1 moved and renamed:
67
+ * `PLACEHOLDER = "\uFFFC"` in `lib/types/client/input/machine.d.ts` became
68
+ * `ATOMIC_CHAR = "\uFFFC"` in `lib/types/client/input/editor/projection.d.ts`.
69
+ * Re-check this range when the supported DSH range moves.
70
+ */
71
+ const RESERVED_REFERENCE_PLACEHOLDER = /[\u{E100}-\u{E11D}\u{FFFC}]/u;
72
+ /** Unicode code point length — the counting unit for every length limit (spec 4.3). */
73
+ function countCodePoints(value) {
74
+ let count = 0;
75
+ for (const _ of value) count += 1;
76
+ return count;
77
+ }
78
+ /** Whether a text holds no character outside ECMAScript `trim()` whitespace (spec 4.3). */
79
+ function isBlankQuickActionText(text) {
80
+ return text.trim().length === 0;
81
+ }
82
+ /** Whether a text carries a DSH-reserved reference placeholder code point (spec 4.3). */
83
+ function containsReservedReferencePlaceholder(text) {
84
+ return RESERVED_REFERENCE_PLACEHOLDER.test(text);
85
+ }
86
+ /**
87
+ * Whether a static text is a Command Send Action: its first non-whitespace
88
+ * character is `/` (spec 4.3). Whitespace is ECMAScript `trim()` whitespace.
89
+ */
90
+ function isCommandSendActionText(text) {
91
+ return text.trimStart().startsWith("/");
92
+ }
93
+ /** Code points allowed to appear inside an emoji grapheme cluster. */
94
+ const EMOJI_CLUSTER_MEMBER = /^[\p{Extended_Pictographic}\p{Emoji_Component}\p{Emoji_Modifier}\u{FE0E}\u{FE0F}\u{200D}]+$/u;
95
+ /** A keycap sequence carries no pictographic code point of its own. */
96
+ const KEYCAP_CLUSTER = /^[0-9#*]\u{FE0F}?\u{20E3}$/u;
97
+ /** A flag is a pair of regional indicators, also without a pictographic code point. */
98
+ const FLAG_CLUSTER = /^\p{Regional_Indicator}{2}$/u;
99
+ const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
100
+ function isEmojiCluster(cluster) {
101
+ if (!EMOJI_CLUSTER_MEMBER.test(cluster)) return false;
102
+ return /\p{Extended_Pictographic}/u.test(cluster) || KEYCAP_CLUSTER.test(cluster) || FLAG_CLUSTER.test(cluster);
103
+ }
104
+ /**
105
+ * Segment an icon candidate into grapheme clusters and report whether each one
106
+ * is an emoji cluster, so validation can tell `too-long` from `not-emoji`.
107
+ */
108
+ function scanEmojiClusters(icon) {
109
+ let clusters = 0;
110
+ let emojiOnly = true;
111
+ for (const { segment } of graphemeSegmenter.segment(icon)) {
112
+ clusters += 1;
113
+ if (!isEmojiCluster(segment)) emojiOnly = false;
114
+ }
115
+ return {
116
+ clusters,
117
+ emojiOnly
118
+ };
119
+ }
120
+ /** Static text limit, in Unicode code points, measured without trimming (spec 4.3). */
121
+ const QUICK_ACTION_TEXT_MAX_CODE_POINTS = 4e3;
122
+ /** Trimmed label, or the reason it cannot be stored. */
123
+ function labelIssue(label) {
124
+ const trimmed = label.trim();
125
+ if (trimmed.length === 0) return {
126
+ field: "label",
127
+ reason: "blank"
128
+ };
129
+ if (countCodePoints(trimmed) > 40) return {
130
+ field: "label",
131
+ reason: "too-long"
132
+ };
133
+ }
134
+ /** The reason a static text cannot be stored, if any. */
135
+ function textIssue(text) {
136
+ if (isBlankQuickActionText(text)) return {
137
+ field: "text",
138
+ reason: "blank"
139
+ };
140
+ if (countCodePoints(text) > 4e3) return {
141
+ field: "text",
142
+ reason: "too-long"
143
+ };
144
+ if (containsReservedReferencePlaceholder(text)) return {
145
+ field: "text",
146
+ reason: "reserved-placeholder"
147
+ };
148
+ }
149
+ /** The reason a non-empty icon cannot be stored, if any. */
150
+ function iconIssue(icon) {
151
+ const { clusters, emojiOnly } = scanEmojiClusters(icon);
152
+ if (!emojiOnly) return {
153
+ field: "icon",
154
+ reason: "not-emoji"
155
+ };
156
+ if (clusters > 4) return {
157
+ field: "icon",
158
+ reason: "too-long"
159
+ };
160
+ }
161
+ /**
162
+ * Validate one edited or created action. Issues come back in field declaration
163
+ * order — label, text, icon — so the form can render them deterministically.
164
+ */
165
+ function validateQuickActionDraft(draft) {
166
+ const issues = [
167
+ labelIssue(draft.label),
168
+ textIssue(draft.text),
169
+ draft.icon === "" ? void 0 : iconIssue(draft.icon)
170
+ ].filter((issue) => issue !== void 0);
171
+ if (issues.length > 0) return {
172
+ ok: false,
173
+ issues
174
+ };
175
+ return {
176
+ ok: true,
177
+ value: {
178
+ label: draft.label.trim(),
179
+ text: draft.text,
180
+ icon: draft.icon === "" ? void 0 : draft.icon,
181
+ confirm: draft.confirm
182
+ }
183
+ };
184
+ }
185
+ function fieldIssue(field, reason) {
186
+ return {
187
+ field,
188
+ reason
189
+ };
190
+ }
191
+ function readId(entry) {
192
+ const { id } = entry;
193
+ if (id === void 0) return { issue: fieldIssue("id", "missing") };
194
+ if (typeof id !== "string") return { issue: fieldIssue("id", "invalid-type") };
195
+ if (id.trim().length === 0) return { issue: fieldIssue("id", "blank") };
196
+ return { id };
197
+ }
198
+ function readPreset(entry, id) {
199
+ if (entry.kind !== void 0 && entry.kind !== "send") return { issue: fieldIssue("kind", "unsupported") };
200
+ const { label, text, icon, confirm } = entry;
201
+ if (typeof label !== "string") return { issue: fieldIssue("label", label === void 0 ? "missing" : "invalid-type") };
202
+ const labelProblem = labelIssue(label);
203
+ if (labelProblem !== void 0) return { issue: labelProblem };
204
+ if (typeof text !== "string") return { issue: fieldIssue("text", text === void 0 ? "missing" : "invalid-type") };
205
+ const textProblem = textIssue(text);
206
+ if (textProblem !== void 0) return { issue: textProblem };
207
+ if (icon !== void 0 && typeof icon !== "string") return { issue: fieldIssue("icon", "invalid-type") };
208
+ if (icon === "") return { issue: fieldIssue("icon", "blank") };
209
+ if (icon !== void 0) {
210
+ const iconProblem = iconIssue(icon);
211
+ if (iconProblem !== void 0) return { issue: iconProblem };
212
+ }
213
+ if (confirm !== void 0 && typeof confirm !== "boolean") return { issue: fieldIssue("confirm", "invalid-type") };
214
+ return { preset: {
215
+ id,
216
+ kind: "send",
217
+ label: label.trim(),
218
+ text,
219
+ ...icon === void 0 ? {} : { icon },
220
+ confirm: confirm ?? true
221
+ } };
222
+ }
223
+ /**
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.
255
+ */
256
+ function readPublishedPresets(entries) {
257
+ if (entries.length > 50) return void 0;
258
+ const presets = [];
259
+ const seen = /* @__PURE__ */ new Set();
260
+ for (const entry of entries) {
261
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return void 0;
262
+ const record = entry;
263
+ const identity = readId(record);
264
+ if ("issue" in identity || seen.has(identity.id)) return void 0;
265
+ const read = readPreset(record, identity.id);
266
+ if ("issue" in read) return void 0;
267
+ seen.add(identity.id);
268
+ presets.push(read.preset);
269
+ }
270
+ return presets;
271
+ }
272
+ //#endregion
273
+ //#region src/model/settings.ts
274
+ /**
275
+ * Settings V1 decoding (spec 4.2, 6.3). Decoding is defensive and lossless: it
276
+ * coerces shapes it recognises, falls back to documented defaults, and preserves
277
+ * anything it cannot render as a tombstone rather than repairing or dropping it.
278
+ * Catalog-aware canonicalization lives in `normalize.ts`.
279
+ */
280
+ /**
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.
284
+ */
285
+ const QUICK_ACTIONS_SETTINGS_NAMESPACE = "composer-quick-actions";
286
+ /**
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).
290
+ */
291
+ const QUICK_ACTIONS_CATALOG_NAMESPACE = "composer-quick-actions-catalog";
292
+ /** The state a fresh install starts from (spec 4.2). */
293
+ const DEFAULT_QUICK_ACTION_SETTINGS = Object.freeze({
294
+ schemaVersion: 1,
295
+ layout: "ribbon",
296
+ userActionsById: Object.freeze({}),
297
+ actionOrder: Object.freeze([]),
298
+ presetStateById: Object.freeze({})
299
+ });
300
+ function asRecord(value) {
301
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
302
+ return value;
303
+ }
304
+ /**
305
+ * Whether a stored entry is a live Send Action this release can render.
306
+ * Everything else is a tombstone: a higher version's `kind`, or a value whose
307
+ * label/text are not readable strings.
308
+ */
309
+ function isLiveQuickAction(value) {
310
+ return value.kind === "send" && typeof value.label === "string" && typeof value.text === "string";
311
+ }
312
+ /** Whether a stored entry must be preserved untouched and kept out of every projection (spec 5.3). */
313
+ function isQuickActionTombstone(value) {
314
+ return !isLiveQuickAction(value);
315
+ }
316
+ function decodeLayout(value) {
317
+ return QUICK_ACTION_LAYOUTS.find((layout) => layout === value) ?? DEFAULT_QUICK_ACTION_SETTINGS.layout;
318
+ }
319
+ /**
320
+ * Read one stored custom action into canonical form. A missing `kind` reads as
321
+ * `'send'` because V1 always writes the tag; anything else marks a tombstone and
322
+ * is handed straight back by identity, so a value written by a higher version
323
+ * survives the round trip intact.
324
+ *
325
+ * A live result always carries `kind`, `confirm` and `enabled` explicitly, which
326
+ * is what spec 4.3 requires of every normalized action. `confirm` is only
327
+ * defaulted when it is absent — never derived from the text.
328
+ */
329
+ function decodeStoredQuickAction(value) {
330
+ const entry = asRecord(value);
331
+ if (entry === void 0) return void 0;
332
+ if ((entry.kind ?? "send") !== "send" || typeof entry.label !== "string" || typeof entry.text !== "string") return entry;
333
+ const { icon, confirm, enabled, clonedFromPresetId } = entry;
334
+ return {
335
+ kind: "send",
336
+ label: entry.label,
337
+ text: entry.text,
338
+ ...typeof icon === "string" && icon !== "" ? { icon } : {},
339
+ confirm: typeof confirm === "boolean" ? confirm : true,
340
+ enabled: typeof enabled === "boolean" ? enabled : true,
341
+ ...typeof clonedFromPresetId === "string" ? { clonedFromPresetId } : {}
342
+ };
343
+ }
344
+ function decodeUserActions(value) {
345
+ const entries = asRecord(value);
346
+ if (entries === void 0) return {};
347
+ const decoded = {};
348
+ for (const [id, raw] of Object.entries(entries)) {
349
+ const action = decodeStoredQuickAction(raw);
350
+ if (action !== void 0) decoded[id] = action;
351
+ }
352
+ return decoded;
353
+ }
354
+ function decodeRef(value) {
355
+ const entry = asRecord(value);
356
+ if (entry === void 0) return void 0;
357
+ if (typeof entry.id !== "string" || entry.id === "") return void 0;
358
+ if (entry.source === "preset") return {
359
+ source: "preset",
360
+ id: entry.id
361
+ };
362
+ if (entry.source === "custom") return {
363
+ source: "custom",
364
+ id: entry.id
365
+ };
366
+ }
367
+ function decodeOrder(value) {
368
+ if (!Array.isArray(value)) return [];
369
+ return value.map(decodeRef).filter((ref) => ref !== void 0);
370
+ }
371
+ /**
372
+ * Canonicalize one preset state: `hidden` becomes an explicit boolean or
373
+ * disappears, and every other field is carried through so a higher version's own
374
+ * preference survives a downgrade (spec 5.3).
375
+ */
376
+ function canonicalPresetState(state) {
377
+ const rest = { ...state };
378
+ delete rest.hidden;
379
+ return state.hidden === true ? {
380
+ ...rest,
381
+ hidden: true
382
+ } : rest;
383
+ }
384
+ /** Whether a canonical preset state carries no user preference at all. */
385
+ function isEmptyPresetState(state) {
386
+ return Object.keys(state).length === 0;
387
+ }
388
+ function decodePresetState(value) {
389
+ const entries = asRecord(value);
390
+ if (entries === void 0) return {};
391
+ const decoded = {};
392
+ for (const [id, raw] of Object.entries(entries)) {
393
+ const state = asRecord(raw);
394
+ if (state === void 0) continue;
395
+ decoded[id] = canonicalPresetState(state);
396
+ }
397
+ return decoded;
398
+ }
399
+ /**
400
+ * Decode any published snapshot into V1. The stored `schemaVersion` is not a
401
+ * gate: a snapshot written by a higher version still yields every field this
402
+ * release understands, which is what makes a downgrade round trip lossless.
403
+ */
404
+ function decodeQuickActionSettings(raw) {
405
+ const stored = asRecord(raw);
406
+ if (stored === void 0) return DEFAULT_QUICK_ACTION_SETTINGS;
407
+ return {
408
+ schemaVersion: 1,
409
+ layout: decodeLayout(stored.layout),
410
+ userActionsById: decodeUserActions(stored.userActionsById),
411
+ actionOrder: decodeOrder(stored.actionOrder),
412
+ presetStateById: decodePresetState(stored.presetStateById)
413
+ };
414
+ }
415
+ //#endregion
416
+ //#region src/model/normalize.ts
417
+ /**
418
+ * Deterministic, idempotent canonicalization of decoded Settings against the
419
+ * current Preset Catalog (spec 5.3). The same input and catalog always produce
420
+ * the same result, and normalizing twice changes nothing — which is what lets the
421
+ * Host rewrite behind a revision fence without fighting its own writes.
422
+ *
423
+ * Normalization repairs references and re-states the discriminant, never content.
424
+ * In particular it never derives `confirm` from the text: that default is
425
+ * initialized when an action is created or cloned, so rewriting it here would undo
426
+ * the user's choice on every load and break idempotence.
427
+ */
428
+ /**
429
+ * Canonicalize one decoded snapshot against a catalog.
430
+ *
431
+ * Order: existing references keep their positions, minus repeats and minus
432
+ * references to custom actions that are gone; references to presets this catalog
433
+ * no longer carries stay put, because the same Preset Action ID coming back must
434
+ * restore the user's preference. Known actions with no reference are appended —
435
+ * presets in catalog order, then custom actions in stored order. Tombstones are
436
+ * never given a new reference, so a downgrade round trip returns their exact order.
437
+ */
438
+ function normalizeQuickActionSettings(settings, catalog) {
439
+ const knownPresetIds = new Set(catalog.presets.map((preset) => preset.id));
440
+ const placed = /* @__PURE__ */ new Set();
441
+ const actionOrder = [];
442
+ const place = (ref) => {
443
+ const key = quickActionRefKey(ref);
444
+ if (placed.has(key)) return;
445
+ placed.add(key);
446
+ actionOrder.push(ref);
447
+ };
448
+ const userActionsById = {};
449
+ for (const [id, stored] of Object.entries(settings.userActionsById)) {
450
+ const value = decodeStoredQuickAction(stored);
451
+ if (value !== void 0) userActionsById[id] = value;
452
+ }
453
+ for (const ref of settings.actionOrder) {
454
+ if (ref.source === "custom" && userActionsById[ref.id] === void 0) continue;
455
+ place(ref);
456
+ }
457
+ for (const preset of catalog.presets) place({
458
+ source: "preset",
459
+ id: preset.id
460
+ });
461
+ for (const [id, value] of Object.entries(userActionsById)) if (isLiveQuickAction(value)) place({
462
+ source: "custom",
463
+ id
464
+ });
465
+ const presetStateById = {};
466
+ for (const [id, stored] of Object.entries(settings.presetStateById)) {
467
+ const state = canonicalPresetState(stored);
468
+ if (!knownPresetIds.has(id)) presetStateById[id] = state;
469
+ else if (!isEmptyPresetState(state)) presetStateById[id] = state;
470
+ }
471
+ return {
472
+ schemaVersion: 1,
473
+ layout: settings.layout,
474
+ userActionsById,
475
+ actionOrder,
476
+ presetStateById
477
+ };
478
+ }
479
+ /**
480
+ * Project one snapshot against a catalog. The snapshot is canonicalized first, so
481
+ * the counts are the real totals even when the caller hands over state the catalog
482
+ * has moved on from — a known preset or a live action missing from the order must
483
+ * never slip past the action ceiling.
484
+ */
485
+ function projectQuickActions(raw, catalog) {
486
+ const settings = normalizeQuickActionSettings(raw, catalog);
487
+ const presets = new Map(catalog.presets.map((preset) => [preset.id, preset]));
488
+ const managed = [];
489
+ const referenced = /* @__PURE__ */ new Set();
490
+ let preserved = 0;
491
+ for (const ref of settings.actionOrder) {
492
+ if (ref.source === "preset") {
493
+ const preset = presets.get(ref.id);
494
+ if (preset === void 0) {
495
+ preserved += 1;
496
+ continue;
497
+ }
498
+ managed.push({
499
+ ref,
500
+ label: preset.label,
501
+ text: preset.text,
502
+ icon: preset.icon,
503
+ confirm: preset.confirm,
504
+ command: isCommandSendActionText(preset.text),
505
+ editable: false,
506
+ hidden: settings.presetStateById[ref.id]?.hidden === true,
507
+ clonedFromPresetId: void 0
508
+ });
509
+ continue;
510
+ }
511
+ referenced.add(ref.id);
512
+ const value = settings.userActionsById[ref.id];
513
+ if (value === void 0 || !isLiveQuickAction(value)) {
514
+ preserved += 1;
515
+ continue;
516
+ }
517
+ managed.push({
518
+ ref,
519
+ label: value.label,
520
+ text: value.text,
521
+ icon: value.icon,
522
+ confirm: value.confirm,
523
+ command: isCommandSendActionText(value.text),
524
+ editable: true,
525
+ hidden: !value.enabled,
526
+ clonedFromPresetId: value.clonedFromPresetId
527
+ });
528
+ }
529
+ for (const [id, value] of Object.entries(settings.userActionsById)) if (!referenced.has(id) && isQuickActionTombstone(value)) preserved += 1;
530
+ const composer = managed.filter((action) => !action.hidden);
531
+ const total = managed.length;
532
+ return {
533
+ layout: settings.layout,
534
+ managed,
535
+ composer,
536
+ counts: {
537
+ total,
538
+ limit: 50,
539
+ overflow: total > 50,
540
+ canAdd: total < 50,
541
+ visible: composer.length,
542
+ hidden: total - composer.length,
543
+ preserved
544
+ }
545
+ };
546
+ }
547
+ //#endregion
548
+ //#region src/model/mutations.ts
549
+ /**
550
+ * Revision-fenced Settings mutation planning (spec 5.2, 5.4, 6.3, 10).
551
+ *
552
+ * A planner is pure: it takes the last Host-confirmed snapshot and returns the
553
+ * canonical snapshot to persist together with the revision the write must be
554
+ * fenced to. It performs no I/O and mints no identity — the caller supplies each
555
+ * new Custom Action ID, so a UUID collision comes back as a refusal to retry
556
+ * rather than as a silent overwrite.
557
+ */
558
+ /** The draft the management form opens on; the only place the confirm default is applied. */
559
+ function newQuickActionDraft() {
560
+ return {
561
+ label: "",
562
+ text: "",
563
+ icon: "",
564
+ confirm: true
565
+ };
566
+ }
567
+ function refuse(reason, issues = []) {
568
+ return {
569
+ ok: false,
570
+ rejection: {
571
+ reason,
572
+ issues
573
+ }
574
+ };
575
+ }
576
+ function unknownAction() {
577
+ return refuse("unknown-action", [{
578
+ field: "id",
579
+ reason: "missing"
580
+ }]);
581
+ }
582
+ function planFrom(context, current, next) {
583
+ const canonical = normalizeQuickActionSettings(next, context.catalog);
584
+ return {
585
+ ok: true,
586
+ plan: {
587
+ expectedRevision: context.revision,
588
+ next: canonical,
589
+ changed: !deepEqualJson(current, canonical)
590
+ }
591
+ };
592
+ }
593
+ /** Every planner starts from the canonical reading of the confirmed snapshot. */
594
+ function currentOf(context) {
595
+ return normalizeQuickActionSettings(context.settings, context.catalog);
596
+ }
597
+ function withUserActions(settings, userActionsById) {
598
+ return {
599
+ ...settings,
600
+ userActionsById
601
+ };
602
+ }
603
+ function liveActionOf(settings, id) {
604
+ const value = settings.userActionsById[id];
605
+ if (value === void 0 || !isLiveQuickAction(value)) return void 0;
606
+ return value;
607
+ }
608
+ /** Build one stored Send Action with every field spec 4.3 requires stated explicitly. */
609
+ function sendActionValue(input) {
610
+ return {
611
+ kind: "send",
612
+ label: input.label,
613
+ text: input.text,
614
+ ...input.icon === void 0 ? {} : { icon: input.icon },
615
+ confirm: input.confirm,
616
+ enabled: input.enabled,
617
+ ...input.clonedFromPresetId === void 0 ? {} : { clonedFromPresetId: input.clonedFromPresetId }
618
+ };
619
+ }
620
+ /**
621
+ * Check a caller-minted Custom Action ID before it is persisted. A taken id comes
622
+ * back as `id-in-use` so the caller mints another UUID and retries; normalization
623
+ * must never renumber an id that is already stored (spec 4.1).
624
+ */
625
+ function identityRefusal(settings, id) {
626
+ if (id === "") return refuse("invalid-fields", [{
627
+ field: "id",
628
+ reason: "blank"
629
+ }]);
630
+ if (settings.userActionsById[id] !== void 0) return refuse("id-in-use", [{
631
+ field: "id",
632
+ reason: "duplicate"
633
+ }]);
634
+ }
635
+ function planWithAction(context, current, id, value) {
636
+ return planFrom(context, current, withUserActions(current, {
637
+ ...current.userActionsById,
638
+ [id]: value
639
+ }));
640
+ }
641
+ /** Create a Custom Quick Action under a caller-minted identity (spec 5.2, 5.4). */
642
+ function planCreateCustomQuickAction(context, input) {
643
+ const current = currentOf(context);
644
+ if (!projectQuickActions(current, context.catalog).counts.canAdd) return refuse("limit-reached");
645
+ const refusal = identityRefusal(current, input.id);
646
+ if (refusal !== void 0) return refusal;
647
+ const validated = validateQuickActionDraft(input.draft);
648
+ if (!validated.ok) return refuse("invalid-fields", validated.issues);
649
+ return planWithAction(context, current, input.id, sendActionValue({
650
+ ...validated.value,
651
+ enabled: true,
652
+ clonedFromPresetId: void 0
653
+ }));
654
+ }
655
+ /**
656
+ * Clone a Preset Quick Action into a new Custom Quick Action (spec 5.2).
657
+ * The confirmation policy is copied as it stands — including a Command Send
658
+ * Action the author left unconfirmed — never re-defaulted.
659
+ */
660
+ function planClonePresetQuickAction(context, input) {
661
+ const current = currentOf(context);
662
+ if (!projectQuickActions(current, context.catalog).counts.canAdd) return refuse("limit-reached");
663
+ const preset = context.catalog.presets.find((candidate) => candidate.id === input.presetId);
664
+ if (preset === void 0) return unknownAction();
665
+ const refusal = identityRefusal(current, input.id);
666
+ if (refusal !== void 0) return refusal;
667
+ return planWithAction(context, current, input.id, sendActionValue({
668
+ label: preset.label,
669
+ text: preset.text,
670
+ icon: preset.icon,
671
+ confirm: preset.confirm,
672
+ enabled: true,
673
+ clonedFromPresetId: preset.id
674
+ }));
675
+ }
676
+ /**
677
+ * Save edited content onto an existing Custom Quick Action. `enabled` and the
678
+ * Clone Provenance survive the edit; `confirm` comes from the form, which is the
679
+ * only place the user can change it.
680
+ */
681
+ function planUpdateCustomQuickAction(context, input) {
682
+ const current = currentOf(context);
683
+ const existing = liveActionOf(current, input.id);
684
+ if (existing === void 0) return unknownAction();
685
+ const validated = validateQuickActionDraft(input.draft);
686
+ if (!validated.ok) return refuse("invalid-fields", validated.issues);
687
+ return planWithAction(context, current, input.id, sendActionValue({
688
+ ...validated.value,
689
+ enabled: existing.enabled,
690
+ clonedFromPresetId: existing.clonedFromPresetId
691
+ }));
692
+ }
693
+ /** Disable or re-enable a Custom Quick Action; disabled actions stay in the management panel (spec 3). */
694
+ function planSetCustomQuickActionEnabled(context, input) {
695
+ const current = currentOf(context);
696
+ const existing = liveActionOf(current, input.id);
697
+ if (existing === void 0) return unknownAction();
698
+ return planWithAction(context, current, input.id, {
699
+ ...existing,
700
+ enabled: input.enabled
701
+ });
702
+ }
703
+ /** Delete a Custom Quick Action; normalization drops its order reference with it. */
704
+ function planDeleteCustomQuickAction(context, input) {
705
+ const current = currentOf(context);
706
+ if (liveActionOf(current, input.id) === void 0) return unknownAction();
707
+ const userActionsById = { ...current.userActionsById };
708
+ delete userActionsById[input.id];
709
+ return planFrom(context, current, withUserActions(current, userActionsById));
710
+ }
711
+ /** Hide or restore a Preset Quick Action; the author's definition is never touched (spec 5.1). */
712
+ function planSetPresetQuickActionHidden(context, input) {
713
+ const current = currentOf(context);
714
+ if (!context.catalog.presets.some((preset) => preset.id === input.presetId)) return unknownAction();
715
+ const presetStateById = { ...current.presetStateById };
716
+ if (input.hidden) presetStateById[input.presetId] = { hidden: true };
717
+ else delete presetStateById[input.presetId];
718
+ return planFrom(context, current, {
719
+ ...current,
720
+ presetStateById
721
+ });
722
+ }
723
+ /**
724
+ * Apply a new order to the actions the management panel lists. Preserved
725
+ * references — unknown presets and tombstones — keep their exact positions, so a
726
+ * reorder here never disturbs data a higher version owns.
727
+ */
728
+ function planReorderQuickActions(context, input) {
729
+ const current = currentOf(context);
730
+ const managed = projectQuickActions(current, context.catalog).managed.map((action) => action.ref);
731
+ const managedKeys = new Set(managed.map(quickActionRefKey));
732
+ const wanted = input.order.map(quickActionRefKey);
733
+ if (wanted.length !== managedKeys.size || new Set(wanted).size !== wanted.length) return refuse("invalid-order");
734
+ if (!wanted.every((key) => managedKeys.has(key))) return refuse("invalid-order");
735
+ const queue = [...input.order];
736
+ const actionOrder = current.actionOrder.map((ref) => managedKeys.has(quickActionRefKey(ref)) ? queue.shift() ?? ref : ref);
737
+ return planFrom(context, current, {
738
+ ...current,
739
+ actionOrder
740
+ });
741
+ }
742
+ /** Move one managed action to another position in the management list. */
743
+ function planMoveQuickAction(context, input) {
744
+ const managed = projectQuickActions(currentOf(context), context.catalog).managed.map((action) => action.ref);
745
+ const from = managed.findIndex((candidate) => quickActionRefKey(candidate) === quickActionRefKey(input.ref));
746
+ if (from === -1) return unknownAction();
747
+ if (!Number.isInteger(input.toIndex) || input.toIndex < 0 || input.toIndex >= managed.length) return refuse("invalid-order");
748
+ const order = [...managed];
749
+ const [moved] = order.splice(from, 1);
750
+ if (moved === void 0) return refuse("invalid-order");
751
+ order.splice(input.toIndex, 0, moved);
752
+ return planReorderQuickActions(context, { order });
753
+ }
754
+ /** Switch the global Quick Action Layout (spec 8.1). */
755
+ function planSetQuickActionLayout(context, input) {
756
+ const current = currentOf(context);
757
+ return planFrom(context, current, {
758
+ ...current,
759
+ layout: input.layout
760
+ });
761
+ }
762
+ //#endregion
763
+ //#region src/client/controller.ts
764
+ /**
765
+ * The Client's single owner of authoritative Quick Action state (spec 7.1).
766
+ *
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.
771
+ *
772
+ * What it deliberately does not own:
773
+ *
774
+ * - **Validation and migration authority.** The Host owns both (spec 6.3). Every
775
+ * rule applied here comes from the shared model, and a snapshot is decoded
776
+ * defensively rather than repaired: this face never rewrites before validating,
777
+ * never writes a file, and never treats browser storage as a source of truth.
778
+ * - **Anything session-scoped.** No Session, InputState, Slot props or
779
+ * `InputActions` object reaches this module — long-lived global state must not
780
+ * pin a session's runtime objects (spec 7.1). Draft occupancy, confirmation and
781
+ * 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
783
+ * 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).
785
+ */
786
+ /**
787
+ * How many Custom Action IDs a create or clone tries before giving up.
788
+ * Normalization must never renumber a stored id (spec 4.1), so the planner
789
+ * refuses a taken id and the controller mints another rather than overwriting
790
+ * anything; exhausting the attempts surfaces the refusal instead of hiding it.
791
+ */
792
+ const ID_MINT_ATTEMPTS = 4;
793
+ /** The five top-level fields of the persisted section (spec 4.2). */
794
+ 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
+ ];
822
+ }
823
+ /**
824
+ * Read the catalog off a bound namespace.
825
+ *
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).
829
+ */
830
+ function readCatalog(snapshot, document) {
831
+ if (snapshot.status === "loading") {
832
+ if (document.view !== void 0) return {
833
+ status: "error",
834
+ reason: "unavailable"
835
+ };
836
+ return document.error === null ? { status: "loading" } : {
837
+ status: "error",
838
+ reason: "unreadable"
839
+ };
840
+ }
841
+ if (snapshot.status === "unavailable" || snapshot.base === void 0) return {
842
+ status: "error",
843
+ reason: "unavailable"
844
+ };
845
+ const catalog = decodeCatalogSnapshot(snapshot.base);
846
+ if (catalog === void 0) return {
847
+ status: "error",
848
+ reason: "undecodable"
849
+ };
850
+ return {
851
+ status: "ready",
852
+ catalog
853
+ };
854
+ }
855
+ /**
856
+ * Read the confirmed user state off a bound namespace.
857
+ *
858
+ * Decoded, not canonicalized: canonicalization is catalog-relative, and whether
859
+ * a namespace is readable has nothing to do with whether the catalog is. Every
860
+ * consumer that needs the canonical form — the projection, each planner, the
861
+ * post-write comparison — derives it against the catalog it already holds.
862
+ */
863
+ function readSettings(snapshot) {
864
+ if (snapshot.status === "loading") return { status: "loading" };
865
+ if (snapshot.status === "unavailable" || snapshot.revision === void 0) return { status: "unavailable" };
866
+ return {
867
+ status: "ready",
868
+ settings: decodeQuickActionSettings(snapshot.value),
869
+ revision: snapshot.revision,
870
+ writable: snapshot.writable
871
+ };
872
+ }
873
+ function writeGate(catalog, settings, stale) {
874
+ if (catalog.status !== "ready" || settings.status !== "ready") return {
875
+ ok: false,
876
+ failure: { kind: "not-ready" }
877
+ };
878
+ if (!settings.writable || stale) return {
879
+ ok: false,
880
+ failure: { kind: "read-only" }
881
+ };
882
+ return {
883
+ ok: true,
884
+ context: {
885
+ settings: settings.settings,
886
+ catalog: catalog.catalog,
887
+ revision: String(settings.revision)
888
+ },
889
+ catalog: catalog.catalog,
890
+ revision: settings.revision
891
+ };
892
+ }
893
+ /**
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.
897
+ */
898
+ function createQuickActionsController(options) {
899
+ 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();
909
+ const listeners = /* @__PURE__ */ new Set();
910
+ let disposed = false;
911
+ let manager = { open: false };
912
+ let writing = 0;
913
+ let failure;
914
+ let connectionState = options.connection.state.getSnapshot();
915
+ let state = derive();
916
+ /** Serializes this controller's own writes so each plan reads the previous one back. */
917
+ let tail = Promise.resolve();
918
+ function derive() {
919
+ const catalog = readCatalog(catalogScope.getSnapshot(), mirror.getSnapshot());
920
+ const settings = readSettings(settingsScope.getSnapshot());
921
+ const projection = catalog.status === "ready" ? projectQuickActions(settings.status === "ready" ? settings.settings : DEFAULT_QUICK_ACTION_SETTINGS, catalog.catalog) : void 0;
922
+ const stale = connectionState !== void 0 && connectionState !== "connected";
923
+ return {
924
+ catalog,
925
+ settings,
926
+ projection,
927
+ readOnly: !writeGate(catalog, settings, stale).ok,
928
+ stale,
929
+ manager,
930
+ writing: writing > 0,
931
+ ...failure === void 0 ? {} : { failure }
932
+ };
933
+ }
934
+ /** Republish only on a real change, so a subscriber never re-renders for nothing. */
935
+ function publish() {
936
+ if (disposed) return;
937
+ const next = derive();
938
+ if (deepEqualJson(state, next)) return;
939
+ state = next;
940
+ for (const listener of Array.from(listeners)) listener();
941
+ }
942
+ const stopSources = [
943
+ mirror.subscribe(publish),
944
+ catalogScope.subscribe(publish),
945
+ settingsScope.subscribe(publish)
946
+ ];
947
+ const stopConnection = options.connection.state.subscribe(() => {
948
+ connectionState = options.connection.state.getSnapshot();
949
+ publish();
950
+ });
951
+ /**
952
+ * Run one planner, re-minting on a taken Custom Action ID. Only a planner that
953
+ * mints can answer `id-in-use`, so this loop costs nothing for the others.
954
+ */
955
+ function plan(planner, context) {
956
+ let outcome = planner(context);
957
+ for (let attempt = 1; attempt < ID_MINT_ATTEMPTS; attempt += 1) {
958
+ if (outcome.ok || outcome.rejection.reason !== "id-in-use") return outcome;
959
+ outcome = planner(context);
960
+ }
961
+ return outcome;
962
+ }
963
+ /**
964
+ * Classify one settled write against the snapshot the scope published for it.
965
+ *
966
+ * The stored state reading back as the plan is the only positive evidence of a
967
+ * commit. Failing that, a namespace revision that moved elsewhere is a lost
968
+ * fence and anything else is a Host refusal — with one bounded blind spot: if
969
+ * the recovery read that follows a refusal also fails, a lost fence is
970
+ * indistinguishable from a refusal and is reported as the latter. In that
971
+ * window the connection is down, so the surfaces are already read-only, and
972
+ * retrying against the stale fence is refused again rather than overwriting
973
+ * the writer that won.
974
+ */
975
+ function classify(catalog, next, fence) {
976
+ const snapshot = settingsScope.getSnapshot();
977
+ const settings = readSettings(snapshot);
978
+ if (settings.status === "ready" && deepEqualJson(normalizeQuickActionSettings(settings.settings, catalog), next)) return {
979
+ ok: true,
980
+ changed: true
981
+ };
982
+ if (snapshot.revision !== fence) return {
983
+ ok: false,
984
+ failure: { kind: "conflict" }
985
+ };
986
+ return {
987
+ ok: false,
988
+ failure: { kind: "refused" }
989
+ };
990
+ }
991
+ function settle(outcome) {
992
+ failure = outcome.ok ? void 0 : outcome.failure;
993
+ return outcome;
994
+ }
995
+ function submit(planner) {
996
+ if (disposed) return Promise.resolve({
997
+ ok: false,
998
+ failure: { kind: "not-ready" }
999
+ });
1000
+ writing += 1;
1001
+ publish();
1002
+ const task = tail.then(async () => {
1003
+ if (disposed) return {
1004
+ ok: false,
1005
+ failure: { kind: "not-ready" }
1006
+ };
1007
+ const gate = writeGate(state.catalog, state.settings, state.stale);
1008
+ if (!gate.ok) return settle(gate);
1009
+ try {
1010
+ const outcome = plan(planner, gate.context);
1011
+ if (!outcome.ok) return settle({
1012
+ ok: false,
1013
+ failure: {
1014
+ kind: "rejected",
1015
+ rejection: outcome.rejection
1016
+ }
1017
+ });
1018
+ if (!outcome.plan.changed) return settle({
1019
+ ok: true,
1020
+ changed: false
1021
+ });
1022
+ await settingsScope.mutate(sectionOps(outcome.plan.next), gate.revision);
1023
+ return settle(classify(gate.catalog, outcome.plan.next, gate.revision));
1024
+ } catch (error) {
1025
+ await mirror.load().catch(() => void 0);
1026
+ return settle({
1027
+ ok: false,
1028
+ failure: {
1029
+ kind: "failed",
1030
+ message: messageOf$1(error)
1031
+ }
1032
+ });
1033
+ }
1034
+ });
1035
+ tail = task.catch(() => void 0);
1036
+ return task.finally(() => {
1037
+ writing -= 1;
1038
+ publish();
1039
+ });
1040
+ }
1041
+ return {
1042
+ getSnapshot: () => state,
1043
+ subscribe(listener) {
1044
+ listeners.add(listener);
1045
+ return () => {
1046
+ listeners.delete(listener);
1047
+ };
1048
+ },
1049
+ refresh: async () => {
1050
+ if (disposed) return;
1051
+ await mirror.load();
1052
+ },
1053
+ openManager() {
1054
+ manager = { open: true };
1055
+ publish();
1056
+ },
1057
+ closeManager() {
1058
+ manager = { open: false };
1059
+ publish();
1060
+ },
1061
+ dismissFailure() {
1062
+ failure = void 0;
1063
+ publish();
1064
+ },
1065
+ createCustomAction: (draft) => submit((context) => planCreateCustomQuickAction(context, {
1066
+ id: mintId(),
1067
+ draft
1068
+ })),
1069
+ clonePreset: (presetId) => submit((context) => planClonePresetQuickAction(context, {
1070
+ presetId,
1071
+ id: mintId()
1072
+ })),
1073
+ updateCustomAction: (id, draft) => submit((context) => planUpdateCustomQuickAction(context, {
1074
+ id,
1075
+ draft
1076
+ })),
1077
+ setCustomActionEnabled: (id, enabled) => submit((context) => planSetCustomQuickActionEnabled(context, {
1078
+ id,
1079
+ enabled
1080
+ })),
1081
+ deleteCustomAction: (id) => submit((context) => planDeleteCustomQuickAction(context, { id })),
1082
+ setPresetHidden: (presetId, hidden) => submit((context) => planSetPresetQuickActionHidden(context, {
1083
+ presetId,
1084
+ hidden
1085
+ })),
1086
+ reorderActions: (order) => submit((context) => planReorderQuickActions(context, { order })),
1087
+ moveAction: (ref, toIndex) => submit((context) => planMoveQuickAction(context, {
1088
+ ref,
1089
+ toIndex
1090
+ })),
1091
+ setLayout: (layout) => submit((context) => planSetQuickActionLayout(context, { layout })),
1092
+ /**
1093
+ * Release what this module owns. The two scope bindings are withdrawn by the
1094
+ * binder's own disposer on the fiber this controller was created on, so the
1095
+ * fiber and this call together leave nothing behind.
1096
+ */
1097
+ dispose() {
1098
+ disposed = true;
1099
+ for (const stop of stopSources) stop();
1100
+ stopConnection();
1101
+ listeners.clear();
1102
+ }
1103
+ };
1104
+ }
1105
+ function messageOf$1(error) {
1106
+ return error instanceof Error ? error.message : String(error);
1107
+ }
1108
+ //#endregion
1109
+ //#region src/client/session/guards.ts
1110
+ /**
1111
+ * Whether one published list field carries content, read totally.
1112
+ *
1113
+ * A field that is absent or not an array counts as content. The snapshot is the
1114
+ * only evidence this guard has, and DSH has renamed a field of it out from
1115
+ * under this plugin once already (`imageIds` → `attachmentIds` in 0.1.5-rc.1,
1116
+ * ticket 29). If that happens again, a Quick Action must go inert rather than
1117
+ * load a draft whose remaining content this guard can no longer see — an action
1118
+ * that fired blind would submit its own text *plus* whatever the user had
1119
+ * attached.
1120
+ *
1121
+ * This is not the compatibility fallback spec 21.3 rules out: no superseded
1122
+ * field name is ever read, and nothing degrades to a working state. The only
1123
+ * thing it buys is that an unreadable snapshot reports `occupied-draft` instead
1124
+ * of throwing through `derive → publish → observe` and taking the whole surface
1125
+ * down with it.
1126
+ */
1127
+ function holdsContent(field) {
1128
+ if (!Array.isArray(field)) return true;
1129
+ return field.length > 0;
1130
+ }
1131
+ /**
1132
+ * Whether the draft is occupied (spec 9.2). Occupancy is any text — pure
1133
+ * whitespace included, so the raw string is compared against `''` and never
1134
+ * trimmed — any attachment, and any rich reference.
1135
+ *
1136
+ * The public snapshot exposes exactly `{ draft, attachmentIds, draftRev, phase,
1137
+ * claim?, occurrences, queue }`. Occupancy reads three of them:
1138
+ *
1139
+ * - `draft` — the clipboard-text projection of the whole document. Compared
1140
+ * against `''`, so a snapshot without it is occupied for the same reason
1141
+ * {@link holdsContent} gives;
1142
+ * - `attachmentIds` — the ordered draft attachments, and the only public
1143
+ * attachment field there is. Since 0.1.5-rc.1 it admits any attachment kind,
1144
+ * not just images;
1145
+ * - `occurrences` — the reference chips. They are already expanded inside
1146
+ * `draft`, so this test is redundant today; it is kept because a future chip
1147
+ * whose clipboard projection is empty must still count as content the send
1148
+ * action would carry away.
1149
+ *
1150
+ * The remaining fields are not occupancy: `draftRev` is a revision counter,
1151
+ * `phase`/`claim` are the submit plane (handled by {@link composerGate}), and
1152
+ * `queue` is the Session's transient inbox, which spec 9.2 explicitly allows a
1153
+ * send to join.
1154
+ */
1155
+ function isOccupiedDraft(input) {
1156
+ return input.draft !== "" || holdsContent(input.attachmentIds) || holdsContent(input.occurrences);
1157
+ }
1158
+ /**
1159
+ * The composer-level guard, reproducing the shipped send button's own
1160
+ * conditions from public state alone (spec 9.2).
1161
+ *
1162
+ * `disabled` in spec 9.2's list is the shipped bar's inert state, which is
1163
+ * reached only without a Session or on the blank-session hero. Neither can
1164
+ * occur here: both dock Slots are session-scoped, and the Quick Action surfaces
1165
+ * render only where the Resident Composer predicate holds.
1166
+ */
1167
+ function composerGate(input, session, block) {
1168
+ if (session.removed) return "session-removed";
1169
+ if (block !== void 0) return "composer-blocked";
1170
+ if (session.subagent?.address.mode === "continuable" && session.subagent.parentAvailable !== true) return "parent-offline";
1171
+ if (input.phase !== "plain") return "composer-busy";
1172
+ if (isOccupiedDraft(input)) return "occupied-draft";
1173
+ }
1174
+ //#endregion
1175
+ //#region src/client/session/execution.ts
1176
+ /**
1177
+ * The per-Session Quick Action execution layer (spec 7.2, 9.2–9.5).
1178
+ *
1179
+ * One engine per Session owns the current `InputActions`, the confirmation in
1180
+ * flight and the send single-flight. Nothing global holds it: the registry below
1181
+ * hands the same engine to whichever Slot entry is currently rendering the
1182
+ * layout, so switching layouts re-mounts a component without ever minting a
1183
+ * second lock for the same Session (spec 7.2).
1184
+ *
1185
+ * ## The single-flight window
1186
+ *
1187
+ * Spec 9.5 asks for a window that runs from the first activation until the
1188
+ * official submission stage ends, judged only from the published Input snapshot
1189
+ * `{ draft, attachmentIds, draftRev, phase, claim?, occurrences, queue }`, with the
1190
+ * hard acceptance criterion that it never produces a duplicate send.
1191
+ *
1192
+ * The mutex itself is this module's own: {@link QuickActionSessionEngine.activate}
1193
+ * claims it synchronously, before any await and before the draft is touched. It
1194
+ * is deliberately not derived from draft occupancy — the official default sink
1195
+ * clears optimistically, so a second activation one frame later would find an
1196
+ * empty draft and pass every precondition. That is the duplicate-send trap, and
1197
+ * the regression test for it pins two activations in the same tick.
1198
+ *
1199
+ * What the public snapshot is used for is deciding *when the window may close*,
1200
+ * and the answer differs by the path the official machine took. Both readings
1201
+ * come from one place: `SessionInputShell.submit()` feeds an `enter` event to a
1202
+ * pure submit machine and executes the resulting effects synchronously, then
1203
+ * publishes. So the first snapshot observed after `submit()` returns already
1204
+ * carries the machine's verdict.
1205
+ *
1206
+ * - **The machine kept the frozen slot** (`phase` is `adjudicating` or
1207
+ * `submitting`). Our attempt owns that slot exclusively — the machine refuses
1208
+ * another `enter` while it is held — so the phase is attributable to this
1209
+ * submission. The window stays open until the phase leaves those states; that
1210
+ * is the latest reliable public boundary of the official stage, and past it
1211
+ * every outcome belongs to DSH's own feedback (spec 9.5).
1212
+ * - **The machine committed an ordinary send** (`phase` back to `plain` and
1213
+ * `draft` empty). The optimistic commit is the official stage's last publicly
1214
+ * observable step: the default sink runs detached, and nothing about it
1215
+ * reaches the public snapshot. So the commit is the latest boundary
1216
+ * attributable to this submission, and the window closes there.
1217
+ * - **The machine refused the submission** (`phase` still `plain` and `draft`
1218
+ * still holding content). Nothing was sent; the text stays exactly as loaded
1219
+ * and the window closes with "not sent, text retained".
1220
+ *
1221
+ * The ordinary-send test is emptiness, not equality with the text that was
1222
+ * loaded: `draft` is the editor's clipboard-text projection rather than the
1223
+ * string handed to `setDraft`, so comparing the two would make the verdict
1224
+ * depend on that round trip. Emptiness needs no such assumption — the load only
1225
+ * ever runs against a verified-unoccupied draft, so anything in `draft` after it
1226
+ * is this feature's own text, and only the official commit clears it.
1227
+ *
1228
+ * The window also never closes inside the activation that opened it: settlement
1229
+ * is read from {@link QuickActionSessionEngine.observe}, which the owning entry
1230
+ * calls once per commit, so two activations in one tick can only ever produce
1231
+ * one send however fast the official sink empties the draft.
1232
+ *
1233
+ * Nothing here reads the DOM, Lexical, a private event or a private state, and
1234
+ * no DSH object is retained beyond the Session it belongs to.
1235
+ */
1236
+ /**
1237
+ * The executable identity of an action: everything a re-verification must find
1238
+ * unchanged before the draft is touched (spec 9.3). `command` is included even
1239
+ * though it is derived from `text`, so a rewrite that moves the action across
1240
+ * the Command Send Action boundary is rejected under its own name.
1241
+ */
1242
+ function identityOf(action) {
1243
+ return JSON.stringify([
1244
+ quickActionRefKey(action.ref),
1245
+ action.label,
1246
+ action.text,
1247
+ action.confirm,
1248
+ action.command,
1249
+ action.hidden
1250
+ ]);
1251
+ }
1252
+ function messageOf(error) {
1253
+ return error instanceof Error ? error.message : String(error);
1254
+ }
1255
+ /** Create the execution engine for one Session. */
1256
+ function createQuickActionSessionEngine(sessionId) {
1257
+ const listeners = /* @__PURE__ */ new Set();
1258
+ let disposed = false;
1259
+ let observed;
1260
+ let actions;
1261
+ let composer = [];
1262
+ let flight;
1263
+ let confirming;
1264
+ let identity;
1265
+ let feedback;
1266
+ /** True while `run` is on the stack, so no settlement can close its own flight. */
1267
+ let running = false;
1268
+ let state = derive();
1269
+ function derive() {
1270
+ return {
1271
+ unavailable: flight !== void 0 ? "sending" : observed === void 0 ? "composer-busy" : composerGate(observed.input, observed.session, observed.block),
1272
+ confirming,
1273
+ sending: flight !== void 0,
1274
+ activeRef: flight?.ref,
1275
+ feedback
1276
+ };
1277
+ }
1278
+ function publish() {
1279
+ if (disposed) return;
1280
+ const next = derive();
1281
+ if (next.unavailable === state.unavailable && next.confirming === state.confirming && next.sending === state.sending && next.activeRef === state.activeRef && next.feedback === state.feedback) return;
1282
+ state = next;
1283
+ for (const listener of Array.from(listeners)) listener();
1284
+ }
1285
+ /** End the flight, optionally reporting why. */
1286
+ function settle(next) {
1287
+ flight = void 0;
1288
+ confirming = void 0;
1289
+ identity = void 0;
1290
+ feedback = next;
1291
+ publish();
1292
+ }
1293
+ /** The action as the current Composer projection carries it, if it still does. */
1294
+ function shownAction(ref) {
1295
+ const key = quickActionRefKey(ref);
1296
+ const found = composer.find((action) => quickActionRefKey(action.ref) === key);
1297
+ return found === void 0 || found.hidden ? void 0 : found;
1298
+ }
1299
+ /** Re-read everything spec 9.3 lists, immediately before the draft is touched. */
1300
+ function reverify(ref) {
1301
+ const found = shownAction(ref);
1302
+ if (found === void 0) return void 0;
1303
+ if (identity !== void 0 && identityOf(found) !== identity) return void 0;
1304
+ if (observed === void 0 || observed.session.sessionId !== sessionId) return void 0;
1305
+ if (composerGate(observed.input, observed.session, observed.block) !== void 0) return void 0;
1306
+ return found;
1307
+ }
1308
+ /**
1309
+ * Load the static text and hand it to the official submit path (spec 9.4).
1310
+ * The two calls are the whole write side of this feature; everything before
1311
+ * them is verification and everything after is observation.
1312
+ */
1313
+ function run(action) {
1314
+ const face = actions;
1315
+ if (face === void 0 || observed === void 0) {
1316
+ settle({ kind: "state-changed" });
1317
+ return;
1318
+ }
1319
+ running = true;
1320
+ try {
1321
+ try {
1322
+ face.setDraft(action.text);
1323
+ } catch (error) {
1324
+ settle({
1325
+ kind: "failed",
1326
+ message: messageOf(error)
1327
+ });
1328
+ return;
1329
+ }
1330
+ flight = {
1331
+ stage: "submitted",
1332
+ ref: action.ref
1333
+ };
1334
+ confirming = void 0;
1335
+ try {
1336
+ face.submit();
1337
+ } catch (error) {
1338
+ settle({ kind: "retained" });
1339
+ return;
1340
+ }
1341
+ } finally {
1342
+ running = false;
1343
+ }
1344
+ publish();
1345
+ }
1346
+ return {
1347
+ getSnapshot: () => state,
1348
+ subscribe(listener) {
1349
+ listeners.add(listener);
1350
+ return () => {
1351
+ listeners.delete(listener);
1352
+ };
1353
+ },
1354
+ observe(input, session, block) {
1355
+ if (disposed) return;
1356
+ observed = {
1357
+ input,
1358
+ session,
1359
+ block
1360
+ };
1361
+ if (running) return;
1362
+ if (flight?.stage === "submitted") {
1363
+ if (input.phase === "adjudicating" || input.phase === "submitting") {
1364
+ flight = {
1365
+ stage: "official",
1366
+ ref: flight.ref
1367
+ };
1368
+ confirming = void 0;
1369
+ feedback = void 0;
1370
+ } else if (input.draft === "") settle(void 0);
1371
+ else settle({ kind: "retained" });
1372
+ } else if (flight?.stage === "official" && input.phase !== "adjudicating" && input.phase !== "submitting") settle(void 0);
1373
+ publish();
1374
+ },
1375
+ bind(face, projection) {
1376
+ if (disposed) return;
1377
+ actions = face;
1378
+ composer = projection;
1379
+ if (confirming !== void 0 && shownAction(confirming.ref) === void 0) settle(void 0);
1380
+ publish();
1381
+ },
1382
+ activate(action) {
1383
+ if (disposed) return;
1384
+ if (flight !== void 0) return;
1385
+ if (observed === void 0 || observed.session.sessionId !== sessionId) {
1386
+ settle({ kind: "state-changed" });
1387
+ return;
1388
+ }
1389
+ if (shownAction(action.ref) === void 0 || composerGate(observed.input, observed.session, observed.block) !== void 0) {
1390
+ settle({ kind: "state-changed" });
1391
+ return;
1392
+ }
1393
+ identity = identityOf(action);
1394
+ feedback = void 0;
1395
+ flight = {
1396
+ stage: "pending",
1397
+ ref: action.ref
1398
+ };
1399
+ if (action.confirm) {
1400
+ confirming = {
1401
+ ref: action.ref,
1402
+ label: action.label,
1403
+ text: action.text,
1404
+ command: action.command
1405
+ };
1406
+ publish();
1407
+ return;
1408
+ }
1409
+ const fresh = reverify(action.ref);
1410
+ if (fresh === void 0) {
1411
+ settle({ kind: "state-changed" });
1412
+ return;
1413
+ }
1414
+ run(fresh);
1415
+ },
1416
+ confirm() {
1417
+ if (disposed || flight?.stage !== "pending" || confirming === void 0) return;
1418
+ const fresh = reverify(flight.ref);
1419
+ if (fresh === void 0) {
1420
+ settle({ kind: "state-changed" });
1421
+ return;
1422
+ }
1423
+ run(fresh);
1424
+ },
1425
+ cancel() {
1426
+ if (disposed || flight?.stage !== "pending" || confirming === void 0) return;
1427
+ settle(void 0);
1428
+ },
1429
+ dismissFeedback() {
1430
+ if (disposed || feedback === void 0) return;
1431
+ feedback = void 0;
1432
+ publish();
1433
+ },
1434
+ cancelPending() {
1435
+ if (disposed || flight === void 0) return;
1436
+ if (flight.stage !== "pending") return;
1437
+ settle(void 0);
1438
+ },
1439
+ dispose() {
1440
+ disposed = true;
1441
+ listeners.clear();
1442
+ actions = void 0;
1443
+ observed = void 0;
1444
+ composer = [];
1445
+ }
1446
+ };
1447
+ }
1448
+ function createQuickActionSessionRegistry() {
1449
+ const engines = /* @__PURE__ */ new Map();
1450
+ return {
1451
+ engineFor(sessionId) {
1452
+ let engine = engines.get(sessionId);
1453
+ if (engine === void 0) {
1454
+ engine = createQuickActionSessionEngine(sessionId);
1455
+ engines.set(sessionId, engine);
1456
+ }
1457
+ return engine;
1458
+ },
1459
+ dispose() {
1460
+ for (const engine of engines.values()) {
1461
+ engine.cancelPending();
1462
+ engine.dispose();
1463
+ }
1464
+ engines.clear();
1465
+ }
1466
+ };
1467
+ }
1468
+ //#endregion
1469
+ //#region src/client/surfaces/residency.ts
1470
+ function createResidentComposerRegistry() {
1471
+ const marks = /* @__PURE__ */ new Map();
1472
+ const listeners = /* @__PURE__ */ new Set();
1473
+ function publish() {
1474
+ for (const listener of Array.from(listeners)) listener();
1475
+ }
1476
+ return {
1477
+ mark(sessionId) {
1478
+ marks.set(sessionId, (marks.get(sessionId) ?? 0) + 1);
1479
+ publish();
1480
+ let withdrawn = false;
1481
+ return () => {
1482
+ if (withdrawn) return;
1483
+ withdrawn = true;
1484
+ const held = (marks.get(sessionId) ?? 1) - 1;
1485
+ if (held > 0) marks.set(sessionId, held);
1486
+ else marks.delete(sessionId);
1487
+ publish();
1488
+ };
1489
+ },
1490
+ isResident: (sessionId) => marks.has(sessionId),
1491
+ primarySessionId: () => marks.keys().next().value,
1492
+ subscribe(listener) {
1493
+ listeners.add(listener);
1494
+ return () => {
1495
+ listeners.delete(listener);
1496
+ };
1497
+ }
1498
+ };
1499
+ }
1500
+ //#endregion
1501
+ //#region src/client/surfaces/ErrorBoundary.tsx
1502
+ /**
1503
+ * One Slot entry's local error boundary (spec 7.3).
1504
+ *
1505
+ * A render error inside the Quick Action surfaces replaces the Quick Action
1506
+ * area and nothing else: the Composer, its draft and its submit button keep
1507
+ * working, and the user can ask for the surface back. Retrying remounts the
1508
+ * subtree by changing its key, so a transient failure clears without a reload.
1509
+ *
1510
+ * DSH's own submission errors never reach here — they are reported by the
1511
+ * Composer, and this feature must not repeat them (spec 9.5).
1512
+ */
1513
+ var SurfaceErrorBoundary = class extends react.Component {
1514
+ state = {
1515
+ failed: false,
1516
+ attempt: 0
1517
+ };
1518
+ static getDerivedStateFromError() {
1519
+ return { failed: true };
1520
+ }
1521
+ componentDidCatch(error, info) {
1522
+ console.error("[composer-quick-actions] surface failed", error, info.componentStack);
1523
+ }
1524
+ retry = () => {
1525
+ this.setState((current) => ({
1526
+ failed: false,
1527
+ attempt: current.attempt + 1
1528
+ }));
1529
+ };
1530
+ render() {
1531
+ const { t, children } = this.props;
1532
+ if (!this.state.failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children }, this.state.attempt);
1533
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1534
+ className: "dsh-cqa-note",
1535
+ role: "status",
1536
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1537
+ className: "dsh-cqa-note-text",
1538
+ children: t("crash.title")
1539
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1540
+ type: "button",
1541
+ className: "dsh-cqa-link",
1542
+ onClick: this.retry,
1543
+ children: t("crash.retry")
1544
+ })]
1545
+ });
1546
+ }
1547
+ };
1548
+ //#endregion
1549
+ //#region src/client/surfaces/ActionFace.tsx
1550
+ function ActionFace({ action, t }) {
1551
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1552
+ action.icon === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1553
+ className: "dsh-cqa-icon",
1554
+ "aria-hidden": "true",
1555
+ children: action.icon
1556
+ }),
1557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1558
+ className: "dsh-cqa-label",
1559
+ children: action.label
1560
+ }),
1561
+ action.command ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
1562
+ className: "dsh-cqa-badge",
1563
+ children: t("command.badge")
1564
+ }) : null
1565
+ ] });
1566
+ }
1567
+ function densityFor(width) {
1568
+ return width > 0 && width < 480 ? "narrow" : "wide";
1569
+ }
1570
+ /**
1571
+ * How many leading actions the `bar` layout shows (spec 8.1). The rest fold into
1572
+ * the shared action panel behind "more", and the management entry stays visible
1573
+ * whatever the answer is.
1574
+ *
1575
+ * Before the first measurement — a fresh mount, or a headless environment with
1576
+ * no layout — every width reads as zero. That answers "all of them": showing the
1577
+ * full row and letting the browser's own overflow handle it for one frame is
1578
+ * better than flashing a "more" button that the next frame withdraws.
1579
+ */
1580
+ function fitActionCount(input) {
1581
+ const { available, widths, reserved, gap } = input;
1582
+ if (widths.length === 0) return 0;
1583
+ if (available <= 0 || widths.every((width) => width <= 0)) return widths.length;
1584
+ let used = reserved;
1585
+ let shown = 0;
1586
+ for (const width of widths) {
1587
+ const next = used + width + (shown === 0 && reserved === 0 ? 0 : gap);
1588
+ if (next > available) break;
1589
+ used = next;
1590
+ shown += 1;
1591
+ }
1592
+ return shown;
1593
+ }
1594
+ //#endregion
1595
+ //#region src/client/manager/search.ts
1596
+ /** The fields a query is compared against. */
1597
+ const SEARCH_FIELDS = ["label", "text"];
1598
+ /**
1599
+ * The one normalization both sides of a comparison go through. Applying it to
1600
+ * the query and to the haystack through the same function is what keeps the two
1601
+ * from drifting apart.
1602
+ */
1603
+ function normalizeQuickActionSearchText(value) {
1604
+ return value.normalize("NFKC").toLowerCase().replace(/\s+/gu, " ").trim();
1605
+ }
1606
+ /**
1607
+ * Whether a query asks for anything at all. A whitespace-only box is an empty
1608
+ * one, and telling the two apart is what lets a panel say "nothing matched"
1609
+ * rather than "nothing to run".
1610
+ */
1611
+ function hasQuickActionQuery(query) {
1612
+ return normalizeQuickActionSearchText(query) !== "";
1613
+ }
1614
+ /**
1615
+ * The actions matching `query`, in their original relative order.
1616
+ *
1617
+ * An empty or whitespace-only query hands the same array back by identity: an
1618
+ * unsearched panel is not a filtered panel, and returning a copy would remount
1619
+ * every row for nothing.
1620
+ */
1621
+ function filterQuickActions(actions, query) {
1622
+ const needle = normalizeQuickActionSearchText(query);
1623
+ if (needle === "") return actions;
1624
+ return actions.filter((action) => SEARCH_FIELDS.some((field) => normalizeQuickActionSearchText(action[field]).includes(needle)));
1625
+ }
1626
+ //#endregion
1627
+ //#region src/client/modal.ts
1628
+ /**
1629
+ * The modal semantics every Quick Action panel shares (spec 8.4).
1630
+ *
1631
+ * Spec 8.4 makes Escape cancellation, a clear focus ring, keyboard traversal and
1632
+ * focus return after a panel closes hard gates. A Slot entry renders in place,
1633
+ * inside the composer stack, so without a Tab boundary a keyboard user would tab
1634
+ * straight out of an open panel into the draft behind it, and every route back
1635
+ * into the panel would take the Escape handler with it.
1636
+ *
1637
+ * That holds for the management overlay too, even though ticket 26 moved it onto
1638
+ * `document.body` through a portal: a portal relocates the DOM node, not the
1639
+ * React tree, so its events still bubble to the dock and its Tab order still
1640
+ * follows document order — which is now the end of the body, further from the
1641
+ * draft rather than nearer it. Both hooks below are unaffected either way,
1642
+ * because both work off the panel element they are handed.
1643
+ *
1644
+ * This module is a leaf beside `dsh.ts`, owned by no directory of spec 14's
1645
+ * source map: the confirmation panel (`session/`), the action panel and the
1646
+ * management panel (`manager/`) all need it, and putting it inside any one of
1647
+ * them would make another depend on that one's directory for something that is
1648
+ * neither an action nor an execution concern.
1649
+ *
1650
+ * Both hooks are deliberately tiny and DOM-only: they read `document.activeElement`
1651
+ * and call `focus()`, which is the browser's own focus contract, not DSH's. No
1652
+ * Composer state, private event or Lexical path is touched (spec 9.1).
1653
+ */
1654
+ /**
1655
+ * What Tab may reach inside a panel. Disabled controls are excluded, which is
1656
+ * what keeps the boundary correct while a write is in flight and half the
1657
+ * panel's buttons are inert.
1658
+ */
1659
+ const FOCUSABLE_SELECTOR = [
1660
+ "a[href]",
1661
+ "button:not([disabled])",
1662
+ "input:not([disabled])",
1663
+ "select:not([disabled])",
1664
+ "textarea:not([disabled])",
1665
+ "[tabindex]:not([tabindex=\"-1\"])"
1666
+ ].join(",");
1667
+ /**
1668
+ * Escape cancels, and Tab stays inside.
1669
+ *
1670
+ * The focusable set is recomputed on every keystroke rather than cached: a
1671
+ * management panel grows a form, a form's Save button goes inert while a write
1672
+ * is in flight, and a stale boundary would trap focus on a control that is no
1673
+ * longer there.
1674
+ */
1675
+ function useModalKeys(onCancel) {
1676
+ const panelRef = (0, react.useRef)(null);
1677
+ return {
1678
+ panelRef,
1679
+ onKeyDown: (0, react.useCallback)((event) => {
1680
+ if (event.key === "Escape") {
1681
+ event.stopPropagation();
1682
+ onCancel();
1683
+ return;
1684
+ }
1685
+ if (event.key !== "Tab") return;
1686
+ const focusable = Array.from(panelRef.current?.querySelectorAll(FOCUSABLE_SELECTOR) ?? []);
1687
+ const first = focusable[0];
1688
+ const last = focusable[focusable.length - 1];
1689
+ if (first === void 0 || last === void 0) return;
1690
+ const edge = event.shiftKey ? first : last;
1691
+ if (event.target !== edge) return;
1692
+ event.preventDefault();
1693
+ (event.shiftKey ? last : first).focus();
1694
+ }, [onCancel])
1695
+ };
1696
+ }
1697
+ /**
1698
+ * Return focus to whatever opened this panel, when it closes (spec 8.4).
1699
+ *
1700
+ * The opener is captured in a layout effect, so it must be declared *before* any
1701
+ * effect that moves focus into the panel: effects run in declaration order
1702
+ * within a component, and layout effects run before passive ones, so the capture
1703
+ * always sees the control the user actually activated.
1704
+ *
1705
+ * An opener that has since been unmounted — an action picked from a list that
1706
+ * closed with it — is skipped rather than focused, and the caller is free to
1707
+ * offer a fallback of its own.
1708
+ *
1709
+ * @param scope - the closing context's own element, for an editing context
1710
+ * nested inside a panel (the management form inside the management panel).
1711
+ * When the outer panel closes, React runs the outer cleanup first, the inner
1712
+ * cleanup next, and only then removes the outer DOM: by the time the nested
1713
+ * context returns focus, the panel has already handed it to *its* opener, and
1714
+ * the nested opener is a control about to leave the document. So a nested
1715
+ * context returns focus only while focus is still inside it (or fell to the
1716
+ * body); focus that has already left belongs to whoever moved it.
1717
+ */
1718
+ function useFocusReturn(scope) {
1719
+ const opener = (0, react.useRef)(null);
1720
+ (0, react.useLayoutEffect)(() => {
1721
+ const active = typeof document === "undefined" ? null : document.activeElement;
1722
+ opener.current = active === document.body ? null : active;
1723
+ return () => {
1724
+ const element = opener.current;
1725
+ opener.current = null;
1726
+ if (element === null || !element.isConnected) return;
1727
+ const current = document.activeElement;
1728
+ if (scope?.current != null && current !== null && current !== document.body && !scope.current.contains(current)) return;
1729
+ element.focus();
1730
+ };
1731
+ }, []);
1732
+ }
1733
+ /**
1734
+ * Move focus into a panel once, on open, addressing the target through the
1735
+ * panel rather than through a ref (spec 8.4).
1736
+ *
1737
+ * `@deepseek-ai/dsh-client-ui-primitives` publishes no `forwardRef` at all, so
1738
+ * an official `Button` or `Input` cannot carry one. Addressing the opening
1739
+ * target by marker attribute — the way the Tab boundary above already addresses
1740
+ * the focusable set — keeps that limitation in this one module instead of
1741
+ * pushing every panel back onto native controls.
1742
+ *
1743
+ * @param container - the panel whose subtree holds the target.
1744
+ * @param selector - CSS selector for the control that should open focused.
1745
+ */
1746
+ function useInitialFocusIn(container, selector) {
1747
+ (0, react.useEffect)(() => {
1748
+ container.current?.querySelector(selector)?.focus();
1749
+ }, [container, selector]);
1750
+ }
1751
+ //#endregion
1752
+ //#region src/client/session/availability.ts
1753
+ /**
1754
+ * The reason this action cannot run right now, or `undefined` when it can.
1755
+ *
1756
+ * While a send holds the Session's single flight, only the action holding it
1757
+ * explains itself as "sending"; the rest are simply unavailable while the
1758
+ * Composer is busy with it (spec 9.5).
1759
+ */
1760
+ function unavailableReasonFor(action, session) {
1761
+ if (session.unavailable === "sending") return session.activeRef !== void 0 && quickActionRefKey(session.activeRef) === quickActionRefKey(action.ref) ? "sending" : "composer-busy";
1762
+ return session.unavailable;
1763
+ }
1764
+ //#endregion
1765
+ //#region src/client/manager/ActionPanel.tsx
1766
+ /**
1767
+ * The shared searchable action panel (spec 8.1).
1768
+ *
1769
+ * One panel serves both entries that need a list rather than a row: the `bar`
1770
+ * layout's "more" overflow and the `launcher` layout's single entry. Sharing it
1771
+ * is a spec requirement, not a convenience — the two entries must not drift into
1772
+ * offering different search behaviour or different keyboard handling.
1773
+ *
1774
+ * The search itself is `./search.js`: it really filters, it keeps the matches in
1775
+ * their `actionOrder` relative order, and its field, case and Unicode policy is
1776
+ * stated and pinned there. The panel adds only the UI around it — a search field
1777
+ * with a visible text label, the same disabled-with-a-reason projection the
1778
+ * layouts use (spec 3), and the modal semantics of spec 8.4.
1779
+ *
1780
+ * Nothing here executes anything: activation is handed straight to the caller's
1781
+ * per-Session engine, which owns the single flight and the final re-verification
1782
+ * (spec 9.3, 9.5). The panel never mints a second lock.
1783
+ */
1784
+ function ActionPanel(props) {
1785
+ const { actions, session, t, labelledBy, onActivate, onClose } = props;
1786
+ const [query, setQuery] = (0, react.useState)("");
1787
+ const { panelRef, onKeyDown } = useModalKeys(onClose);
1788
+ useInitialFocusIn(panelRef, "[data-quick-actions-search]");
1789
+ const matches = filterQuickActions(actions, query);
1790
+ const emptyKey = hasQuickActionQuery(query) ? "panel.search.empty" : "empty";
1791
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1792
+ className: "dsh-cqa-backdrop",
1793
+ "data-quick-actions-backdrop": "",
1794
+ "aria-hidden": "true",
1795
+ onClick: onClose
1796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1797
+ className: "dsh-cqa-panel",
1798
+ ref: panelRef,
1799
+ role: "dialog",
1800
+ "aria-modal": "true",
1801
+ "aria-labelledby": labelledBy,
1802
+ "data-quick-actions-panel": "",
1803
+ onKeyDown,
1804
+ children: [
1805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1806
+ className: "dsh-cqa-panel-head",
1807
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1808
+ className: "dsh-cqa-panel-title",
1809
+ id: labelledBy,
1810
+ children: t("panel.title")
1811
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1812
+ type: "button",
1813
+ className: "dsh-cqa-link",
1814
+ "data-quick-actions-panel-close": "",
1815
+ onClick: onClose,
1816
+ children: t("panel.close")
1817
+ })]
1818
+ }),
1819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1820
+ className: "dsh-cqa-field",
1821
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1822
+ className: "dsh-cqa-field-label",
1823
+ children: t("panel.search")
1824
+ }), /* @__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, {}),
1826
+ type: "search",
1827
+ value: query,
1828
+ "data-quick-actions-search": "",
1829
+ onChange: (event) => {
1830
+ setQuery(event.target.value);
1831
+ }
1832
+ })]
1833
+ }),
1834
+ matches.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1835
+ className: "dsh-cqa-panel-title",
1836
+ children: t(emptyKey)
1837
+ }) : null,
1838
+ matches.map((action) => {
1839
+ const reason = unavailableReasonFor(action, session);
1840
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1841
+ type: "button",
1842
+ className: "dsh-cqa-panel-item",
1843
+ "data-quick-action": quickActionRefKey(action.ref),
1844
+ disabled: reason !== void 0,
1845
+ title: reason === void 0 ? action.text : t(`unavailable.${reason}`),
1846
+ onClick: () => {
1847
+ onActivate(action);
1848
+ onClose();
1849
+ },
1850
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionFace, {
1851
+ action,
1852
+ t
1853
+ })
1854
+ }, quickActionRefKey(action.ref));
1855
+ })
1856
+ ]
1857
+ })] });
1858
+ }
1859
+ //#endregion
1860
+ //#region src/client/session/ConfirmPanel.tsx
1861
+ function ConfirmPanel({ pending, t, onConfirm, onCancel }) {
1862
+ const { panelRef, onKeyDown } = useModalKeys(onCancel);
1863
+ useInitialFocusIn(panelRef, "[data-quick-actions-confirm-send]");
1864
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1865
+ className: "dsh-cqa-backdrop",
1866
+ "data-quick-actions-backdrop": "",
1867
+ "aria-hidden": "true",
1868
+ onClick: onCancel
1869
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1870
+ className: "dsh-cqa-panel",
1871
+ ref: panelRef,
1872
+ role: "dialog",
1873
+ "aria-modal": "true",
1874
+ "aria-label": t("confirm.title"),
1875
+ "data-quick-actions-confirm": "",
1876
+ onKeyDown,
1877
+ children: [
1878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1879
+ className: "dsh-cqa-panel-title",
1880
+ children: pending.label
1881
+ }),
1882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1883
+ className: "dsh-cqa-confirm-text",
1884
+ children: pending.text
1885
+ }),
1886
+ pending.command ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1887
+ className: "dsh-cqa-note",
1888
+ "data-quick-actions-command-notice": "",
1889
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1890
+ className: "dsh-cqa-note-text",
1891
+ children: t("confirm.command")
1892
+ })
1893
+ }) : null,
1894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1895
+ className: "dsh-cqa-confirm-actions",
1896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1897
+ variant: "toolbar",
1898
+ size: "sm",
1899
+ className: "dsh-cqa-entry",
1900
+ onClick: onCancel,
1901
+ children: t("confirm.cancel")
1902
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1903
+ variant: "toolbar",
1904
+ size: "sm",
1905
+ className: "dsh-cqa-entry",
1906
+ "data-quick-actions-confirm-send": "",
1907
+ onClick: onConfirm,
1908
+ children: t("confirm.send")
1909
+ })]
1910
+ })
1911
+ ]
1912
+ })] });
1913
+ }
1914
+ //#endregion
1915
+ //#region src/client/surfaces/QuickActionsSurface.tsx
1916
+ /**
1917
+ * The three Quick Action Layouts and the control they share (spec 8.1, 8.2).
1918
+ *
1919
+ * | layout | Slot | body |
1920
+ * |------------|-----------------------------|-------------------------------------------------------------|
1921
+ * | `ribbon` | `conversation.input.dock` | title, the actions in shared order, and "manage" on one line |
1922
+ * | `bar` | `conversation.composer.dock`| the leading actions that fit, then "more", then "manage" |
1923
+ * | `launcher` | `conversation.input.dock` | one compact entry carrying the Composer projection's count |
1924
+ *
1925
+ * Three rules hold across all of them:
1926
+ *
1927
+ * - the management entry is always rendered, so hiding or disabling every action
1928
+ * still leaves a compact, operable entry rather than an empty strip;
1929
+ * - controls keep a visible text label at every width — a narrow surface drops
1930
+ * the section title and tightens spacing, and nothing else (spec 8.2, 8.4);
1931
+ * - an action that cannot run right now is shown disabled with its reason, never
1932
+ * removed. Removal is the Hidden Quick Action projection, and that is the
1933
+ * management panel's business (spec 3).
1934
+ */
1935
+ /** Gap between adjacent controls; mirrors `--gap` in the stylesheet. */
1936
+ const CONTROL_GAP = 8;
1937
+ function ActionControl(props) {
1938
+ const { action, reason, t, onActivate } = props;
1939
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1940
+ variant: "toolbar",
1941
+ size: "sm",
1942
+ className: "dsh-cqa-action",
1943
+ "data-quick-action": quickActionRefKey(action.ref),
1944
+ disabled: reason !== void 0,
1945
+ title: reason === void 0 ? action.text : t(`unavailable.${reason}`),
1946
+ onClick: () => {
1947
+ onActivate(action);
1948
+ },
1949
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionFace, {
1950
+ action,
1951
+ t
1952
+ })
1953
+ });
1954
+ }
1955
+ /**
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.
1960
+ */
1961
+ function useElementWidth() {
1962
+ const ref = (0, react.useRef)(null);
1963
+ const [width, setWidth] = (0, react.useState)(0);
1964
+ (0, react.useEffect)(() => {
1965
+ const element = ref.current;
1966
+ if (element === null || typeof ResizeObserver === "undefined") return;
1967
+ const observer = new ResizeObserver(() => {
1968
+ setWidth(element.clientWidth);
1969
+ });
1970
+ observer.observe(element);
1971
+ setWidth(element.clientWidth);
1972
+ return () => {
1973
+ observer.disconnect();
1974
+ };
1975
+ }, []);
1976
+ return [ref, width];
1977
+ }
1978
+ function QuickActionsSurface(props) {
1979
+ const { layout, actions, session, t, onActivate, onConfirm, onCancelConfirm, onDismissFeedback, onManage } = props;
1980
+ const [rowRef, width] = useElementWidth();
1981
+ const [fitRef, fitWidth] = useElementWidth();
1982
+ const density = densityFor(width);
1983
+ const [panelOpen, setPanelOpen] = (0, react.useState)(false);
1984
+ const panelTitleId = (0, react.useId)();
1985
+ const widths = (0, react.useRef)(/* @__PURE__ */ new Map());
1986
+ const [measured, setMeasured] = (0, react.useState)(0);
1987
+ (0, react.useLayoutEffect)(() => {
1988
+ if (layout !== "bar") return;
1989
+ let changed = false;
1990
+ for (const node of Array.from(fitRef.current?.querySelectorAll("[data-quick-action]") ?? [])) {
1991
+ const key = node.dataset["quickAction"];
1992
+ const value = node.offsetWidth;
1993
+ if (key === void 0 || value <= 0 || widths.current.get(key) === value) continue;
1994
+ widths.current.set(key, value);
1995
+ changed = true;
1996
+ }
1997
+ if (changed) setMeasured((seen) => seen + 1);
1998
+ });
1999
+ const shown = (0, react.useMemo)(() => {
2000
+ if (layout !== "bar") return actions.length;
2001
+ const known = actions.map((action) => quickActionRefKey(action.ref)).map((key) => widths.current.get(key) ?? 0);
2002
+ if (known.some((value) => value <= 0)) return actions.length;
2003
+ return fitActionCount({
2004
+ available: fitWidth,
2005
+ widths: known,
2006
+ reserved: 0,
2007
+ gap: CONTROL_GAP
2008
+ });
2009
+ }, [
2010
+ layout,
2011
+ actions,
2012
+ fitWidth,
2013
+ measured
2014
+ ]);
2015
+ const overflow = layout === "bar" ? actions.slice(shown) : actions;
2016
+ /**
2017
+ * Resolve one of this surface's own controls by its data marker.
2018
+ *
2019
+ * `Button` takes no ref, so the focus contract of spec 8.4 addresses controls
2020
+ * the way the stylesheet does: through a marker attribute, queried from the
2021
+ * row this surface already measures. The selector and the attribute that
2022
+ * answers it live in this one file, so the coupling stays local.
2023
+ */
2024
+ const control = (0, react.useCallback)((marker) => {
2025
+ return rowRef.current?.querySelector(`[${marker}]`) ?? null;
2026
+ }, [rowRef]);
2027
+ const closePanel = (0, react.useCallback)(() => {
2028
+ setPanelOpen(false);
2029
+ control("data-quick-actions-entry")?.focus();
2030
+ }, [control]);
2031
+ const confirmOpener = (0, react.useRef)(null);
2032
+ const confirming = session.confirming;
2033
+ const activate = (0, react.useCallback)((action) => {
2034
+ confirmOpener.current = document.activeElement;
2035
+ onActivate(action);
2036
+ }, [onActivate]);
2037
+ (0, react.useEffect)(() => {
2038
+ if (confirming !== void 0) return;
2039
+ const opener = confirmOpener.current;
2040
+ confirmOpener.current = null;
2041
+ if (opener === null) return;
2042
+ (opener.isConnected && !opener.disabled ? opener : control("data-quick-actions-manage"))?.focus();
2043
+ }, [confirming, control]);
2044
+ (0, react.useEffect)(() => {
2045
+ if (overflow.length === 0) setPanelOpen(false);
2046
+ }, [overflow.length]);
2047
+ const manage = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2048
+ variant: "toolbar",
2049
+ size: "sm",
2050
+ className: "dsh-cqa-entry",
2051
+ "data-quick-actions-manage": "",
2052
+ title: t("manage.tooltip"),
2053
+ onClick: onManage,
2054
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2055
+ className: "dsh-cqa-label",
2056
+ children: t("manage")
2057
+ })
2058
+ });
2059
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2060
+ className: layout === "bar" ? "dsh-cqa-bar" : layout === "launcher" ? "dsh-cqa-launcher" : "dsh-cqa-ribbon",
2061
+ "data-quick-actions-layout": layout,
2062
+ "data-quick-actions-density": density,
2063
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2064
+ className: "dsh-cqa-row",
2065
+ ref: rowRef,
2066
+ "data-density": density,
2067
+ children: [
2068
+ layout === "ribbon" && density === "wide" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2069
+ className: "dsh-cqa-title",
2070
+ children: t("title")
2071
+ }) : null,
2072
+ layout === "ribbon" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2073
+ className: "dsh-cqa-scroll",
2074
+ children: [actions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2075
+ className: "dsh-cqa-title",
2076
+ children: t("empty")
2077
+ }) : null, actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionControl, {
2078
+ action,
2079
+ reason: unavailableReasonFor(action, session),
2080
+ t,
2081
+ onActivate: activate
2082
+ }, quickActionRefKey(action.ref)))]
2083
+ }) : null,
2084
+ layout === "bar" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2085
+ className: "dsh-cqa-fit",
2086
+ ref: fitRef,
2087
+ children: [actions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2088
+ className: "dsh-cqa-title",
2089
+ children: t("empty")
2090
+ }) : null, actions.slice(0, shown).map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionControl, {
2091
+ action,
2092
+ reason: unavailableReasonFor(action, session),
2093
+ t,
2094
+ onActivate: activate
2095
+ }, quickActionRefKey(action.ref)))]
2096
+ }) : null,
2097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2098
+ className: "dsh-cqa-trailing",
2099
+ children: [layout === "launcher" || layout === "bar" && overflow.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2100
+ className: "dsh-cqa-anchor",
2101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2102
+ variant: "toolbar",
2103
+ size: "sm",
2104
+ className: "dsh-cqa-entry",
2105
+ "data-quick-actions-entry": layout,
2106
+ "aria-expanded": panelOpen,
2107
+ "aria-haspopup": "dialog",
2108
+ disabled: layout === "launcher" && actions.length === 0,
2109
+ onClick: () => {
2110
+ setPanelOpen((open) => !open);
2111
+ },
2112
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2113
+ className: "dsh-cqa-label",
2114
+ children: layout === "launcher" ? t("launcher", { count: actions.length }) : t("more", { count: overflow.length })
2115
+ })
2116
+ }), panelOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPanel, {
2117
+ actions: overflow,
2118
+ session,
2119
+ t,
2120
+ labelledBy: panelTitleId,
2121
+ onActivate: activate,
2122
+ onClose: closePanel
2123
+ }) : null]
2124
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2125
+ className: "dsh-cqa-anchor",
2126
+ children: [manage, session.confirming === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmPanel, {
2127
+ pending: session.confirming,
2128
+ t,
2129
+ onConfirm,
2130
+ onCancel: onCancelConfirm
2131
+ })]
2132
+ })]
2133
+ })
2134
+ ]
2135
+ }), session.feedback === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2136
+ className: "dsh-cqa-note",
2137
+ role: "status",
2138
+ "data-quick-actions-feedback": session.feedback.kind,
2139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2140
+ className: "dsh-cqa-note-text",
2141
+ children: session.feedback.kind === "failed" ? t("feedback.failed", { message: session.feedback.message }) : t(`feedback.${session.feedback.kind}`)
2142
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2143
+ type: "button",
2144
+ className: "dsh-cqa-link",
2145
+ onClick: onDismissFeedback,
2146
+ children: t("feedback.dismiss")
2147
+ })]
2148
+ })]
2149
+ });
2150
+ }
2151
+ //#endregion
2152
+ //#region src/client/manager/press.ts
2153
+ /**
2154
+ * Gate one control. `blocked` is its own extra condition — an end of the list,
2155
+ * the ceiling, the choice already made; the gate's own `busy` is folded in here
2156
+ * so no caller has to remember it.
2157
+ */
2158
+ function pressProps(gate, blocked, onPress) {
2159
+ const refused = gate.readOnly || gate.busy || blocked;
2160
+ return {
2161
+ disabled: gate.readOnly,
2162
+ "aria-disabled": gate.busy || blocked,
2163
+ onClick: () => {
2164
+ if (refused) return;
2165
+ onPress();
2166
+ }
2167
+ };
2168
+ }
2169
+ //#endregion
2170
+ //#region src/locales/index.ts
2171
+ /** The locale namespace this package registers and every Slot entry binds. */
2172
+ const QUICK_ACTIONS_LOCALE_NAMESPACE = "composer-quick-actions";
2173
+ const zh = {
2174
+ "title": "快捷动作",
2175
+ "manage": "管理",
2176
+ "manage.tooltip": "管理快捷动作",
2177
+ "more": "更多 {count}",
2178
+ "launcher": "快捷动作 {count}",
2179
+ "empty": "暂无可用快捷动作",
2180
+ "command.badge": "命令",
2181
+ "unavailable.occupied-draft": "草稿中已有内容;发送动作不会覆盖或携带它",
2182
+ "unavailable.composer-busy": "消息编辑器正在提交,请稍候",
2183
+ "unavailable.composer-blocked": "当前会话的消息编辑器已被占用",
2184
+ "unavailable.session-removed": "会话已被移除",
2185
+ "unavailable.parent-offline": "父会话不可用",
2186
+ "unavailable.sending": "正在发送",
2187
+ "feedback.state-changed": "状态已变化,请重试",
2188
+ "feedback.retained": "未发送,文本已保留",
2189
+ "feedback.failed": "未能执行:{message}",
2190
+ "feedback.dismiss": "知道了",
2191
+ "catalog.loading": "正在读取快捷动作…",
2192
+ "catalog.unreadable": "无法读取设置,快捷动作暂不可用",
2193
+ "catalog.unavailable": "未找到预置目录",
2194
+ "catalog.undecodable": "预置目录版本过新,无法读取",
2195
+ "catalog.retry": "重试",
2196
+ "crash.title": "快捷动作出错了",
2197
+ "crash.retry": "重新加载",
2198
+ "confirm.title": "确认发送",
2199
+ "confirm.send": "发送",
2200
+ "confirm.cancel": "取消",
2201
+ "confirm.command": "这条文本会按命令进入 DSH 官方裁决路径。此处不会出现输入 / 时的原生候选菜单,你看到的就是最终提交内容。",
2202
+ "panel.title": "选择快捷动作",
2203
+ "panel.close": "关闭",
2204
+ "panel.search": "搜索快捷动作",
2205
+ "panel.search.empty": "没有匹配的快捷动作",
2206
+ "manager.title": "管理快捷动作",
2207
+ "manager.close": "关闭",
2208
+ "manager.layout": "布局",
2209
+ "manager.layout.ribbon": "上方动作带",
2210
+ "manager.layout.bar": "下方操作栏",
2211
+ "manager.layout.launcher": "单入口面板",
2212
+ "manager.actions": "快捷动作",
2213
+ "manager.count": "共 {total} / {limit} 项",
2214
+ "manager.new": "新建快捷动作",
2215
+ "manager.empty": "还没有任何快捷动作",
2216
+ "manager.overflow": "动作总数为 {total} 项,已超过 {limit} 项上限。现有动作全部保留,但在恢复到上限以内之前无法新增或克隆。",
2217
+ "manager.limit": "已达 {limit} 项上限,无法新增或克隆。",
2218
+ "manager.command.notice": "标有「命令」的动作以 / 开头,会按命令进入 DSH 官方裁决路径。确认面板只展示最终提交的文本,不会出现原生 / 候选菜单;关闭确认后,命令将一键提交且没有任何预览。",
2219
+ "manager.preset": "预置",
2220
+ "manager.custom": "自定义",
2221
+ "manager.clonedFrom": "克隆自预置",
2222
+ "manager.hidden": "已隐藏",
2223
+ "manager.disabled": "已停用",
2224
+ "manager.hide": "隐藏",
2225
+ "manager.restore": "恢复",
2226
+ "manager.clone": "克隆",
2227
+ "manager.edit": "编辑",
2228
+ "manager.enable": "启用",
2229
+ "manager.disable": "停用",
2230
+ "manager.delete": "删除",
2231
+ "manager.delete.confirm": "确认删除",
2232
+ "manager.delete.cancel": "不删除",
2233
+ "manager.moveUp": "上移",
2234
+ "manager.moveDown": "下移",
2235
+ "manager.readonly.storage": "设置存储当前不可用,管理界面为只读。",
2236
+ "manager.readonly.offline": "连接已断开,管理界面为只读;连接恢复后可继续修改。",
2237
+ "form.title.new": "新建快捷动作",
2238
+ "form.title.edit": "编辑快捷动作",
2239
+ "form.label": "标签",
2240
+ "form.label.hint": "按钮上显示的名称,最长 {max} 个字符",
2241
+ "form.text": "发送文本",
2242
+ "form.text.hint": "原样提交的静态文本,保留换行,最长 {max} 个字符",
2243
+ "form.icon": "图标(可选)",
2244
+ "form.icon.hint": "1–{max} 个 emoji,仅作装饰",
2245
+ "form.confirm": "发送前确认",
2246
+ "form.save": "保存",
2247
+ "form.cancel": "取消",
2248
+ "form.command.warning": "这条文本以 / 开头,是命令发送动作:它会按命令进入 DSH 官方裁决路径;确认面板不会展示原生 / 候选菜单;关闭确认后,该命令将一键提交且没有任何预览。确认开关仍由你自行设置。",
2249
+ "issue.invalid": "这个字段不符合要求",
2250
+ "issue.label.blank": "请填写标签",
2251
+ "issue.label.too-long": "标签超出长度上限",
2252
+ "issue.text.blank": "发送文本至少要有一个非空白字符",
2253
+ "issue.text.too-long": "发送文本超出长度上限",
2254
+ "issue.text.reserved-placeholder": "发送文本包含 DSH 保留的引用占位符,无法作为静态文本提交",
2255
+ "issue.icon.not-emoji": "图标只能由 emoji 组成",
2256
+ "issue.icon.too-long": "图标的 emoji 数量超出上限",
2257
+ "write.dismiss": "知道了",
2258
+ "write.retry": "重试",
2259
+ "write.not-ready": "设置尚未就绪,改动没有保存。",
2260
+ "write.read-only": "当前无法写入设置,改动没有保存。",
2261
+ "write.refused": "保存被拒绝,没有写入任何内容;请重试。",
2262
+ "write.conflict": "设置已在别处被修改,已刷新到最新状态;请核对后重新确认这次修改。",
2263
+ "write.failed": "保存失败:{message};请重试。",
2264
+ "write.invalid-fields": "有字段不符合要求,改动没有保存。",
2265
+ "write.limit-reached": "已达动作数量上限,无法新增或克隆。",
2266
+ "write.unknown-action": "这个动作已经不存在了,请核对当前列表后重试。",
2267
+ "write.id-in-use": "生成的标识发生重复,请重试。",
2268
+ "write.invalid-order": "顺序已经变化,请核对当前列表后重试。"
2269
+ };
2270
+ /** The dictionaries as `ctx.locale.register` takes them. */
2271
+ const quickActionsDictionaries = {
2272
+ zh,
2273
+ en: {
2274
+ "title": "Quick Actions",
2275
+ "manage": "Manage",
2276
+ "manage.tooltip": "Manage Quick Actions",
2277
+ "more": "More {count}",
2278
+ "launcher": "Quick Actions {count}",
2279
+ "empty": "No Quick Actions available",
2280
+ "command.badge": "Command",
2281
+ "unavailable.occupied-draft": "The draft already has content; a send action never overwrites or carries it",
2282
+ "unavailable.composer-busy": "The composer is submitting; try again in a moment",
2283
+ "unavailable.composer-blocked": "Another feature owns this session’s composer",
2284
+ "unavailable.session-removed": "This session was removed",
2285
+ "unavailable.parent-offline": "The parent session is unavailable",
2286
+ "unavailable.sending": "Sending",
2287
+ "feedback.state-changed": "Something changed — try again",
2288
+ "feedback.retained": "Not sent; the text was kept in the draft",
2289
+ "feedback.failed": "Could not run: {message}",
2290
+ "feedback.dismiss": "Dismiss",
2291
+ "catalog.loading": "Loading Quick Actions…",
2292
+ "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",
2295
+ "catalog.retry": "Retry",
2296
+ "crash.title": "Quick Actions hit an error",
2297
+ "crash.retry": "Reload",
2298
+ "confirm.title": "Confirm send",
2299
+ "confirm.send": "Send",
2300
+ "confirm.cancel": "Cancel",
2301
+ "confirm.command": "This text enters DSH’s own command adjudication path. The native “/” suggestion menu does not appear here, so what you see is exactly what is submitted.",
2302
+ "panel.title": "Pick a Quick Action",
2303
+ "panel.close": "Close",
2304
+ "panel.search": "Search Quick Actions",
2305
+ "panel.search.empty": "No Quick Action matches that",
2306
+ "manager.title": "Manage Quick Actions",
2307
+ "manager.close": "Close",
2308
+ "manager.layout": "Layout",
2309
+ "manager.layout.ribbon": "Action ribbon",
2310
+ "manager.layout.bar": "Action bar",
2311
+ "manager.layout.launcher": "Single launcher",
2312
+ "manager.actions": "Quick Actions",
2313
+ "manager.count": "{total} of {limit}",
2314
+ "manager.new": "New Quick Action",
2315
+ "manager.empty": "No Quick Actions yet",
2316
+ "manager.overflow": "There are {total} actions, over the limit of {limit}. Everything you have is kept, but creating and cloning stay disabled until the total is back within the limit.",
2317
+ "manager.limit": "The limit of {limit} actions is reached, so creating and cloning are disabled.",
2318
+ "manager.command.notice": "Actions marked “Command” start with “/” and enter DSH’s own command adjudication path. The confirmation panel shows only the text that will be submitted — the native “/” suggestion menu does not appear — and with confirmation off the command is submitted in one click with no preview.",
2319
+ "manager.preset": "Preset",
2320
+ "manager.custom": "Custom",
2321
+ "manager.clonedFrom": "Cloned from a preset",
2322
+ "manager.hidden": "Hidden",
2323
+ "manager.disabled": "Disabled",
2324
+ "manager.hide": "Hide",
2325
+ "manager.restore": "Restore",
2326
+ "manager.clone": "Clone",
2327
+ "manager.edit": "Edit",
2328
+ "manager.enable": "Enable",
2329
+ "manager.disable": "Disable",
2330
+ "manager.delete": "Delete",
2331
+ "manager.delete.confirm": "Confirm delete",
2332
+ "manager.delete.cancel": "Keep it",
2333
+ "manager.moveUp": "Move up",
2334
+ "manager.moveDown": "Move down",
2335
+ "manager.readonly.storage": "Settings storage is unavailable right now, so management is read-only.",
2336
+ "manager.readonly.offline": "The connection is down, so management is read-only until it is back.",
2337
+ "form.title.new": "New Quick Action",
2338
+ "form.title.edit": "Edit Quick Action",
2339
+ "form.label": "Label",
2340
+ "form.label.hint": "The name on the button, up to {max} characters",
2341
+ "form.text": "Text to send",
2342
+ "form.text.hint": "Static text submitted as-is, line breaks kept, up to {max} characters",
2343
+ "form.icon": "Icon (optional)",
2344
+ "form.icon.hint": "1–{max} emoji, decoration only",
2345
+ "form.confirm": "Confirm before sending",
2346
+ "form.save": "Save",
2347
+ "form.cancel": "Cancel",
2348
+ "form.command.warning": "This text starts with “/”, which makes it a Command Send Action: it enters DSH’s own command adjudication path, the confirmation panel shows no native “/” suggestion menu, and with confirmation off the command is submitted in one click with no preview at all. The confirmation switch stays yours to set.",
2349
+ "issue.invalid": "This field is not acceptable",
2350
+ "issue.label.blank": "Enter a label",
2351
+ "issue.label.too-long": "The label is over the length limit",
2352
+ "issue.text.blank": "The text needs at least one non-whitespace character",
2353
+ "issue.text.too-long": "The text is over the length limit",
2354
+ "issue.text.reserved-placeholder": "The text holds a DSH-reserved reference placeholder and cannot be submitted as static text",
2355
+ "issue.icon.not-emoji": "An icon may only be made of emoji",
2356
+ "issue.icon.too-long": "The icon has too many emoji",
2357
+ "write.dismiss": "Dismiss",
2358
+ "write.retry": "Retry",
2359
+ "write.not-ready": "Settings are not ready yet, so nothing was saved.",
2360
+ "write.read-only": "Settings cannot be written right now, so nothing was saved.",
2361
+ "write.refused": "The write was refused and nothing was saved; try again.",
2362
+ "write.conflict": "Settings changed elsewhere and have been refreshed; check what is on screen and confirm your change again.",
2363
+ "write.failed": "The write failed: {message}. Try again.",
2364
+ "write.invalid-fields": "Some fields are not acceptable, so nothing was saved.",
2365
+ "write.limit-reached": "The action limit is reached, so creating and cloning are disabled.",
2366
+ "write.unknown-action": "That action no longer exists; check the current list and try again.",
2367
+ "write.id-in-use": "The generated id collided; try again.",
2368
+ "write.invalid-order": "The order has changed; check the current list and try again."
2369
+ }
2370
+ };
2371
+ /**
2372
+ * Every key this package ships, taken from the dictionary rather than restated,
2373
+ * so a composed key is checked against what actually exists.
2374
+ */
2375
+ const SHIPPED_KEYS = new Set(Object.keys(zh));
2376
+ /**
2377
+ * Narrow a composed key to one this package ships, falling back when it does
2378
+ * not. Composed keys come from names the model and the controller own — a field
2379
+ * issue, a mutation refusal, a write failure — so a case added upstream shows a
2380
+ * general sentence instead of leaking `write.something-new` onto a surface.
2381
+ */
2382
+ function quickActionsLocaleKey(candidate, fallback) {
2383
+ return SHIPPED_KEYS.has(candidate) ? candidate : fallback;
2384
+ }
2385
+ /** The dictionary entry naming one field issue (spec 4.3). */
2386
+ function quickActionIssueKey(issue) {
2387
+ return quickActionsLocaleKey(`issue.${issue.field}.${issue.reason}`, "issue.invalid");
2388
+ }
2389
+ //#endregion
2390
+ //#region src/client/manager/ActionForm.tsx
2391
+ /**
2392
+ * The Custom Quick Action form (spec 8.3).
2393
+ *
2394
+ * It edits exactly four things — label, static text, optional emoji and the send
2395
+ * confirmation — and validates them through the shared model, so a draft this
2396
+ * form accepts is a draft the Host will accept (spec 4.3).
2397
+ *
2398
+ * ## What this form deliberately does not have
2399
+ *
2400
+ * - **An action-type selector.** `kind` is not a configuration field: the first
2401
+ * release is always `'send'`, and no user path may change it (spec 4.1, 16.1).
2402
+ * - **Any control the Command Send Action warning locks.** When the text becomes
2403
+ * a command the form warns and nothing else (spec 8.3, 16.1): the warning
2404
+ * states that the text enters DSH's own adjudication path, that the
2405
+ * confirmation panel shows no native `/` suggestion menu, and that with
2406
+ * confirmation off the command is submitted in one click with no preview. The
2407
+ * confirmation switch stays editable throughout.
2408
+ * - **Any rewrite of `confirm` in response to the text.** The default is applied
2409
+ * once, when a draft is created or cloned (`newQuickActionDraft`, the clone
2410
+ * planner). Deriving it from the text here would silently undo the user's own
2411
+ * choice every time they edited the text — the same reason spec 4.3 forbids
2412
+ * normalization from touching it.
2413
+ *
2414
+ * The draft lives in the caller's state, which is what makes "a failed write
2415
+ * keeps the form content" (spec 10) structural rather than incidental.
2416
+ *
2417
+ * ## The form as a nested editing context
2418
+ *
2419
+ * The management panel's Escape handler relies on this form stopping Escape
2420
+ * before it arrives — but a handler on the form only sees keys typed *inside*
2421
+ * the form. Until the form took focus of its own, the first Escape after "new"
2422
+ * or "edit" was still aimed at the button that opened it, outside the form, and
2423
+ * closed the whole panel (ticket 25). So the form has the two focus rules every
2424
+ * other panel of spec 8.4 has: it opens with the caret in its first field, and
2425
+ * it hands focus back to the control that opened it when it closes — which is
2426
+ * what keeps a second Escape meaning "close the panel".
2427
+ */
2428
+ /**
2429
+ * The issues to render for one draft.
2430
+ *
2431
+ * Every rule comes from the shared model; the only presentation decision is that
2432
+ * a blank field stays quiet until the user has tried to save it.
2433
+ */
2434
+ function visibleQuickActionIssues(draft, attempted) {
2435
+ const validated = validateQuickActionDraft(draft);
2436
+ if (validated.ok) return [];
2437
+ return attempted ? validated.issues : validated.issues.filter((issue) => issue.reason !== "blank");
2438
+ }
2439
+ /** One labelled field, with its hint and — when it has one — its issue. */
2440
+ function Field(props) {
2441
+ const { field, label, hint, issue, t, children } = props;
2442
+ const id = (0, react.useId)();
2443
+ const hintId = `${id}-hint`;
2444
+ const errorId = `${id}-error`;
2445
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2446
+ className: "dsh-cqa-field",
2447
+ children: [
2448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2449
+ className: "dsh-cqa-field-label",
2450
+ htmlFor: id,
2451
+ children: label
2452
+ }),
2453
+ children({
2454
+ id,
2455
+ describedBy: issue === void 0 ? hintId : `${hintId} ${errorId}`,
2456
+ invalid: issue !== void 0
2457
+ }),
2458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2459
+ className: "dsh-cqa-field-hint",
2460
+ id: hintId,
2461
+ children: hint
2462
+ }),
2463
+ issue === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2464
+ className: "dsh-cqa-field-error",
2465
+ id: errorId,
2466
+ "data-quick-actions-issue": field,
2467
+ children: t(quickActionIssueKey(issue))
2468
+ })
2469
+ ]
2470
+ });
2471
+ }
2472
+ function ActionForm(props) {
2473
+ const { mode, draft, attempted, gate, t, onChange, onSave, onCancel } = props;
2474
+ const issues = visibleQuickActionIssues(draft, attempted);
2475
+ const issueFor = (field) => issues.find((issue) => issue.field === field);
2476
+ const command = isCommandSendActionText(draft.text);
2477
+ const titleId = (0, react.useId)();
2478
+ const formRef = (0, react.useRef)(null);
2479
+ useFocusReturn(formRef);
2480
+ useInitialFocusIn(formRef, "[data-quick-actions-form-label]");
2481
+ /**
2482
+ * Escape leaves the form, not the panel behind it.
2483
+ *
2484
+ * Without stopping it here, Escape typed in a field would reach the management
2485
+ * panel's own handler and close the whole panel — a far larger action than the
2486
+ * user asked for. The innermost editing context is what Escape means.
2487
+ */
2488
+ const onKeyDown = (event) => {
2489
+ if (event.key !== "Escape") return;
2490
+ event.stopPropagation();
2491
+ onCancel();
2492
+ };
2493
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2494
+ className: "dsh-cqa-form",
2495
+ ref: formRef,
2496
+ role: "group",
2497
+ "aria-labelledby": titleId,
2498
+ "data-quick-actions-form": mode,
2499
+ onKeyDown,
2500
+ children: [
2501
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2502
+ className: "dsh-cqa-section-title",
2503
+ id: titleId,
2504
+ children: t(mode === "new" ? "form.title.new" : "form.title.edit")
2505
+ }),
2506
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2507
+ field: "label",
2508
+ label: t("form.label"),
2509
+ hint: t("form.label.hint", { max: 40 }),
2510
+ issue: issueFor("label"),
2511
+ t,
2512
+ children: ({ id, describedBy, invalid }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2513
+ className: "dsh-cqa-input",
2514
+ id,
2515
+ "aria-describedby": describedBy,
2516
+ "aria-invalid": invalid,
2517
+ disabled: gate.readOnly,
2518
+ "data-quick-actions-form-label": "",
2519
+ value: draft.label,
2520
+ onChange: (event) => {
2521
+ onChange({
2522
+ ...draft,
2523
+ label: event.target.value
2524
+ });
2525
+ }
2526
+ })
2527
+ }),
2528
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2529
+ field: "text",
2530
+ label: t("form.text"),
2531
+ hint: t("form.text.hint", { max: QUICK_ACTION_TEXT_MAX_CODE_POINTS }),
2532
+ issue: issueFor("text"),
2533
+ t,
2534
+ children: ({ id, describedBy, invalid }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2535
+ className: "dsh-cqa-textarea",
2536
+ id,
2537
+ rows: 4,
2538
+ "aria-describedby": describedBy,
2539
+ "aria-invalid": invalid,
2540
+ disabled: gate.readOnly,
2541
+ value: draft.text,
2542
+ onChange: (event) => {
2543
+ onChange({
2544
+ ...draft,
2545
+ text: event.target.value
2546
+ });
2547
+ }
2548
+ })
2549
+ }),
2550
+ command ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2551
+ className: "dsh-cqa-note",
2552
+ role: "status",
2553
+ "data-quick-actions-command-warning": "",
2554
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2555
+ className: "dsh-cqa-note-text",
2556
+ children: t("form.command.warning")
2557
+ })
2558
+ }) : null,
2559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2560
+ field: "icon",
2561
+ label: t("form.icon"),
2562
+ hint: t("form.icon.hint", { max: 4 }),
2563
+ issue: issueFor("icon"),
2564
+ t,
2565
+ children: ({ id, describedBy, invalid }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2566
+ className: "dsh-cqa-input",
2567
+ id,
2568
+ "aria-describedby": describedBy,
2569
+ "aria-invalid": invalid,
2570
+ disabled: gate.readOnly,
2571
+ value: draft.icon,
2572
+ onChange: (event) => {
2573
+ onChange({
2574
+ ...draft,
2575
+ icon: event.target.value
2576
+ });
2577
+ }
2578
+ })
2579
+ }),
2580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2581
+ className: "dsh-cqa-switch",
2582
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2583
+ type: "checkbox",
2584
+ disabled: gate.readOnly,
2585
+ checked: draft.confirm,
2586
+ "data-quick-actions-confirm-switch": "",
2587
+ onChange: (event) => {
2588
+ onChange({
2589
+ ...draft,
2590
+ confirm: event.target.checked
2591
+ });
2592
+ }
2593
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2594
+ className: "dsh-cqa-field-label",
2595
+ children: t("form.confirm")
2596
+ })]
2597
+ }),
2598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2599
+ className: "dsh-cqa-form-actions",
2600
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2601
+ variant: "toolbar",
2602
+ size: "sm",
2603
+ className: "dsh-cqa-entry",
2604
+ onClick: onCancel,
2605
+ children: t("form.cancel")
2606
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2607
+ variant: "toolbar",
2608
+ size: "sm",
2609
+ className: "dsh-cqa-entry",
2610
+ ...pressProps(gate, false, onSave),
2611
+ children: t("form.save")
2612
+ })]
2613
+ })
2614
+ ]
2615
+ });
2616
+ }
2617
+ //#endregion
2618
+ //#region src/client/manager/ManagedRow.tsx
2619
+ function ManagedRow({ action, index, total, gate, deleting, t, on }) {
2620
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2621
+ className: "dsh-cqa-list-item",
2622
+ "data-quick-action": quickActionRefKey(action.ref),
2623
+ "data-quick-action-hidden": action.hidden ? "" : void 0,
2624
+ children: [
2625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2626
+ className: "dsh-cqa-list-head",
2627
+ children: [
2628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionFace, {
2629
+ action,
2630
+ t
2631
+ }),
2632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
2633
+ className: "dsh-cqa-tag",
2634
+ children: t(action.editable ? "manager.custom" : "manager.preset")
2635
+ }),
2636
+ action.hidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
2637
+ active: true,
2638
+ className: "dsh-cqa-tag",
2639
+ children: t(action.editable ? "manager.disabled" : "manager.hidden")
2640
+ }) : null,
2641
+ action.clonedFromPresetId === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
2642
+ className: "dsh-cqa-tag",
2643
+ children: t("manager.clonedFrom")
2644
+ })
2645
+ ]
2646
+ }),
2647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2648
+ className: "dsh-cqa-list-text",
2649
+ children: action.text
2650
+ }),
2651
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2652
+ className: "dsh-cqa-list-controls",
2653
+ children: [
2654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2655
+ variant: "toolbar",
2656
+ size: "sm",
2657
+ className: "dsh-cqa-entry",
2658
+ "data-quick-actions-move": "up",
2659
+ ...pressProps(gate, index === 0, () => {
2660
+ on.onMove(action.ref, index - 1);
2661
+ }),
2662
+ children: t("manager.moveUp")
2663
+ }),
2664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2665
+ variant: "toolbar",
2666
+ size: "sm",
2667
+ className: "dsh-cqa-entry",
2668
+ "data-quick-actions-move": "down",
2669
+ ...pressProps(gate, index === total - 1, () => {
2670
+ on.onMove(action.ref, index + 1);
2671
+ }),
2672
+ children: t("manager.moveDown")
2673
+ }),
2674
+ action.editable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2675
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2676
+ variant: "toolbar",
2677
+ size: "sm",
2678
+ className: "dsh-cqa-entry",
2679
+ ...pressProps(gate, false, on.onEdit),
2680
+ children: t("manager.edit")
2681
+ }),
2682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2683
+ variant: "toolbar",
2684
+ size: "sm",
2685
+ className: "dsh-cqa-entry",
2686
+ ...pressProps(gate, false, on.onToggleEnabled),
2687
+ children: t(action.hidden ? "manager.enable" : "manager.disable")
2688
+ }),
2689
+ deleting ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2690
+ variant: "toolbar",
2691
+ size: "sm",
2692
+ className: "dsh-cqa-entry",
2693
+ "data-quick-actions-delete": "confirm",
2694
+ ...pressProps(gate, false, on.onConfirmDelete),
2695
+ children: t("manager.delete.confirm")
2696
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2697
+ variant: "toolbar",
2698
+ size: "sm",
2699
+ className: "dsh-cqa-entry",
2700
+ onClick: on.onCancelDelete,
2701
+ children: t("manager.delete.cancel")
2702
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2703
+ variant: "toolbar",
2704
+ size: "sm",
2705
+ className: "dsh-cqa-entry",
2706
+ "data-quick-actions-delete": "ask",
2707
+ ...pressProps(gate, false, on.onAskDelete),
2708
+ children: t("manager.delete")
2709
+ })
2710
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2711
+ variant: "toolbar",
2712
+ size: "sm",
2713
+ className: "dsh-cqa-entry",
2714
+ ...pressProps(gate, false, on.onToggleHidden),
2715
+ children: t(action.hidden ? "manager.restore" : "manager.hide")
2716
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2717
+ variant: "toolbar",
2718
+ size: "sm",
2719
+ className: "dsh-cqa-entry",
2720
+ "data-quick-actions-clone": "",
2721
+ ...pressProps(gate, !gate.canAdd, on.onClone),
2722
+ children: t("manager.clone")
2723
+ })] })
2724
+ ]
2725
+ })
2726
+ ]
2727
+ });
2728
+ }
2729
+ //#endregion
2730
+ //#region src/client/manager/status.ts
2731
+ /**
2732
+ * How the management panel names the state it is in (spec 10).
2733
+ *
2734
+ * Both readings are pure functions over the snapshot the controller publishes,
2735
+ * kept beside the panel rather than inside it — the same split the surfaces use
2736
+ * for `layout.ts` and the action panel for `search.ts`.
2737
+ */
2738
+ /**
2739
+ * Report read-only state, and only read-only state.
2740
+ *
2741
+ * Spec 10 keeps two first-read failures apart: a catalog that could not be read
2742
+ * shows a retryable catalog error, while a *readable* catalog whose user
2743
+ * namespace failed shows "storage unavailable". Without the projection guard
2744
+ * below, a loading or failed catalog also reads as read-only — the write gate
2745
+ * refuses on `not-ready` — and the panel would print both notices at once,
2746
+ * telling the user their storage is broken when it is the catalog that is
2747
+ * missing. With no catalog there is nothing to be read-only *about*: the catalog
2748
+ * notice is the whole story.
2749
+ */
2750
+ function managerReadOnlyReason(client) {
2751
+ if (client.projection === void 0) return void 0;
2752
+ if (!client.readOnly) return void 0;
2753
+ return client.stale ? "offline" : "storage";
2754
+ }
2755
+ /** One write failure as a sentence, with the recovery it implies (spec 10). */
2756
+ function managerFailureMessage(failure, t) {
2757
+ if (failure.kind === "failed") return t("write.failed", { message: failure.message });
2758
+ return t(quickActionsLocaleKey(failure.kind === "rejected" ? `write.${failure.rejection.reason}` : `write.${failure.kind}`, "write.refused"));
2759
+ }
2760
+ //#endregion
2761
+ //#region src/client/manager/ManagerPanel.tsx
2762
+ /**
2763
+ * The centralized management panel (spec 8.3).
2764
+ *
2765
+ * Everything a user can change about Quick Actions lives here and nowhere else:
2766
+ * the shared order, a preset's hidden state and its clone, a custom action's
2767
+ * whole life cycle, and the global layout. The Composer surfaces only read the
2768
+ * projection and execute.
2769
+ *
2770
+ * ## Where the truth is
2771
+ *
2772
+ * The panel renders the authoritative snapshot the controller publishes and
2773
+ * never a local copy of it. So a new UI state appears only once the Host has
2774
+ * persisted it (spec 10) — including the order, which is why a failed reorder
2775
+ * needs no "restore the old order" path: the old order was never left.
2776
+ *
2777
+ * ## Failure, and the one retry
2778
+ *
2779
+ * Every write goes through one funnel, which also remembers how to run it again.
2780
+ * That is spec 10's 「提供明确重试」: the failure banner offers the retry, and
2781
+ * pressing it *re-plans* rather than replaying — the controller builds each plan
2782
+ * from the snapshot it holds at that moment, and a create mints a fresh Custom
2783
+ * Action ID per attempt. Nothing ever retries on its own; a silent replay could
2784
+ * submit a plan the user has already moved past.
2785
+ *
2786
+ * Each failure is named, because spec 10 asks for different handling per kind: a
2787
+ * refusal or a transport failure keeps the form content and the panel open,
2788
+ * while a revision conflict means the controller has already re-read the
2789
+ * authoritative state, so the notice asks the user to confirm the change again
2790
+ * against what is now on screen — this feature keeps no second offline truth.
2791
+ *
2792
+ * Nothing here reports success: the list is the feedback, and spec 9.5 forbids an
2793
+ * extra success toast.
2794
+ */
2795
+ /** The form's starting draft for an existing Custom Quick Action. */
2796
+ function draftOf(action) {
2797
+ return {
2798
+ label: action.label,
2799
+ text: action.text,
2800
+ icon: action.icon ?? "",
2801
+ confirm: action.confirm
2802
+ };
2803
+ }
2804
+ function ManagerPanel({ client, controller, t }) {
2805
+ const close = (0, react.useCallback)(() => {
2806
+ controller.closeManager();
2807
+ }, [controller]);
2808
+ useFocusReturn();
2809
+ const { panelRef, onKeyDown } = useModalKeys(close);
2810
+ useInitialFocusIn(panelRef, "[data-quick-actions-manager-close]");
2811
+ const titleId = (0, react.useId)();
2812
+ const layoutTitleId = (0, react.useId)();
2813
+ const listTitleId = (0, react.useId)();
2814
+ const [form, setForm] = (0, react.useState)(void 0);
2815
+ const [pendingDelete, setPendingDelete] = (0, react.useState)(void 0);
2816
+ const [pending, setPending] = (0, react.useState)(void 0);
2817
+ const projection = client.projection;
2818
+ const managed = projection?.managed ?? [];
2819
+ const counts = projection?.counts;
2820
+ const readOnlyReason = managerReadOnlyReason(client);
2821
+ const gate = {
2822
+ readOnly: readOnlyReason !== void 0,
2823
+ busy: client.writing,
2824
+ canAdd: counts?.canAdd === true
2825
+ };
2826
+ const editingId = form?.target.kind === "edit" ? form.target.id : void 0;
2827
+ const editingLives = editingId === void 0 || managed.some((action) => action.editable && action.ref.source === "custom" && action.ref.id === editingId);
2828
+ (0, react.useEffect)(() => {
2829
+ if (!editingLives) setForm(void 0);
2830
+ }, [editingLives]);
2831
+ /**
2832
+ * The one funnel every write goes through: remember it, run it, and answer
2833
+ * whether it landed.
2834
+ *
2835
+ * The `catch` is the belt. A controller write answers with an outcome rather
2836
+ * than rejecting, so this arm should be unreachable — but an unhandled
2837
+ * rejection must never reach the page from a click handler, and the failure
2838
+ * the user sees comes from the snapshot the controller publishes either way.
2839
+ */
2840
+ const write = (0, react.useCallback)(async (remember, run) => {
2841
+ setPending(remember);
2842
+ try {
2843
+ const outcome = await run();
2844
+ if (outcome.ok) setPending(void 0);
2845
+ return outcome.ok;
2846
+ } catch {
2847
+ return false;
2848
+ }
2849
+ }, []);
2850
+ /** Start a write from a click handler, with no outcome to react to. */
2851
+ const fire = (0, react.useCallback)((run) => {
2852
+ write({
2853
+ kind: "write",
2854
+ run
2855
+ }, run);
2856
+ }, [write]);
2857
+ const save = (0, react.useCallback)(async () => {
2858
+ if (form === void 0) return;
2859
+ if (!validateQuickActionDraft(form.draft).ok) {
2860
+ setForm({
2861
+ ...form,
2862
+ attempted: true
2863
+ });
2864
+ return;
2865
+ }
2866
+ const { target, draft } = form;
2867
+ if (await write({ kind: "form" }, () => target.kind === "new" ? controller.createCustomAction(draft) : controller.updateCustomAction(target.id, draft))) setForm(void 0);
2868
+ else setForm({
2869
+ ...form,
2870
+ attempted: true
2871
+ });
2872
+ }, [
2873
+ controller,
2874
+ form,
2875
+ write
2876
+ ]);
2877
+ const retry = (0, react.useCallback)(() => {
2878
+ if (pending === void 0) return;
2879
+ if (pending.kind === "form") {
2880
+ save();
2881
+ return;
2882
+ }
2883
+ write(pending, pending.run);
2884
+ }, [
2885
+ pending,
2886
+ save,
2887
+ write
2888
+ ]);
2889
+ const remove = (0, react.useCallback)((id) => {
2890
+ const run = () => controller.deleteCustomAction(id);
2891
+ write({
2892
+ kind: "write",
2893
+ run
2894
+ }, run).then((landed) => {
2895
+ if (landed) setPendingDelete(void 0);
2896
+ });
2897
+ }, [controller, write]);
2898
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2899
+ className: "dsh-cqa-backdrop dsh-cqa-manager-backdrop",
2900
+ "data-quick-actions-backdrop": "",
2901
+ "aria-hidden": "true",
2902
+ onClick: close
2903
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2904
+ className: "dsh-cqa-manager",
2905
+ ref: panelRef,
2906
+ role: "dialog",
2907
+ "aria-modal": "true",
2908
+ "aria-labelledby": titleId,
2909
+ "data-quick-actions-manager": "",
2910
+ onKeyDown,
2911
+ children: [
2912
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2913
+ className: "dsh-cqa-manager-head",
2914
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2915
+ className: "dsh-cqa-manager-title",
2916
+ id: titleId,
2917
+ children: t("manager.title")
2918
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2919
+ variant: "toolbar",
2920
+ size: "sm",
2921
+ className: "dsh-cqa-entry",
2922
+ "data-quick-actions-manager-close": "",
2923
+ onClick: close,
2924
+ children: t("manager.close")
2925
+ })]
2926
+ }),
2927
+ projection === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2928
+ className: "dsh-cqa-note",
2929
+ role: "status",
2930
+ "data-quick-actions-catalog-error": client.catalog.status === "error" ? client.catalog.reason : "loading",
2931
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2932
+ className: "dsh-cqa-note-text",
2933
+ children: t(client.catalog.status === "error" ? `catalog.${client.catalog.reason}` : "catalog.loading")
2934
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2935
+ type: "button",
2936
+ className: "dsh-cqa-link",
2937
+ onClick: () => {
2938
+ controller.refresh().catch(() => void 0);
2939
+ },
2940
+ children: t("catalog.retry")
2941
+ })]
2942
+ }) : null,
2943
+ readOnlyReason === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2944
+ className: "dsh-cqa-note",
2945
+ role: "status",
2946
+ "data-quick-actions-readonly": readOnlyReason,
2947
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2948
+ className: "dsh-cqa-note-text",
2949
+ children: t(`manager.readonly.${readOnlyReason}`)
2950
+ })
2951
+ }),
2952
+ client.failure === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2953
+ className: "dsh-cqa-note",
2954
+ role: "alert",
2955
+ "data-quick-actions-write-failure": client.failure.kind,
2956
+ children: [
2957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2958
+ className: "dsh-cqa-note-text",
2959
+ children: managerFailureMessage(client.failure, t)
2960
+ }),
2961
+ pending === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2962
+ type: "button",
2963
+ className: "dsh-cqa-link",
2964
+ "data-quick-actions-write-retry": "",
2965
+ onClick: retry,
2966
+ children: t("write.retry")
2967
+ }),
2968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2969
+ type: "button",
2970
+ className: "dsh-cqa-link",
2971
+ onClick: () => {
2972
+ controller.dismissFailure();
2973
+ },
2974
+ children: t("write.dismiss")
2975
+ })
2976
+ ]
2977
+ }),
2978
+ counts === void 0 || !counts.overflow && counts.canAdd ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2979
+ className: "dsh-cqa-note",
2980
+ role: "status",
2981
+ "data-quick-actions-limit": counts.overflow ? "overflow" : "reached",
2982
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2983
+ className: "dsh-cqa-note-text",
2984
+ children: t(counts.overflow ? "manager.overflow" : "manager.limit", {
2985
+ limit: counts.limit,
2986
+ total: counts.total
2987
+ })
2988
+ })
2989
+ }),
2990
+ projection === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2992
+ className: "dsh-cqa-section",
2993
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2994
+ className: "dsh-cqa-section-title",
2995
+ id: layoutTitleId,
2996
+ children: t("manager.layout")
2997
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2998
+ className: "dsh-cqa-group",
2999
+ role: "group",
3000
+ "aria-labelledby": layoutTitleId,
3001
+ children: QUICK_ACTION_LAYOUTS.map((layout) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3002
+ variant: "toolbar",
3003
+ size: "sm",
3004
+ className: "dsh-cqa-entry",
3005
+ "data-quick-actions-layout-choice": layout,
3006
+ "aria-pressed": projection.layout === layout,
3007
+ ...pressProps(gate, projection.layout === layout, () => {
3008
+ fire(() => controller.setLayout(layout));
3009
+ }),
3010
+ children: t(`manager.layout.${layout}`)
3011
+ }, layout))
3012
+ })]
3013
+ }),
3014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3015
+ className: "dsh-cqa-section",
3016
+ children: [
3017
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3018
+ className: "dsh-cqa-section-head",
3019
+ children: [
3020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3021
+ className: "dsh-cqa-section-title",
3022
+ id: listTitleId,
3023
+ children: t("manager.actions")
3024
+ }),
3025
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3026
+ className: "dsh-cqa-field-hint",
3027
+ "data-quick-actions-count": "",
3028
+ children: t("manager.count", {
3029
+ total: counts?.total ?? 0,
3030
+ limit: counts?.limit ?? 0
3031
+ })
3032
+ }),
3033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3034
+ variant: "toolbar",
3035
+ size: "sm",
3036
+ className: "dsh-cqa-entry",
3037
+ "data-quick-actions-new": "",
3038
+ ...pressProps(gate, !gate.canAdd || form?.target.kind === "new", () => {
3039
+ setForm({
3040
+ target: { kind: "new" },
3041
+ draft: newQuickActionDraft(),
3042
+ attempted: false
3043
+ });
3044
+ }),
3045
+ children: t("manager.new")
3046
+ })
3047
+ ]
3048
+ }),
3049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3050
+ className: "dsh-cqa-note",
3051
+ "data-quick-actions-command-notice": "",
3052
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3053
+ className: "dsh-cqa-note-text",
3054
+ children: t("manager.command.notice")
3055
+ })
3056
+ }),
3057
+ managed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3058
+ className: "dsh-cqa-field-hint",
3059
+ children: t("manager.empty")
3060
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3061
+ className: "dsh-cqa-list",
3062
+ "aria-labelledby": listTitleId,
3063
+ children: managed.map((action, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagedRow, {
3064
+ action,
3065
+ index,
3066
+ total: managed.length,
3067
+ gate,
3068
+ deleting: action.ref.source === "custom" && pendingDelete === action.ref.id,
3069
+ t,
3070
+ on: {
3071
+ onMove: (ref, toIndex) => {
3072
+ fire(() => controller.moveAction(ref, toIndex));
3073
+ },
3074
+ onEdit: () => {
3075
+ setForm({
3076
+ target: {
3077
+ kind: "edit",
3078
+ id: action.ref.id
3079
+ },
3080
+ draft: draftOf(action),
3081
+ attempted: false
3082
+ });
3083
+ },
3084
+ onClone: () => {
3085
+ fire(() => controller.clonePreset(action.ref.id));
3086
+ },
3087
+ onToggleHidden: () => {
3088
+ fire(() => controller.setPresetHidden(action.ref.id, !action.hidden));
3089
+ },
3090
+ onToggleEnabled: () => {
3091
+ fire(() => controller.setCustomActionEnabled(action.ref.id, action.hidden));
3092
+ },
3093
+ onAskDelete: () => {
3094
+ setPendingDelete(action.ref.id);
3095
+ },
3096
+ onCancelDelete: () => {
3097
+ setPendingDelete(void 0);
3098
+ },
3099
+ onConfirmDelete: () => {
3100
+ remove(action.ref.id);
3101
+ }
3102
+ }
3103
+ }, quickActionRefKey(action.ref)))
3104
+ })
3105
+ ]
3106
+ }),
3107
+ form === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionForm, {
3108
+ mode: form.target.kind,
3109
+ draft: form.draft,
3110
+ attempted: form.attempted,
3111
+ gate,
3112
+ t,
3113
+ onChange: (draft) => {
3114
+ setForm({
3115
+ ...form,
3116
+ draft
3117
+ });
3118
+ },
3119
+ onSave: () => {
3120
+ save();
3121
+ },
3122
+ onCancel: () => {
3123
+ setForm(void 0);
3124
+ }
3125
+ }, editingId === void 0 ? "new" : `edit:${editingId}`)
3126
+ ] })
3127
+ ]
3128
+ })] }), document.body);
3129
+ }
3130
+ //#endregion
3131
+ //#region src/client/surfaces/entries.tsx
3132
+ /**
3133
+ * The two dock Slot entries (spec 7.3, 8.1).
3134
+ *
3135
+ * Both are always registered; which one renders the layout is decided at render
3136
+ * time from the authoritative Settings snapshot, and both render nothing at all
3137
+ * unless the Resident Composer predicate holds for their Session:
3138
+ *
3139
+ * - `conversation.composer.dock` is the residency beacon. Its mount is DSH's own
3140
+ * statement that this Session's composer is in its resident variant, so it
3141
+ * marks the Session while mounted and additionally renders the `bar` layout.
3142
+ * - `conversation.input.dock` renders `ribbon` or `launcher`, but only while the
3143
+ * beacon's mark stands — that Slot also mounts on the blank-session hero, and
3144
+ * the first release must never appear there.
3145
+ *
3146
+ * A third entry rides the same input dock: the centralized management overlay,
3147
+ * which spec 8.1 requires to be registered independently of the layout entries.
3148
+ * It is a separate Slot cell with its own id, order and error boundary, so a
3149
+ * failure in the management panel cannot take a Composer layout down with it and
3150
+ * vice versa (spec 7.3). Because the overlay is global state rendered from a
3151
+ * session-scoped Slot, only the Session the residency registry elects primary
3152
+ * draws it — two Resident Composers on screen must not stack two overlays.
3153
+ *
3154
+ * Each entry carries its own error boundary, so a failure replaces the Quick
3155
+ * Action area alone.
3156
+ */
3157
+ function useControllerState(controller) {
3158
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => controller.subscribe(listener), [controller]), () => controller.getSnapshot());
3159
+ }
3160
+ function useResident(residency, sessionId) {
3161
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => residency.subscribe(listener), [residency]), () => residency.isResident(sessionId));
3162
+ }
3163
+ /** Whether this Session is the one that draws anything global (see the registry). */
3164
+ function usePrimaryResident(residency, sessionId) {
3165
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => residency.subscribe(listener), [residency]), () => residency.primarySessionId() === sessionId);
3166
+ }
3167
+ /**
3168
+ * The Composer block for one Session, resolved on every read rather than cached.
3169
+ *
3170
+ * `conversation` is reached through `ctx.get` rather than `inject`, so nothing
3171
+ * orders it before this component's first render. Caching the resolution would
3172
+ * turn "the service was not there yet" into "this Session can never be blocked",
3173
+ * and a Quick Action would then send into a Composer another feature has made
3174
+ * inert. `storeFor` answers an identity-stable store, so re-resolving costs a
3175
+ * map lookup and keeps `getSnapshot` referentially stable.
3176
+ */
3177
+ function useComposerBlock(blocks, sessionId) {
3178
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => blocks()?.storeFor(sessionId)?.subscribe(listener) ?? (() => {}), [blocks, sessionId]), () => blocks()?.storeFor(sessionId)?.getSnapshot());
3179
+ }
3180
+ /**
3181
+ * The body both entries render once residency and the layout agree.
3182
+ *
3183
+ * It owns the Session's execution engine for as long as it is mounted: every
3184
+ * commit republishes the live `InputActions` and the live Composer projection
3185
+ * into it, and unmounting cancels whatever has not reached the official state
3186
+ * machine (spec 7.2).
3187
+ */
3188
+ function SessionSurface(props) {
3189
+ const { deps, layout, actions, sessionId, useInput, useSession, inputActions, t } = props;
3190
+ const engine = (0, react.useMemo)(() => deps.sessions.engineFor(sessionId), [deps.sessions, sessionId]);
3191
+ const input = useInput((state) => state);
3192
+ const session = useSession((state) => state);
3193
+ const block = useComposerBlock(deps.blocks, sessionId);
3194
+ (0, react.useLayoutEffect)(() => {
3195
+ engine.bind(inputActions, actions);
3196
+ engine.observe(input, session, block);
3197
+ });
3198
+ (0, react.useEffect)(() => () => {
3199
+ engine.cancelPending();
3200
+ }, [engine]);
3201
+ const state = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => engine.subscribe(listener), [engine]), () => engine.getSnapshot());
3202
+ const onActivate = (0, react.useCallback)((action) => {
3203
+ engine.activate(action);
3204
+ }, [engine]);
3205
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickActionsSurface, {
3206
+ layout,
3207
+ actions,
3208
+ session: state,
3209
+ t,
3210
+ onActivate,
3211
+ onConfirm: () => {
3212
+ engine.confirm();
3213
+ },
3214
+ onCancelConfirm: () => {
3215
+ engine.cancel();
3216
+ },
3217
+ onDismissFeedback: () => {
3218
+ engine.dismissFeedback();
3219
+ },
3220
+ onManage: () => {
3221
+ deps.controller.openManager();
3222
+ }
3223
+ });
3224
+ }
3225
+ /** The catalog error and loading states of spec 10, rendered where the layout would be. */
3226
+ function CatalogNotice(props) {
3227
+ const { client, t, onRetry } = props;
3228
+ if (client.catalog.status === "loading") return null;
3229
+ if (client.catalog.status === "ready") return null;
3230
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3231
+ className: "dsh-cqa-note",
3232
+ role: "status",
3233
+ "data-quick-actions-catalog-error": client.catalog.reason,
3234
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3235
+ className: "dsh-cqa-note-text",
3236
+ children: t(`catalog.${client.catalog.reason}`)
3237
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3238
+ type: "button",
3239
+ className: "dsh-cqa-link",
3240
+ onClick: onRetry,
3241
+ children: t("catalog.retry")
3242
+ })]
3243
+ });
3244
+ }
3245
+ /** Build both Slot entry components over one set of fiber-owned dependencies. */
3246
+ function createQuickActionDockEntries(deps) {
3247
+ function Body(props) {
3248
+ const { owns, ...slot } = props;
3249
+ const client = useControllerState(deps.controller);
3250
+ const projection = client.projection;
3251
+ const onRetry = (0, react.useCallback)(() => {
3252
+ deps.controller.refresh().catch(() => void 0);
3253
+ }, []);
3254
+ if (projection === void 0) return owns.includes("ribbon") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogNotice, {
3255
+ client,
3256
+ t: slot.t,
3257
+ onRetry
3258
+ }) : null;
3259
+ if (!owns.includes(projection.layout)) return null;
3260
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionSurface, {
3261
+ ...slot,
3262
+ deps,
3263
+ layout: projection.layout,
3264
+ actions: projection.composer
3265
+ });
3266
+ }
3267
+ function InputDock(props) {
3268
+ const resident = useResident(deps.residency, props.sessionId);
3269
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SurfaceErrorBoundary, {
3270
+ t: props.t,
3271
+ children: resident ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Body, {
3272
+ ...props,
3273
+ owns: INPUT_DOCK_LAYOUTS
3274
+ }) : null
3275
+ });
3276
+ }
3277
+ function ComposerDock(props) {
3278
+ const { sessionId } = props;
3279
+ (0, react.useLayoutEffect)(() => deps.residency.mark(sessionId), [sessionId]);
3280
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SurfaceErrorBoundary, {
3281
+ t: props.t,
3282
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Body, {
3283
+ ...props,
3284
+ owns: COMPOSER_DOCK_LAYOUTS
3285
+ })
3286
+ });
3287
+ }
3288
+ /**
3289
+ * The centralized management overlay (spec 8.1, 8.3).
3290
+ *
3291
+ * It renders only where a Resident Composer's own "manage" entry could have
3292
+ * opened it, and only for the primary Session, so the global panel stays
3293
+ * single however many composers are on screen.
3294
+ */
3295
+ function ManagerDock(props) {
3296
+ const primary = usePrimaryResident(deps.residency, props.sessionId);
3297
+ const client = useControllerState(deps.controller);
3298
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SurfaceErrorBoundary, {
3299
+ t: props.t,
3300
+ children: primary && client.manager.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagerPanel, {
3301
+ client,
3302
+ controller: deps.controller,
3303
+ t: props.t
3304
+ }) : null
3305
+ });
3306
+ }
3307
+ return {
3308
+ InputDock,
3309
+ ComposerDock,
3310
+ ManagerDock
3311
+ };
3312
+ }
3313
+ /** `ribbon` and `launcher` render above the composer card (spec 8.1). */
3314
+ const INPUT_DOCK_LAYOUTS = ["ribbon", "launcher"];
3315
+ /** `bar` renders below it. */
3316
+ const COMPOSER_DOCK_LAYOUTS = ["bar"];
3317
+ //#endregion
3318
+ //#region src/styles/index.ts
3319
+ /**
3320
+ * The Composer Quick Action surfaces' stylesheet (spec 8.2, 8.4).
3321
+ *
3322
+ * It is shipped as one string installed into a `<style data-plugin-css>` tag,
3323
+ * the same shape and idempotence the first-party DSH client bundles use, rather
3324
+ * than as CSS Modules: the Client build adapter produces one browser CJS file
3325
+ * with no CSS pipeline (spec 11.2), and adding one would change a contract the
3326
+ * bundle's own tests pin. Class names carry the `dsh-cqa-` prefix instead of a
3327
+ * generated hash, which gives the same collision safety by convention.
3328
+ *
3329
+ * Colours are DSH alias theme tokens only — nothing here defines a palette, and
3330
+ * nothing overrides a global theme (spec 8.4).
3331
+ *
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.
3346
+ */
3347
+ const TAG_ID = "dsh-quick-actions/surfaces.css";
3348
+ const QUICK_ACTIONS_CSS = `
3349
+ .dsh-cqa-ribbon,
3350
+ .dsh-cqa-launcher {
3351
+ box-sizing: border-box;
3352
+ width: calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance));
3353
+ max-width: var(--dsh-composer-card-max-width);
3354
+ margin: 0 auto;
3355
+ }
3356
+ .dsh-cqa-bar {
3357
+ box-sizing: border-box;
3358
+ width: 100%;
3359
+ max-width: var(--dsh-composer-card-max-width);
3360
+ margin: 0 auto;
3361
+ }
3362
+
3363
+ .dsh-cqa-row {
3364
+ display: flex;
3365
+ align-items: center;
3366
+ gap: 8px;
3367
+ min-width: 0;
3368
+ padding: 2px 0;
3369
+ }
3370
+ .dsh-cqa-row[data-density='narrow'] {
3371
+ gap: 6px;
3372
+ }
3373
+
3374
+ .dsh-cqa-title {
3375
+ flex: none;
3376
+ color: var(--dsw-alias-label-tertiary);
3377
+ font-size: 12px;
3378
+ font-weight: 500;
3379
+ line-height: 18px;
3380
+ white-space: nowrap;
3381
+ }
3382
+
3383
+ /* The ribbon overflows horizontally; the management entry never scrolls away. */
3384
+ .dsh-cqa-scroll {
3385
+ display: flex;
3386
+ align-items: center;
3387
+ gap: 8px;
3388
+ flex: 1 1 auto;
3389
+ min-width: 0;
3390
+ overflow-x: auto;
3391
+ scrollbar-width: thin;
3392
+ }
3393
+ .dsh-cqa-fit {
3394
+ display: flex;
3395
+ align-items: center;
3396
+ gap: 8px;
3397
+ flex: 1 1 auto;
3398
+ min-width: 0;
3399
+ overflow: hidden;
3400
+ }
3401
+ .dsh-cqa-trailing {
3402
+ display: flex;
3403
+ align-items: center;
3404
+ gap: 8px;
3405
+ flex: none;
3406
+ margin-left: auto;
3407
+ }
3408
+
3409
+ /*
3410
+ * The capsule itself — height, radius, padding, colours, hover and ":disabled"
3411
+ * — belongs to the official "Button". What is left here is what a shared
3412
+ * control cannot know (how it behaves inside these rows) plus what the
3413
+ * primitive does not supply. Nothing below may restate a Button property,
3414
+ * because the plugin class is applied after the primitive's and would win on
3415
+ * equal specificity.
3416
+ *
3417
+ * The focus ring is one of the things it does not supply: "Button.module.css"
3418
+ * and "Pill.module.css" carry no ":focus" rule at all — the library leaves the
3419
+ * ring to each component that wants one ("Input.module.css" has
3420
+ * ":focus-within", "HoverCard" and "JsonTree" their own). Spec 8.4 makes a
3421
+ * clear focus ring a hard gate, so it stays here for every control this plugin
3422
+ * renders.
3423
+ */
3424
+ .dsh-cqa-action,
3425
+ .dsh-cqa-entry {
3426
+ /* No global border-box reset exists in this stylesheet, and Button pads
3427
+ itself, so the cap has to count that padding to mean 240px on screen. */
3428
+ box-sizing: border-box;
3429
+ flex: none;
3430
+ /* Long labels truncate rather than pushing the row's other controls out. */
3431
+ max-width: 240px;
3432
+ }
3433
+ .dsh-cqa-action:focus-visible,
3434
+ .dsh-cqa-entry:focus-visible {
3435
+ outline: 2px solid var(--dsw-alias-state-business-primary);
3436
+ outline-offset: 1px;
3437
+ }
3438
+ /* "aria-disabled" is not ":disabled": a control kept focusable while a write is
3439
+ in flight still has to read as inert, and no primitive expresses that. */
3440
+ .dsh-cqa-entry[aria-disabled='true'] {
3441
+ opacity: 0.5;
3442
+ cursor: default;
3443
+ }
3444
+ /* The searchable panel's rows stay native — a full-width, left-aligned list row
3445
+ is not a capsule — so they still need a focus ring of their own. */
3446
+ .dsh-cqa-panel-item:focus-visible,
3447
+ .dsh-cqa-link:focus-visible {
3448
+ outline: 2px solid var(--dsw-alias-state-business-primary);
3449
+ outline-offset: 1px;
3450
+ }
3451
+ .dsh-cqa-label {
3452
+ min-width: 0;
3453
+ overflow: hidden;
3454
+ text-overflow: ellipsis;
3455
+ white-space: nowrap;
3456
+ }
3457
+ .dsh-cqa-icon {
3458
+ flex: none;
3459
+ }
3460
+ /* The chip look is "Pill"'s; only its behaviour in a flex row is ours. */
3461
+ .dsh-cqa-badge,
3462
+ .dsh-cqa-tag {
3463
+ flex: none;
3464
+ }
3465
+
3466
+ .dsh-cqa-note {
3467
+ box-sizing: border-box;
3468
+ width: 100%;
3469
+ margin: 4px 0 0;
3470
+ padding: 4px 8px;
3471
+ border-radius: 8px;
3472
+ background: var(--dsw-alias-interactive-bg-hover);
3473
+ color: var(--dsw-alias-label-secondary);
3474
+ font-size: 12px;
3475
+ line-height: 18px;
3476
+ display: flex;
3477
+ align-items: center;
3478
+ gap: 8px;
3479
+ }
3480
+ .dsh-cqa-note-text {
3481
+ flex: 1 1 auto;
3482
+ min-width: 0;
3483
+ }
3484
+ .dsh-cqa-link {
3485
+ flex: none;
3486
+ border: 0;
3487
+ background: none;
3488
+ padding: 0;
3489
+ color: var(--dsw-alias-state-business-primary);
3490
+ font: inherit;
3491
+ font-size: 12px;
3492
+ cursor: pointer;
3493
+ text-decoration: underline;
3494
+ }
3495
+
3496
+ /* Anchored surfaces: the popover list and the confirmation panel. */
3497
+ .dsh-cqa-anchor {
3498
+ position: relative;
3499
+ }
3500
+ .dsh-cqa-backdrop {
3501
+ position: fixed;
3502
+ z-index: 19;
3503
+ inset: 0;
3504
+ }
3505
+ .dsh-cqa-panel {
3506
+ position: absolute;
3507
+ z-index: 20;
3508
+ bottom: calc(100% + 6px);
3509
+ left: 0;
3510
+ box-sizing: border-box;
3511
+ max-width: min(100%, 360px);
3512
+ min-width: 220px;
3513
+ max-height: 260px;
3514
+ overflow-y: auto;
3515
+ padding: 6px;
3516
+ border: 0.5px solid var(--dsw-alias-border-l1);
3517
+ border-radius: 12px;
3518
+ background: var(--dsw-specific-tip);
3519
+ box-shadow: var(--dsw-elevation-soft);
3520
+ }
3521
+ .dsh-cqa-panel-head {
3522
+ display: flex;
3523
+ align-items: center;
3524
+ gap: 8px;
3525
+ }
3526
+ .dsh-cqa-panel-head .dsh-cqa-panel-title {
3527
+ flex: 1 1 auto;
3528
+ min-width: 0;
3529
+ }
3530
+ .dsh-cqa-panel-title {
3531
+ padding: 4px 8px;
3532
+ color: var(--dsw-alias-label-tertiary);
3533
+ font-size: 12px;
3534
+ line-height: 18px;
3535
+ }
3536
+ .dsh-cqa-panel-item {
3537
+ display: flex;
3538
+ align-items: center;
3539
+ gap: 6px;
3540
+ width: 100%;
3541
+ padding: 6px 8px;
3542
+ border: 0;
3543
+ border-radius: 8px;
3544
+ background: none;
3545
+ color: var(--dsw-alias-label-primary);
3546
+ font: inherit;
3547
+ font-size: 13px;
3548
+ line-height: 20px;
3549
+ text-align: left;
3550
+ cursor: pointer;
3551
+ }
3552
+ .dsh-cqa-panel-item:hover:not(:disabled) {
3553
+ background: var(--dsw-alias-interactive-bg-hover);
3554
+ }
3555
+ .dsh-cqa-panel-item:disabled {
3556
+ opacity: 0.5;
3557
+ cursor: default;
3558
+ }
3559
+
3560
+ .dsh-cqa-confirm-text {
3561
+ margin: 6px 0;
3562
+ padding: 8px;
3563
+ border-radius: 8px;
3564
+ background: var(--dsw-alias-interactive-bg-hover);
3565
+ color: var(--dsw-alias-label-primary);
3566
+ font-family: var(--ds-font-family-code);
3567
+ font-size: 12px;
3568
+ line-height: 18px;
3569
+ white-space: pre-wrap;
3570
+ word-break: break-word;
3571
+ max-height: 160px;
3572
+ overflow-y: auto;
3573
+ }
3574
+ .dsh-cqa-confirm-actions {
3575
+ display: flex;
3576
+ justify-content: flex-end;
3577
+ gap: 8px;
3578
+ }
3579
+
3580
+ /* ------------------------------------------------------------------------- */
3581
+ /* Fields, shared by the search box and the Custom Quick Action form */
3582
+ /* ------------------------------------------------------------------------- */
3583
+
3584
+ .dsh-cqa-field {
3585
+ display: flex;
3586
+ flex-direction: column;
3587
+ gap: 2px;
3588
+ padding: 4px 0;
3589
+ }
3590
+ .dsh-cqa-field-label {
3591
+ color: var(--dsw-alias-label-secondary);
3592
+ font-size: 12px;
3593
+ line-height: 18px;
3594
+ }
3595
+ .dsh-cqa-field-hint {
3596
+ color: var(--dsw-alias-label-tertiary);
3597
+ font-size: 11px;
3598
+ line-height: 16px;
3599
+ }
3600
+ .dsh-cqa-field-error {
3601
+ color: var(--dsw-alias-state-danger-primary, var(--dsw-alias-label-primary));
3602
+ font-size: 11px;
3603
+ line-height: 16px;
3604
+ font-weight: 500;
3605
+ }
3606
+
3607
+ .dsh-cqa-input,
3608
+ .dsh-cqa-textarea {
3609
+ box-sizing: border-box;
3610
+ width: 100%;
3611
+ padding: 4px 8px;
3612
+ border: 0.5px solid var(--dsw-alias-border-l1);
3613
+ border-radius: 8px;
3614
+ background: transparent;
3615
+ color: var(--dsw-alias-label-primary);
3616
+ font: inherit;
3617
+ font-size: 13px;
3618
+ line-height: 20px;
3619
+ }
3620
+ .dsh-cqa-textarea {
3621
+ resize: vertical;
3622
+ min-height: 72px;
3623
+ font-family: var(--ds-font-family-code);
3624
+ white-space: pre-wrap;
3625
+ }
3626
+ .dsh-cqa-input:focus-visible,
3627
+ .dsh-cqa-textarea:focus-visible {
3628
+ outline: 2px solid var(--dsw-alias-state-business-primary);
3629
+ outline-offset: 1px;
3630
+ }
3631
+ .dsh-cqa-input:disabled,
3632
+ .dsh-cqa-textarea:disabled {
3633
+ opacity: 0.5;
3634
+ }
3635
+
3636
+ .dsh-cqa-switch {
3637
+ display: flex;
3638
+ align-items: center;
3639
+ gap: 6px;
3640
+ padding: 4px 0;
3641
+ cursor: pointer;
3642
+ }
3643
+
3644
+ /* ------------------------------------------------------------------------- */
3645
+ /* The centralized management overlay */
3646
+ /* ------------------------------------------------------------------------- */
3647
+
3648
+ /*
3649
+ * The overlay and its click-catching backdrop. Neither paints: only DSH alias
3650
+ * theme tokens may be used here, and this release has no token to spend on a
3651
+ * modal scrim (spec 8.4).
3652
+ *
3653
+ * These two numbers rank the overlay against its own backdrop, and nothing
3654
+ * else. Since ticket 26 the overlay is portaled to the document body, so it and
3655
+ * the anchored popovers (19 and 20, still rendered in the input dock) sit in
3656
+ * different stacking contexts: which of them paints on top follows from where
3657
+ * the dock's own ancestors land in the root context, not from comparing 31
3658
+ * against 20. Raising these would not change that.
3659
+ */
3660
+ .dsh-cqa-manager-backdrop {
3661
+ z-index: 30;
3662
+ }
3663
+ .dsh-cqa-manager {
3664
+ position: fixed;
3665
+ z-index: 31;
3666
+ top: 50%;
3667
+ left: 50%;
3668
+ transform: translate(-50%, -50%);
3669
+ box-sizing: border-box;
3670
+ display: flex;
3671
+ flex-direction: column;
3672
+ gap: 6px;
3673
+ width: min(560px, calc(100vw - 24px));
3674
+ max-height: min(80vh, 680px);
3675
+ overflow-y: auto;
3676
+ padding: 12px;
3677
+ border: 0.5px solid var(--dsw-alias-border-l1);
3678
+ border-radius: 12px;
3679
+ background: var(--dsw-specific-tip);
3680
+ color: var(--dsw-alias-label-primary);
3681
+ box-shadow: var(--dsw-elevation-soft);
3682
+ }
3683
+
3684
+ .dsh-cqa-manager-head {
3685
+ display: flex;
3686
+ align-items: center;
3687
+ gap: 8px;
3688
+ }
3689
+ .dsh-cqa-manager-title {
3690
+ flex: 1 1 auto;
3691
+ min-width: 0;
3692
+ margin: 0;
3693
+ color: var(--dsw-alias-label-primary);
3694
+ font-size: 14px;
3695
+ font-weight: 600;
3696
+ line-height: 20px;
3697
+ }
3698
+
3699
+ .dsh-cqa-section {
3700
+ display: flex;
3701
+ flex-direction: column;
3702
+ gap: 6px;
3703
+ padding: 6px 0;
3704
+ border-top: 0.5px solid var(--dsw-alias-border-l1);
3705
+ }
3706
+ .dsh-cqa-section-head {
3707
+ display: flex;
3708
+ align-items: center;
3709
+ gap: 8px;
3710
+ flex-wrap: wrap;
3711
+ }
3712
+ .dsh-cqa-section-head .dsh-cqa-section-title {
3713
+ flex: 1 1 auto;
3714
+ }
3715
+ .dsh-cqa-section-title {
3716
+ color: var(--dsw-alias-label-secondary);
3717
+ font-size: 12px;
3718
+ font-weight: 500;
3719
+ line-height: 18px;
3720
+ }
3721
+ .dsh-cqa-group {
3722
+ display: flex;
3723
+ align-items: center;
3724
+ gap: 6px;
3725
+ flex-wrap: wrap;
3726
+ }
3727
+ .dsh-cqa-entry[aria-pressed='true'] {
3728
+ background: var(--dsw-alias-state-business-tertiary);
3729
+ color: var(--dsw-alias-label-primary-bluish);
3730
+ opacity: 1;
3731
+ }
3732
+
3733
+ .dsh-cqa-list {
3734
+ display: flex;
3735
+ flex-direction: column;
3736
+ gap: 6px;
3737
+ margin: 0;
3738
+ padding: 0;
3739
+ list-style: none;
3740
+ }
3741
+ .dsh-cqa-list-item {
3742
+ display: flex;
3743
+ flex-direction: column;
3744
+ gap: 4px;
3745
+ padding: 6px 8px;
3746
+ border-radius: 8px;
3747
+ background: var(--dsw-alias-interactive-bg-hover);
3748
+ }
3749
+ /* A hidden or disabled action stays legible: it is managed here, not removed. */
3750
+ .dsh-cqa-list-item[data-quick-action-hidden] .dsh-cqa-list-head,
3751
+ .dsh-cqa-list-item[data-quick-action-hidden] .dsh-cqa-list-text {
3752
+ opacity: 0.6;
3753
+ }
3754
+ .dsh-cqa-list-head {
3755
+ display: flex;
3756
+ align-items: center;
3757
+ gap: 6px;
3758
+ flex-wrap: wrap;
3759
+ min-width: 0;
3760
+ font-size: 13px;
3761
+ line-height: 20px;
3762
+ }
3763
+ .dsh-cqa-list-text {
3764
+ min-width: 0;
3765
+ overflow: hidden;
3766
+ color: var(--dsw-alias-label-tertiary);
3767
+ font-family: var(--ds-font-family-code);
3768
+ font-size: 11px;
3769
+ line-height: 16px;
3770
+ white-space: nowrap;
3771
+ text-overflow: ellipsis;
3772
+ }
3773
+ .dsh-cqa-list-controls {
3774
+ display: flex;
3775
+ align-items: center;
3776
+ gap: 4px;
3777
+ flex-wrap: wrap;
3778
+ }
3779
+
3780
+ .dsh-cqa-form {
3781
+ display: flex;
3782
+ flex-direction: column;
3783
+ gap: 2px;
3784
+ padding: 6px 0 0;
3785
+ border-top: 0.5px solid var(--dsw-alias-border-l1);
3786
+ }
3787
+ .dsh-cqa-form-actions {
3788
+ display: flex;
3789
+ justify-content: flex-end;
3790
+ gap: 8px;
3791
+ padding-top: 6px;
3792
+ }
3793
+ `;
3794
+ /**
3795
+ * Install the stylesheet once per document and return its disposer, so the
3796
+ * Client fiber owns it like every other registration (spec 7.3).
3797
+ *
3798
+ * The holder count lives on the tag rather than in this module, because a hot
3799
+ * reload runs two bundle instances at once: the new fiber installs before the
3800
+ * old one unloads, and each has its own module scope. Counting in the DOM is
3801
+ * what keeps the surviving instance's styles from being removed underneath it.
3802
+ */
3803
+ function installQuickActionStyles() {
3804
+ if (typeof document === "undefined") return () => {};
3805
+ const selector = `style[data-plugin-css=${JSON.stringify(TAG_ID)}]`;
3806
+ const existing = document.querySelector(selector);
3807
+ const tag = existing ?? document.createElement("style");
3808
+ if (existing === null) {
3809
+ tag.dataset["plugin"] = "dsh-quick-actions";
3810
+ tag.dataset["pluginCss"] = TAG_ID;
3811
+ tag.textContent = QUICK_ACTIONS_CSS;
3812
+ document.head.appendChild(tag);
3813
+ }
3814
+ tag.dataset["pluginCssHolders"] = String(holdersOf(tag) + 1);
3815
+ let released = false;
3816
+ return () => {
3817
+ if (released) return;
3818
+ released = true;
3819
+ const left = holdersOf(tag) - 1;
3820
+ if (left > 0) {
3821
+ tag.dataset["pluginCssHolders"] = String(left);
3822
+ return;
3823
+ }
3824
+ tag.remove();
3825
+ };
3826
+ }
3827
+ function holdersOf(tag) {
3828
+ const held = Number(tag.dataset["pluginCssHolders"]);
3829
+ return Number.isFinite(held) && held > 0 ? held : 0;
3830
+ }
3831
+ //#endregion
3832
+ //#region src/client/index.tsx
3833
+ const name = "composer-quick-actions";
3834
+ /**
3835
+ * The services this Client requires (spec 7.3). `conversation` is deliberately
3836
+ * absent: its Composer-block registry is read through `ctx.get`, the documented
3837
+ * read "without the inject requirement", because the service is guaranteed
3838
+ * present wherever the two dock Slots it declares are rendered.
3839
+ */
3840
+ const inject = [
3841
+ "slots",
3842
+ "settingsScope",
3843
+ "connection",
3844
+ "locale"
3845
+ ];
3846
+ /** Ascending position among the shipped dock entries; late enough to sit last. */
3847
+ const DOCK_ORDER = 100;
3848
+ /** The management overlay's own cell, right after the layout's (spec 8.1). */
3849
+ const MANAGER_ORDER = 101;
3850
+ /**
3851
+ * Start the feature and tie every piece of it to this fiber.
3852
+ *
3853
+ * Nothing is returned and no internal type is re-exported: the controller, the
3854
+ * execution engine and the surfaces are internal to this package (spec 14).
3855
+ */
3856
+ function apply(ctx) {
3857
+ const controller = createQuickActionsController({
3858
+ settingsScope: ctx.settingsScope,
3859
+ connection: ctx.connection
3860
+ });
3861
+ const sessions = createQuickActionSessionRegistry();
3862
+ const residency = createResidentComposerRegistry();
3863
+ ctx.effect(() => () => {
3864
+ sessions.dispose();
3865
+ controller.dispose();
3866
+ }, "composer-quick-actions: client controller");
3867
+ ctx.effect(() => ctx.locale.register(QUICK_ACTIONS_LOCALE_NAMESPACE, quickActionsDictionaries), "composer-quick-actions: dictionaries");
3868
+ ctx.effect(installQuickActionStyles, "composer-quick-actions: surface styles");
3869
+ const { InputDock, ComposerDock, ManagerDock } = createQuickActionDockEntries({
3870
+ controller,
3871
+ sessions,
3872
+ residency,
3873
+ blocks: () => ctx.get("conversation")?.blocks
3874
+ });
3875
+ ctx.slots.inject("conversation.input.dock", () => [ctx.slots.register({
3876
+ name: "conversation.input.dock",
3877
+ id: "composer-quick-actions",
3878
+ order: DOCK_ORDER,
3879
+ locale: QUICK_ACTIONS_LOCALE_NAMESPACE
3880
+ }, InputDock), ctx.slots.register({
3881
+ name: "conversation.input.dock",
3882
+ id: "composer-quick-actions-manager",
3883
+ order: MANAGER_ORDER,
3884
+ locale: QUICK_ACTIONS_LOCALE_NAMESPACE
3885
+ }, ManagerDock)]);
3886
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
3887
+ name: "conversation.composer.dock",
3888
+ id: "composer-quick-actions",
3889
+ order: DOCK_ORDER,
3890
+ locale: QUICK_ACTIONS_LOCALE_NAMESPACE
3891
+ }, ComposerDock));
3892
+ }
3893
+ //#endregion
3894
+ exports.apply = apply;
3895
+ exports.inject = inject;
3896
+ exports.name = name;
3897
+ return module.exports;
3898
+ }
3899
+ });
3900
+
3901
+ //# sourceMappingURL=client.js.map