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