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/index.js
CHANGED
|
@@ -263,6 +263,74 @@ function buildPresetCatalog(input) {
|
|
|
263
263
|
};
|
|
264
264
|
}
|
|
265
265
|
//#endregion
|
|
266
|
+
//#region src/model/presets.ts
|
|
267
|
+
/**
|
|
268
|
+
* The package's built-in Preset Quick Action manifest — component 1 of the
|
|
269
|
+
* Preset Catalog (spec 5.1), declared ahead of the Host composition's
|
|
270
|
+
* `Config.presets`.
|
|
271
|
+
*
|
|
272
|
+
* It lives in the shared model because both faces merge it: the Host validates
|
|
273
|
+
* the catalog at load, and the Client rebuilds the same catalog from this list
|
|
274
|
+
* plus the `presets` form field it reads (spec 22.3). One list, so the two can
|
|
275
|
+
* never disagree about what the built-ins are.
|
|
276
|
+
*
|
|
277
|
+
* These are author-facing product copy in one language; changing a label or a
|
|
278
|
+
* text is a product decision, not an implementation one.
|
|
279
|
+
*
|
|
280
|
+
* Two rules bind any edit here:
|
|
281
|
+
*
|
|
282
|
+
* - A Preset Action ID is permanent. Label, icon and text may change under the
|
|
283
|
+
* same id, but `kind` and `confirm` are the immutable safety behaviour
|
|
284
|
+
* signature — changing either needs a new id (spec 5.1). Editing a text so it
|
|
285
|
+
* crosses the Command Send Action boundary counts as crossing that signature.
|
|
286
|
+
* - Adding an entry is additive: a new id appends to the end of every existing
|
|
287
|
+
* user's order and rewrites no stored data (spec 5.3).
|
|
288
|
+
*
|
|
289
|
+
* `summarize` and `explain` carry new ids rather than reusing
|
|
290
|
+
* `summarize-thread` and `explain-last-change`: those two shipped on the
|
|
291
|
+
* default `confirm: true`, and turning confirmation off is exactly the
|
|
292
|
+
* signature change the first rule reserves a new id for. The old ids simply
|
|
293
|
+
* leave the catalog, which the model already handles — a stored preference for
|
|
294
|
+
* an id no longer in the catalog is kept as a tombstone, neither shown nor
|
|
295
|
+
* counted (spec 5.3).
|
|
296
|
+
*/
|
|
297
|
+
const BUILT_IN_PRESETS = Object.freeze([
|
|
298
|
+
Object.freeze({
|
|
299
|
+
id: "approve",
|
|
300
|
+
label: "确认",
|
|
301
|
+
text: "确认,按你刚才说的做。",
|
|
302
|
+
icon: "✅",
|
|
303
|
+
confirm: false
|
|
304
|
+
}),
|
|
305
|
+
Object.freeze({
|
|
306
|
+
id: "continue",
|
|
307
|
+
label: "继续",
|
|
308
|
+
text: "继续。",
|
|
309
|
+
icon: "▶️",
|
|
310
|
+
confirm: false
|
|
311
|
+
}),
|
|
312
|
+
Object.freeze({
|
|
313
|
+
id: "summarize",
|
|
314
|
+
label: "总结",
|
|
315
|
+
text: "总结当前对话:已确定的结论、仍未决的问题、下一步要做的事。",
|
|
316
|
+
icon: "📝",
|
|
317
|
+
confirm: false
|
|
318
|
+
}),
|
|
319
|
+
Object.freeze({
|
|
320
|
+
id: "explain",
|
|
321
|
+
label: "解释",
|
|
322
|
+
text: "解释你刚才的改动:为什么这样做,考虑过哪些替代方案,取舍是什么。",
|
|
323
|
+
icon: "🔍",
|
|
324
|
+
confirm: false
|
|
325
|
+
}),
|
|
326
|
+
Object.freeze({
|
|
327
|
+
id: "compact-context",
|
|
328
|
+
label: "压缩",
|
|
329
|
+
text: "/compact",
|
|
330
|
+
icon: "🧹"
|
|
331
|
+
})
|
|
332
|
+
]);
|
|
333
|
+
//#endregion
|
|
266
334
|
//#region src/model/settings.ts
|
|
267
335
|
/**
|
|
268
336
|
* Settings V1 decoding (spec 4.2, 6.3). Decoding is defensive and lossless: it
|
|
@@ -271,17 +339,27 @@ function buildPresetCatalog(input) {
|
|
|
271
339
|
* Catalog-aware canonicalization lives in `normalize.ts`.
|
|
272
340
|
*/
|
|
273
341
|
/**
|
|
274
|
-
* The one namespace holding user data (spec 4.2); renaming it orphans every
|
|
275
|
-
* stored section.
|
|
276
|
-
* the
|
|
342
|
+
* The one namespace holding user data (spec 4.2, 22); renaming it orphans every
|
|
343
|
+
* stored section. Since DSH 0.1.7 a Settings namespace *is* a profile entry id,
|
|
344
|
+
* so this is also the Cordis loader entry id the bundle patch inserts — the two
|
|
345
|
+
* can no longer drift apart. It lives in the shared model because both faces
|
|
346
|
+
* address it: the Host describes and fences its canonical rewrite against it,
|
|
347
|
+
* the Client reads and writes the same form.
|
|
277
348
|
*/
|
|
278
349
|
const QUICK_ACTIONS_SETTINGS_NAMESPACE = "composer-quick-actions";
|
|
279
350
|
/**
|
|
280
|
-
* The
|
|
281
|
-
*
|
|
282
|
-
*
|
|
351
|
+
* The top-level fields of the stored section, in canonical order (spec 4.2).
|
|
352
|
+
* Each is a separate volatile Config field (spec 22.2): the DSH legacy import
|
|
353
|
+
* addresses a section by these exact top-level keys, and every write sets them
|
|
354
|
+
* field by field so a write never reaches the author's `presets`.
|
|
283
355
|
*/
|
|
284
|
-
const
|
|
356
|
+
const QUICK_ACTION_STATE_FIELDS = Object.freeze([
|
|
357
|
+
"schemaVersion",
|
|
358
|
+
"layout",
|
|
359
|
+
"userActionsById",
|
|
360
|
+
"actionOrder",
|
|
361
|
+
"presetStateById"
|
|
362
|
+
]);
|
|
285
363
|
/** The state a fresh install starts from (spec 4.2). */
|
|
286
364
|
const DEFAULT_QUICK_ACTION_SETTINGS = Object.freeze({
|
|
287
365
|
schemaVersion: 1,
|
|
@@ -466,69 +544,6 @@ function normalizeQuickActionSettings(settings, catalog) {
|
|
|
466
544
|
};
|
|
467
545
|
}
|
|
468
546
|
//#endregion
|
|
469
|
-
//#region src/host/presets.ts
|
|
470
|
-
/**
|
|
471
|
-
* The package's built-in Preset Quick Action manifest — component 1 of the
|
|
472
|
-
* Preset Catalog (spec 5.1), declared ahead of the Host composition's
|
|
473
|
-
* `Config.presets`.
|
|
474
|
-
*
|
|
475
|
-
* These are author-facing product copy in one language; changing a label or a
|
|
476
|
-
* text is a product decision, not an implementation one.
|
|
477
|
-
*
|
|
478
|
-
* Two rules bind any edit here:
|
|
479
|
-
*
|
|
480
|
-
* - A Preset Action ID is permanent. Label, icon and text may change under the
|
|
481
|
-
* same id, but `kind` and `confirm` are the immutable safety behaviour
|
|
482
|
-
* signature — changing either needs a new id (spec 5.1). Editing a text so it
|
|
483
|
-
* crosses the Command Send Action boundary counts as crossing that signature.
|
|
484
|
-
* - Adding an entry is additive: a new id appends to the end of every existing
|
|
485
|
-
* user's order and rewrites no stored data (spec 5.3).
|
|
486
|
-
*
|
|
487
|
-
* `summarize` and `explain` carry new ids rather than reusing
|
|
488
|
-
* `summarize-thread` and `explain-last-change`: those two shipped on the
|
|
489
|
-
* default `confirm: true`, and turning confirmation off is exactly the
|
|
490
|
-
* signature change the first rule reserves a new id for. The old ids simply
|
|
491
|
-
* leave the catalog, which the model already handles — a stored preference for
|
|
492
|
-
* an id no longer in the catalog is kept as a tombstone, neither shown nor
|
|
493
|
-
* counted (spec 5.3).
|
|
494
|
-
*/
|
|
495
|
-
const BUILT_IN_PRESETS = Object.freeze([
|
|
496
|
-
Object.freeze({
|
|
497
|
-
id: "approve",
|
|
498
|
-
label: "确认",
|
|
499
|
-
text: "确认,按你刚才说的做。",
|
|
500
|
-
icon: "✅",
|
|
501
|
-
confirm: false
|
|
502
|
-
}),
|
|
503
|
-
Object.freeze({
|
|
504
|
-
id: "continue",
|
|
505
|
-
label: "继续",
|
|
506
|
-
text: "继续。",
|
|
507
|
-
icon: "▶️",
|
|
508
|
-
confirm: false
|
|
509
|
-
}),
|
|
510
|
-
Object.freeze({
|
|
511
|
-
id: "summarize",
|
|
512
|
-
label: "总结",
|
|
513
|
-
text: "总结当前对话:已确定的结论、仍未决的问题、下一步要做的事。",
|
|
514
|
-
icon: "📝",
|
|
515
|
-
confirm: false
|
|
516
|
-
}),
|
|
517
|
-
Object.freeze({
|
|
518
|
-
id: "explain",
|
|
519
|
-
label: "解释",
|
|
520
|
-
text: "解释你刚才的改动:为什么这样做,考虑过哪些替代方案,取舍是什么。",
|
|
521
|
-
icon: "🔍",
|
|
522
|
-
confirm: false
|
|
523
|
-
}),
|
|
524
|
-
Object.freeze({
|
|
525
|
-
id: "compact-context",
|
|
526
|
-
label: "压缩",
|
|
527
|
-
text: "/compact",
|
|
528
|
-
icon: "🧹"
|
|
529
|
-
})
|
|
530
|
-
]);
|
|
531
|
-
//#endregion
|
|
532
547
|
//#region src/host/config.ts
|
|
533
548
|
/**
|
|
534
549
|
* Host composition config: the author-facing `presets` list, merged behind the
|
|
@@ -545,11 +560,20 @@ function describeIssue(issue) {
|
|
|
545
560
|
return `${issue.id === void 0 ? at : `${at} ("${issue.id}")`}: ${issue.field} is ${issue.reason}`;
|
|
546
561
|
}
|
|
547
562
|
/**
|
|
563
|
+
* The plain value behind one Config field. A Loader-resolved config hands each
|
|
564
|
+
* volatile field over as a `Volatile` reference (spec 22.2); a composition that
|
|
565
|
+
* calls `apply` directly hands the plain value.
|
|
566
|
+
*/
|
|
567
|
+
function readVolatile(field) {
|
|
568
|
+
if (typeof field === "object" && field !== null && typeof field.get === "function") return field.get();
|
|
569
|
+
return field;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
548
572
|
* Read one Host composition config into the authoritative catalog.
|
|
549
573
|
* @param config - the composition entry's config, absent when nothing was declared.
|
|
550
574
|
*/
|
|
551
575
|
function readComposerQuickActionsConfig(config) {
|
|
552
|
-
const declared = config?.presets;
|
|
576
|
+
const declared = readVolatile(config?.presets);
|
|
553
577
|
if (declared !== void 0 && !Array.isArray(declared)) return {
|
|
554
578
|
ok: false,
|
|
555
579
|
message: "composer-quick-actions: `presets` must be a list of preset quick actions"
|
|
@@ -570,39 +594,61 @@ function readComposerQuickActionsConfig(config) {
|
|
|
570
594
|
//#endregion
|
|
571
595
|
//#region src/host/settings.ts
|
|
572
596
|
/**
|
|
573
|
-
* The
|
|
574
|
-
* (spec 4.2, 6.1, 6.3).
|
|
597
|
+
* The plugin's Config — which since DSH 0.1.7 *is* its Settings form — and the
|
|
598
|
+
* Host's canonical rewrite (spec 4.2, 6.1, 6.3, 22).
|
|
575
599
|
*
|
|
576
|
-
* The Host is the sole validation and migration authority, so the
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
600
|
+
* The Host is the sole validation and migration authority, so the schema is
|
|
601
|
+
* deliberately permissive: it fixes the shape of the section and its defaults,
|
|
602
|
+
* and nothing else. A strict schema would refuse a section a higher version
|
|
603
|
+
* wrote — which is exactly the data spec 5.3 requires to survive a downgrade
|
|
604
|
+
* untouched. The shared model decodes and normalizes what the schema lets
|
|
605
|
+
* through, and it never rewrites content it cannot render.
|
|
582
606
|
*/
|
|
583
607
|
/**
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
608
|
+
* The plugin Config (spec 22.2). Every field is `volatile`, because DSH lets a
|
|
609
|
+
* Settings form — and so the Client — reach volatile fields only.
|
|
610
|
+
*
|
|
611
|
+
* - The five user-state fields are top-level on purpose: the DSH legacy import
|
|
612
|
+
* moves an old `settings.yaml` section into the entry of the same id key by
|
|
613
|
+
* key, and refuses the whole section when any top-level key is not a volatile
|
|
614
|
+
* field. This exact shape is what lets that one-shot import land.
|
|
615
|
+
* - `presets` is the author's list merged behind the built-ins (spec 5.1). It
|
|
616
|
+
* is volatile only so the Client can read it — a non-volatile field never
|
|
617
|
+
* reaches the browser — and nothing in this plugin ever writes it.
|
|
618
|
+
*
|
|
619
|
+
* The collections stay unconstrained: a stricter schema would refuse a section
|
|
620
|
+
* this release cannot render, and refusing it is how stored data gets lost.
|
|
588
621
|
*/
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
622
|
+
const Config = Schema.object({
|
|
623
|
+
presets: Schema.any().default([]).volatile(),
|
|
624
|
+
schemaVersion: Schema.number().default(1).volatile(),
|
|
625
|
+
layout: Schema.string().default("ribbon").volatile(),
|
|
626
|
+
userActionsById: Schema.any().default({}).volatile(),
|
|
627
|
+
actionOrder: Schema.any().default([]).volatile(),
|
|
628
|
+
presetStateById: Schema.any().default({}).volatile()
|
|
595
629
|
});
|
|
596
630
|
/**
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
631
|
+
* One write per user-state field, never touching `presets`: the author's list
|
|
632
|
+
* shares the entry's config, and a write that restated it would pin whatever it
|
|
633
|
+
* read into the profile layer.
|
|
600
634
|
*/
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
635
|
+
function stateOps(section) {
|
|
636
|
+
return QUICK_ACTION_STATE_FIELDS.map((field) => ({
|
|
637
|
+
op: "set",
|
|
638
|
+
path: [field],
|
|
639
|
+
value: section[field]
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The user-state fields the profile layer actually sets, or `undefined` when it
|
|
644
|
+
* sets none. The same layer may carry the author's `presets`; those are Config,
|
|
645
|
+
* not stored state, so a profile holding only presets has nothing stored.
|
|
646
|
+
*/
|
|
647
|
+
function storedState(user) {
|
|
648
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) return void 0;
|
|
649
|
+
const fields = Object.entries(user).filter(([field]) => QUICK_ACTION_STATE_FIELDS.includes(field));
|
|
650
|
+
return fields.length === 0 ? void 0 : Object.fromEntries(fields);
|
|
651
|
+
}
|
|
606
652
|
/** Recognize a revision conflict by its stable code, never by prototype (realm copies). */
|
|
607
653
|
function isConflict(error) {
|
|
608
654
|
return typeof error === "object" && error !== null && error.code === "SETTINGS_CONFLICT";
|
|
@@ -614,9 +660,9 @@ const REWRITE_ATTEMPTS = 3;
|
|
|
614
660
|
* persist it behind the revision it was read at (spec 6.3).
|
|
615
661
|
*
|
|
616
662
|
* Nothing is written when nothing is stored: materializing the defaults into the
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
663
|
+
* profile layer would shadow every layer below it for good. Nothing is written
|
|
664
|
+
* for a higher `schemaVersion` either — that data belongs to the version that
|
|
665
|
+
* wrote it.
|
|
620
666
|
*
|
|
621
667
|
* A conflict means another writer won the race, so the rewrite refreshes the
|
|
622
668
|
* authoritative snapshot and recomputes rather than replaying its own stale
|
|
@@ -628,7 +674,7 @@ async function rewriteCanonicalSettings(settings, catalog) {
|
|
|
628
674
|
for (let attempt = 1; attempt <= REWRITE_ATTEMPTS; attempt += 1) {
|
|
629
675
|
const descriptor = settings.describe().find((candidate) => candidate.ns === QUICK_ACTIONS_SETTINGS_NAMESPACE);
|
|
630
676
|
if (descriptor === void 0) return { status: "unregistered" };
|
|
631
|
-
const stored = descriptor.user;
|
|
677
|
+
const stored = storedState(descriptor.user);
|
|
632
678
|
if (stored === void 0) return { status: "nothing-stored" };
|
|
633
679
|
const storedVersion = storedSchemaVersion(stored);
|
|
634
680
|
if (storedVersion > 1) return {
|
|
@@ -638,7 +684,7 @@ async function rewriteCanonicalSettings(settings, catalog) {
|
|
|
638
684
|
const canonical = canonicalSettings(stored, catalog);
|
|
639
685
|
if (deepEqualJson(stored, canonical)) return { status: "unchanged" };
|
|
640
686
|
try {
|
|
641
|
-
await settings.
|
|
687
|
+
await settings.mutate(QUICK_ACTIONS_SETTINGS_NAMESPACE, stateOps(canonical), descriptor.revision);
|
|
642
688
|
return {
|
|
643
689
|
status: "rewritten",
|
|
644
690
|
revision: descriptor.revision
|
|
@@ -664,49 +710,52 @@ function canonicalSettings(stored, catalog) {
|
|
|
664
710
|
//#endregion
|
|
665
711
|
//#region src/host/index.ts
|
|
666
712
|
/**
|
|
667
|
-
* Host ownership of the Quick Action state (spec 6.1,
|
|
668
|
-
* validates the Preset Catalog,
|
|
669
|
-
* the read-only catalog namespace, canonicalizes the stored section behind a
|
|
713
|
+
* Host ownership of the Quick Action state (spec 6.1, 22): it merges and
|
|
714
|
+
* validates the Preset Catalog, canonicalizes the stored section behind a
|
|
670
715
|
* revision fence, and serves the authoritative catalog snapshot.
|
|
671
716
|
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
717
|
+
* Since DSH 0.1.7 there is nothing to register: the plugin's own Config is its
|
|
718
|
+
* Settings form (spec 22.1). The Host never reaches past Settings to a file or
|
|
719
|
+
* a storage backend, and it never substitutes its own storage when the
|
|
720
|
+
* provider is missing.
|
|
675
721
|
*/
|
|
676
722
|
/** A copy nothing can reach the authoritative catalog through. */
|
|
677
723
|
function detachedCatalog(catalog) {
|
|
678
724
|
return JSON.parse(JSON.stringify(catalog));
|
|
679
725
|
}
|
|
680
726
|
/**
|
|
681
|
-
* Load
|
|
727
|
+
* Load and validate the catalog.
|
|
682
728
|
*
|
|
683
|
-
* An invalid preset configuration throws
|
|
684
|
-
*
|
|
685
|
-
*
|
|
729
|
+
* An invalid preset configuration throws: the catalog is author-owned, so a
|
|
730
|
+
* mistake fails plugin loading rather than silently shipping a partial catalog
|
|
731
|
+
* (spec 5.1).
|
|
686
732
|
*
|
|
687
|
-
* @param settings - the Host settings provider.
|
|
688
733
|
* @param config - the Host composition entry's config.
|
|
689
734
|
*/
|
|
690
|
-
function startQuickActionsHost(
|
|
735
|
+
function startQuickActionsHost(config) {
|
|
691
736
|
const loaded = readComposerQuickActionsConfig(config);
|
|
692
737
|
if (!loaded.ok) throw new Error(loaded.message);
|
|
693
738
|
const { catalog } = loaded;
|
|
694
|
-
settings.register(QUICK_ACTIONS_SETTINGS_NAMESPACE, quickActionSettingsSchema, { applies: "live" });
|
|
695
|
-
settings.register(QUICK_ACTIONS_CATALOG_NAMESPACE, quickActionCatalogSchema, {
|
|
696
|
-
base: detachedCatalog(catalog),
|
|
697
|
-
applies: "restart"
|
|
698
|
-
});
|
|
699
739
|
return {
|
|
700
|
-
|
|
701
|
-
|
|
740
|
+
describeCatalog: async () => detachedCatalog(catalog),
|
|
741
|
+
canonicalize: (settings) => rewriteCanonicalSettings(settings, catalog)
|
|
702
742
|
};
|
|
703
743
|
}
|
|
704
744
|
//#endregion
|
|
705
745
|
//#region src/index.ts
|
|
706
746
|
const name = "composer-quick-actions";
|
|
707
|
-
const inject = ["settings"];
|
|
708
747
|
/**
|
|
709
|
-
*
|
|
748
|
+
* Settles once every profile entry has, which is when DSH serves this entry's
|
|
749
|
+
* form at all: an entry is described only while its fiber is active, and the
|
|
750
|
+
* legacy `settings.yaml` import waits on the same promise. A composition with
|
|
751
|
+
* no Loader has nothing to wait for.
|
|
752
|
+
*/
|
|
753
|
+
function entriesSettled(ctx) {
|
|
754
|
+
const loader = ctx.get("loader");
|
|
755
|
+
return typeof loader?.await === "function" ? loader.await() : Promise.resolve();
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Load the Preset Catalog and, while Settings is present, canonicalize the
|
|
710
759
|
* stored section. An invalid preset configuration throws here, failing plugin
|
|
711
760
|
* loading loudly rather than shipping a partial catalog (spec 5.1).
|
|
712
761
|
*
|
|
@@ -714,24 +763,27 @@ const inject = ["settings"];
|
|
|
714
763
|
* can reach the catalog projection; the cordis loader ignores the value.
|
|
715
764
|
*/
|
|
716
765
|
function apply(ctx, config) {
|
|
717
|
-
const
|
|
718
|
-
const host = startQuickActionsHost(settings, config);
|
|
766
|
+
const host = startQuickActionsHost(config);
|
|
719
767
|
const logger = ctx.logger("composer-quick-actions");
|
|
720
|
-
ctx.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
768
|
+
ctx.inject(["settings"], (scoped) => {
|
|
769
|
+
const settings = scoped.settings;
|
|
770
|
+
scoped.effect(() => scoped.settings.configure({ auto: false }, ctx.fiber), "composer-quick-actions: settings page policy");
|
|
771
|
+
scoped.effect(() => {
|
|
772
|
+
let owned = true;
|
|
773
|
+
entriesSettled(ctx).then(() => owned ? host.canonicalize(settings) : void 0).then((outcome) => {
|
|
774
|
+
if (!owned || outcome === void 0) return;
|
|
775
|
+
if (outcome.status === "conflict") logger.warn("settings kept moving during the canonical rewrite after %d attempts; leaving the stored section as the concurrent writer left it", outcome.attempts);
|
|
776
|
+
else if (outcome.status === "read-only") logger.warn("settings are read-only; the stored section was left untouched");
|
|
777
|
+
else if (outcome.status === "newer-version") logger.warn("the stored section declares schemaVersion %d; leaving it untouched so downgrading loses nothing", outcome.schemaVersion);
|
|
778
|
+
}, (error) => {
|
|
779
|
+
if (owned) logger.error(error);
|
|
780
|
+
});
|
|
781
|
+
return () => {
|
|
782
|
+
owned = false;
|
|
783
|
+
};
|
|
784
|
+
}, "composer-quick-actions: canonical settings rewrite");
|
|
785
|
+
});
|
|
734
786
|
return host;
|
|
735
787
|
}
|
|
736
788
|
//#endregion
|
|
737
|
-
export {
|
|
789
|
+
export { Config, apply, name };
|
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
import type { CustomActionId, PresetActionId, PresetCatalog, QuickActionDraft, QuickActionLayout, QuickActionMutationRejection, QuickActionProjection, QuickActionRef, QuickActionSettingsV1 } from '../model/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* One path-addressed edit inside
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* One path-addressed edit inside the form. The transport also accepts an `unset`
|
|
4
|
+
* form; this plugin writes the user state field by field and never clears one,
|
|
5
|
+
* so only the form it submits is declared.
|
|
6
6
|
*/
|
|
7
7
|
export interface SettingsSetOp {
|
|
8
8
|
readonly op: 'set';
|
|
9
|
-
readonly path:
|
|
10
|
-
readonly value:
|
|
9
|
+
readonly path: string[];
|
|
10
|
+
readonly value: SettingsJsonValue;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
export
|
|
14
|
-
|
|
12
|
+
/** A JSON value as the Settings wire carries it: the same data the model holds, minus `readonly`. */
|
|
13
|
+
export type SettingsJsonValue = null | boolean | number | string | SettingsJsonValue[] | {
|
|
14
|
+
[key: string]: SettingsJsonValue;
|
|
15
|
+
};
|
|
16
|
+
/** The plugin entry's form as `ctx.configForms` publishes it. */
|
|
17
|
+
export interface ConfigFormSnapshot {
|
|
18
|
+
/** `loading` until the shared document answers; `unavailable` when the entry is not served. */
|
|
15
19
|
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
16
|
-
/** Resolved
|
|
20
|
+
/** Resolved volatile fields: schema defaults, then the layers below the profile, then the profile. */
|
|
17
21
|
readonly value?: unknown;
|
|
18
|
-
/**
|
|
19
|
-
readonly
|
|
20
|
-
/** Revision of the raw user section, sent back to fence a write. */
|
|
21
|
-
readonly revision?: number;
|
|
22
|
+
/** Revision of the raw entry configuration, sent back to fence a write. */
|
|
23
|
+
readonly revision?: number | undefined;
|
|
22
24
|
/** Whether the provider accepts writes at all. */
|
|
23
25
|
readonly writable: boolean;
|
|
24
26
|
/** `memory` on a page this Client keeps process-local; no write ever crosses the wire. */
|
|
25
27
|
readonly mode: 'host' | 'memory';
|
|
26
28
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
getSnapshot(): SettingsScopeSnapshot;
|
|
29
|
+
/**
|
|
30
|
+
* One entry's form: a derived read plus that entry's serialized writes. A write
|
|
31
|
+
* answers whether the Host accepted it, but a refusal and a lost fence both
|
|
32
|
+
* answer `false`, so this controller never reads the answer — it classifies
|
|
33
|
+
* from the snapshot the form publishes after the write instead.
|
|
34
|
+
*/
|
|
35
|
+
export interface ConfigForm {
|
|
36
|
+
getSnapshot(): ConfigFormSnapshot;
|
|
36
37
|
subscribe(listener: () => void): () => void;
|
|
37
|
-
mutate(ops: readonly SettingsSetOp[], expectedRevision?: number): Promise<
|
|
38
|
+
mutate(ops: readonly SettingsSetOp[], expectedRevision?: number): Promise<unknown>;
|
|
38
39
|
}
|
|
39
40
|
/** The shared settings document as the mirror holds it. */
|
|
40
41
|
export interface SettingsMirrorSnapshot {
|
|
@@ -44,19 +45,20 @@ export interface SettingsMirrorSnapshot {
|
|
|
44
45
|
readonly error: string | null;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
|
-
* The shared describe mirror's read face. A
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* The shared describe mirror's read face. A form publishes nothing at all while
|
|
49
|
+
* the document is unanswered, so a read that *failed* is only visible here —
|
|
50
|
+
* which is what tells a first catalog read failure apart from one still in
|
|
50
51
|
* flight (spec 10).
|
|
51
52
|
*/
|
|
52
53
|
export interface SettingsMirror {
|
|
53
54
|
getSnapshot(): SettingsMirrorSnapshot;
|
|
54
55
|
subscribe(listener: () => void): () => void;
|
|
55
|
-
|
|
56
|
+
/** Read the document if nothing holds or reads it — exactly the retry after a failed first read. */
|
|
57
|
+
ensure(): Promise<void>;
|
|
56
58
|
}
|
|
57
|
-
/** `ctx.
|
|
58
|
-
export interface
|
|
59
|
-
|
|
59
|
+
/** `ctx.configForms`. */
|
|
60
|
+
export interface ConfigFormsService {
|
|
61
|
+
get(entryId: string): ConfigForm;
|
|
60
62
|
describe(): SettingsMirror;
|
|
61
63
|
}
|
|
62
64
|
/** Connection lifecycle as `ctx.connection` publishes it; `undefined` before the loop starts. */
|
|
@@ -72,10 +74,19 @@ export interface ConnectionLike {
|
|
|
72
74
|
export type CatalogErrorReason =
|
|
73
75
|
/** The settings document could not be read at all; retrying is the remedy. */
|
|
74
76
|
'unreadable'
|
|
75
|
-
/** The Host serves no
|
|
77
|
+
/** The Host serves this page no form for the entry — it is not active, or the page is process-local. */
|
|
76
78
|
| 'unavailable'
|
|
77
|
-
/**
|
|
79
|
+
/** The author `presets` field is not a list, or any entry is invalid; never a truncated catalog (spec 5.1, 22.3). */
|
|
78
80
|
| 'undecodable';
|
|
81
|
+
/**
|
|
82
|
+
* Whether a user retry can clear this catalog error (spec 10).
|
|
83
|
+
*
|
|
84
|
+
* Only a failed first read can: the mirror's public face re-reads only while it
|
|
85
|
+
* holds no document (`ensure()`, spec 22.4). The other two arrive with a held
|
|
86
|
+
* document and clear on their own when the Host's `settings/document-updated`
|
|
87
|
+
* broadcast or a reconnect refreshes it, so a retry there would do nothing.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isRetryableCatalogError(reason: CatalogErrorReason): boolean;
|
|
79
90
|
/** The authoritative Preset Catalog as the Client currently knows it. */
|
|
80
91
|
export type CatalogState = {
|
|
81
92
|
readonly status: 'loading';
|
|
@@ -130,9 +141,10 @@ export type QuickActionWriteFailure =
|
|
|
130
141
|
/**
|
|
131
142
|
* The structured result every mutation answers with (spec 15, decision 4).
|
|
132
143
|
*
|
|
133
|
-
* The shipped `
|
|
134
|
-
*
|
|
135
|
-
*
|
|
144
|
+
* The shipped `ConfigForm.mutate` answers a bare boolean that folds a refusal and
|
|
145
|
+
* a lost fence into one `false`, and this release adds no DSH core interface
|
|
146
|
+
* (spec 16.4), so success, refusal and conflict are told apart here, from the
|
|
147
|
+
* authoritative snapshot the form publishes after the write: the
|
|
136
148
|
* write committed when the stored state now reads back as the plan, the fence was
|
|
137
149
|
* lost when the namespace revision moved elsewhere, and anything else is a Host
|
|
138
150
|
* refusal. Each failing case leaves the caller's form content untouched — the
|
|
@@ -194,14 +206,24 @@ export interface QuickActionsController {
|
|
|
194
206
|
}
|
|
195
207
|
/** What the controller is built over. */
|
|
196
208
|
export interface QuickActionsControllerOptions {
|
|
197
|
-
readonly
|
|
209
|
+
readonly configForms: ConfigFormsService;
|
|
198
210
|
readonly connection: ConnectionLike;
|
|
199
211
|
/** Mints one Custom Action ID. Defaults to `crypto.randomUUID()`. */
|
|
200
212
|
readonly mintCustomActionId?: () => string;
|
|
213
|
+
/**
|
|
214
|
+
* The built-in manifest merged ahead of the `presets` field. Defaults to the
|
|
215
|
+
* package's own, the one the Host validated at load; the suite passes its own
|
|
216
|
+
* fixtures so that a catalog is exactly what a case declares.
|
|
217
|
+
*/
|
|
218
|
+
readonly builtinPresets?: readonly unknown[];
|
|
201
219
|
}
|
|
202
220
|
/**
|
|
203
|
-
* Create the controller.
|
|
204
|
-
*
|
|
205
|
-
*
|
|
221
|
+
* Create the controller. The form is owned by the settings provider and shared
|
|
222
|
+
* by every reader of this entry, so there is nothing to release for it;
|
|
223
|
+
* `dispose()` withdraws this module's own subscriptions.
|
|
224
|
+
*
|
|
225
|
+
* Both the catalog and the user state come off the same form. It is decoded
|
|
226
|
+
* here, by the shared model, rather than trusted as the transport validated
|
|
227
|
+
* it: the Client's reading of a confirmed snapshot is the model's.
|
|
206
228
|
*/
|
|
207
229
|
export declare function createQuickActionsController(options: QuickActionsControllerOptions): QuickActionsController;
|
|
@@ -40,10 +40,14 @@ export interface InputOccurrence {
|
|
|
40
40
|
readonly source: string;
|
|
41
41
|
readonly ref: string;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/**
|
|
44
|
+
* One row of the read-only transient queue projection. Opaque on purpose: the
|
|
45
|
+
* plugin never reads a row — a send may join a running turn (spec 9.2) — and
|
|
46
|
+
* DSH 0.1.7 replaced the row shape outright (`placement` rows became the next
|
|
47
|
+
* turn's `UserMessage`s), so any field declared here would only be one more
|
|
48
|
+
* name to drift (spec 22.5).
|
|
49
|
+
*/
|
|
50
|
+
export type InputQueueRow = unknown;
|
|
47
51
|
/**
|
|
48
52
|
* The published per-Session input state — the whole public snapshot, listed
|
|
49
53
|
* field by field because spec 9.5 makes this exact set the only evidence the
|
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
* Client composition entry for Composer Quick Actions.
|
|
3
3
|
*
|
|
4
4
|
* This fiber owns everything the feature holds in the browser: one global
|
|
5
|
-
* `QuickActionsController` over the
|
|
5
|
+
* `QuickActionsController` over the entry's Settings form, the per-Session
|
|
6
6
|
* execution registry, the Resident Composer registry, the locale dictionaries,
|
|
7
7
|
* the stylesheet, and the two dock Slot registrations. All of it is installed
|
|
8
8
|
* through `ctx.effect` and `ctx.slots.inject`, so unloading the plugin leaves
|
|
9
9
|
* no listener, registration, namespace, style tag or subscription behind
|
|
10
10
|
* (spec 7.3).
|
|
11
11
|
*
|
|
12
|
-
* `
|
|
12
|
+
* `configForms` is how this Client reaches Host-authoritative state at all — it
|
|
13
13
|
* never writes a file, never treats browser storage as a source of truth, and
|
|
14
14
|
* never reaches past Settings to a provider (spec 6.3). `connection` supplies the
|
|
15
15
|
* generation state the read-only-while-disconnected rule needs (spec 10).
|
|
16
16
|
*/
|
|
17
17
|
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
-
import type {
|
|
18
|
+
import type { ConfigFormsService, ConnectionLike } from './controller.js';
|
|
19
19
|
import type { ConversationLike, LocaleService, SlotsService } from './dsh.js';
|
|
20
20
|
declare module '@deepseek-ai/cordis' {
|
|
21
21
|
interface Context {
|
|
22
|
-
|
|
22
|
+
configForms: ConfigFormsService;
|
|
23
23
|
connection: ConnectionLike;
|
|
24
24
|
slots: SlotsService;
|
|
25
25
|
locale: LocaleService;
|