dsh-plugin-effort-declare 0.1.1 → 0.1.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 (42) hide show
  1. package/CONTRIBUTING.en.md +5 -1
  2. package/CONTRIBUTING.md +5 -1
  3. package/INSTALL.en.md +1 -1
  4. package/INSTALL.md +1 -1
  5. package/README.en.md +8 -7
  6. package/README.md +8 -7
  7. package/lib/client.js +389 -65
  8. package/lib/client.js.map +1 -1
  9. package/lib/types/client/EffortDeclareSection.d.ts +12 -2
  10. package/lib/types/client/EffortDeclareSection.d.ts.map +1 -1
  11. package/lib/types/client/build-info.d.ts +2 -0
  12. package/lib/types/client/build-info.d.ts.map +1 -0
  13. package/lib/types/client/index.d.ts.map +1 -1
  14. package/lib/types/client/load-drafts.d.ts +36 -6
  15. package/lib/types/client/load-drafts.d.ts.map +1 -1
  16. package/lib/types/client/locales.d.ts +1 -1
  17. package/lib/types/client/locales.d.ts.map +1 -1
  18. package/lib/types/core/attribution.d.ts +13 -0
  19. package/lib/types/core/attribution.d.ts.map +1 -0
  20. package/lib/types/core/catalog.d.ts +5 -3
  21. package/lib/types/core/catalog.d.ts.map +1 -1
  22. package/lib/types/core/drafts.d.ts +34 -2
  23. package/lib/types/core/drafts.d.ts.map +1 -1
  24. package/lib/types/core/efforts.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/README.en.md +1 -1
  27. package/src/README.md +1 -1
  28. package/src/client/EffortDeclareSection.tsx +158 -46
  29. package/src/client/README.en.md +5 -3
  30. package/src/client/README.md +5 -3
  31. package/src/client/build-info.ts +12 -0
  32. package/src/client/effort-declare.module.css +8 -0
  33. package/src/client/globals.d.ts +5 -0
  34. package/src/client/index.ts +14 -9
  35. package/src/client/load-drafts.ts +124 -8
  36. package/src/client/locales.ts +3 -0
  37. package/src/core/README.en.md +4 -3
  38. package/src/core/README.md +4 -3
  39. package/src/core/attribution.ts +24 -0
  40. package/src/core/catalog.ts +5 -3
  41. package/src/core/drafts.ts +151 -4
  42. package/src/core/efforts.ts +4 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dictionary namespace for the effort-declare settings section.
3
3
  */
4
- export type EffortDeclareKey = 'nav' | 'title' | 'intro' | 'empty' | 'emptyHint' | 'loadError' | 'loading' | 'readOnly' | 'save' | 'saving' | 'cancel' | 'saved' | 'conflict' | 'dirtyConflict' | 'presets' | 'presetDeepSeek' | 'presetOpenAI' | 'presetToggle' | 'presetToggleWarn' | 'model' | 'levels' | 'wire' | 'offMode' | 'offAbsent' | 'offEmpty' | 'offValue' | 'offValuePlaceholder' | 'clear' | 'advanced' | 'thinkingFormat' | 'thinkingFormatDefault' | 'supportsDeveloperRole' | 'supportsReasoningEffort' | 'developerTrueHint' | 'compatSummary' | 'errorEmpty' | 'errorOffOnly' | 'errorBadWire' | 'noModels' | 'reload';
4
+ export type EffortDeclareKey = 'nav' | 'title' | 'intro' | 'empty' | 'emptyHint' | 'loadError' | 'loading' | 'readOnly' | 'save' | 'saving' | 'saveBusy' | 'cancel' | 'saved' | 'conflict' | 'dirtyConflict' | 'presets' | 'presetDeepSeek' | 'presetOpenAI' | 'presetToggle' | 'presetToggleWarn' | 'model' | 'levels' | 'wire' | 'offMode' | 'offAbsent' | 'offEmpty' | 'offValue' | 'offValuePlaceholder' | 'clear' | 'advanced' | 'thinkingFormat' | 'thinkingFormatDefault' | 'supportsDeveloperRole' | 'supportsReasoningEffort' | 'developerTrueHint' | 'compatSummary' | 'errorEmpty' | 'errorOffOnly' | 'errorBadWire' | 'noModels' | 'reload';
5
5
  export declare const NS = "plugin-effort-declare";
6
6
  export declare const zh: Record<EffortDeclareKey, string>;
7
7
  export declare const en: Record<EffortDeclareKey, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,gBAAgB,GACxB,KAAK,GACL,OAAO,GACP,OAAO,GACP,OAAO,GACP,WAAW,GACX,WAAW,GACX,SAAS,GACT,UAAU,GACV,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,UAAU,GACV,eAAe,GACf,SAAS,GACT,gBAAgB,GAChB,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,OAAO,GACP,QAAQ,GACR,MAAM,GACN,SAAS,GACT,WAAW,GACX,UAAU,GACV,UAAU,GACV,qBAAqB,GACrB,OAAO,GACP,UAAU,GACV,gBAAgB,GAChB,uBAAuB,GACvB,uBAAuB,GACvB,yBAAyB,GACzB,mBAAmB,GACnB,eAAe,GACf,YAAY,GACZ,cAAc,GACd,cAAc,GACd,UAAU,GACV,QAAQ,CAAA;AAEZ,eAAO,MAAM,EAAE,0BAA0B,CAAA;AAEzC,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAyC/C,CAAA;AAED,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAyC/C,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,gBAAgB,GACxB,KAAK,GACL,OAAO,GACP,OAAO,GACP,OAAO,GACP,WAAW,GACX,WAAW,GACX,SAAS,GACT,UAAU,GACV,MAAM,GACN,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,OAAO,GACP,UAAU,GACV,eAAe,GACf,SAAS,GACT,gBAAgB,GAChB,cAAc,GACd,cAAc,GACd,kBAAkB,GAClB,OAAO,GACP,QAAQ,GACR,MAAM,GACN,SAAS,GACT,WAAW,GACX,UAAU,GACV,UAAU,GACV,qBAAqB,GACrB,OAAO,GACP,UAAU,GACV,gBAAgB,GAChB,uBAAuB,GACvB,uBAAuB,GACvB,yBAAyB,GACzB,mBAAmB,GACnB,eAAe,GACf,YAAY,GACZ,cAAc,GACd,cAAc,GACd,UAAU,GACV,QAAQ,CAAA;AAEZ,eAAO,MAAM,EAAE,0BAA0B,CAAA;AAEzC,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CA0C/C,CAAA;AAED,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CA0C/C,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Plugin footer attribution. Version and end-year are frozen into the client
3
+ * bundle at pack time; this module only formats the line.
4
+ */
5
+ /** First publication year (LICENSE). Not the user's wall clock. */
6
+ export declare const COPYRIGHT_FROM = 2026;
7
+ export declare const COPYRIGHT_HOLDER = "Stardust";
8
+ /**
9
+ * `0.1.2 © 2026 Stardust` or `0.1.2 © 2026–2027 Stardust`.
10
+ * Throws if version is empty or `to < from` — a bad stamp must not render.
11
+ */
12
+ export declare function formatAttribution(version: string, from: number, to: number): string;
13
+ //# sourceMappingURL=attribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../../../src/core/attribution.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mEAAmE;AACnE,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,eAAO,MAAM,gBAAgB,aAAa,CAAA;AAE1C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CASnF"}
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * Levels match `@deepseek-ai/dsh-llm-pi-ai` catalog.ts `THINKING_LEVELS`.
5
5
  * Formats match `SUPPORTED_THINKING_FORMATS` in the same file (rc.8).
6
- * Tests pin these lists against llm-pi-ai. The settings page never offers
7
- * the handwritten thinkingFormat list as writable choices — only the live
8
- * schema union, plus a stored value that the union omitted.
6
+ * Tests pin these lists against a checked-in schema fixture
7
+ * (`tests/fixtures/pi-ai-thinking-format-union.ts`) and the local level
8
+ * whitelist. The settings page never offers the handwritten thinkingFormat
9
+ * list as writable choices — only the live schema union, plus a stored
10
+ * value that the union omitted.
9
11
  */
10
12
  /** Selectable reasoning levels, in pi-ai escalation order. */
11
13
  export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../src/core/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8DAA8D;AAC9D,eAAO,MAAM,eAAe,sEAQlB,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE5D,yEAAyE;AACzE,eAAO,MAAM,2BAA2B,6DAEvC,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,gJAW5B,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,sDAAsD;AACtD,eAAO,MAAM,kBAAkB,uBAAuB,CAAA;AAEtD,6CAA6C;AAC7C,eAAO,MAAM,YAAY,cAAc,CAAA;AAEvC,kDAAkD;AAClD,eAAO,MAAM,eAAe,iBAAiB,CAAA;AAE7C,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AAEpD,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,YAAgB,CAAA"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../src/core/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8DAA8D;AAC9D,eAAO,MAAM,eAAe,sEAQlB,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE5D,yEAAyE;AACzE,eAAO,MAAM,2BAA2B,6DAEvC,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,gJAW5B,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,sDAAsD;AACtD,eAAO,MAAM,kBAAkB,uBAAuB,CAAA;AAEtD,6CAA6C;AAC7C,eAAO,MAAM,YAAY,cAAc,CAAA;AAEvC,kDAAkD;AAClD,eAAO,MAAM,eAAe,iBAAiB,CAAA;AAE7C,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AAEpD,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,YAAgB,CAAA"}
@@ -13,6 +13,10 @@ export interface RouteDraft {
13
13
  /** Whether `compat` is present on the user profile (not the effective view). */
14
14
  compatPresent: boolean;
15
15
  }
16
+ /** JSON-stable equality matching pathOps (key order included). */
17
+ export declare function sliceEqual(left: unknown, right: unknown): boolean;
18
+ /** Whether two settings slices differ. */
19
+ export declare function sliceChanged(before: unknown, after: unknown): boolean;
16
20
  /** Build a draft from the stored user subtree (never from effective `value`). */
17
21
  export declare function routeDraftFromUserProfile(args: {
18
22
  provider: string;
@@ -31,8 +35,36 @@ export declare function applySaveSuccess(drafts: readonly RouteDraft[], savedPro
31
35
  revision: number;
32
36
  }): RouteDraft[];
33
37
  /**
34
- * Apply a freshly loaded table. Dirty cards keep models/compat; originals and
35
- * revision follow the incoming snapshot so a later save is against the new user layer.
38
+ * Membership follows the latest user-layer models list (Models page add/delete).
39
+ * Local unsaved `reasoningEfforts` (including a cleared key) overlay by id.
40
+ */
41
+ export declare function mergeModelsById(args: {
42
+ prevModels: readonly Record<string, unknown>[];
43
+ prevOriginal: readonly Record<string, unknown>[];
44
+ incomingModels: readonly Record<string, unknown>[];
45
+ incomingOriginal: readonly Record<string, unknown>[];
46
+ }): {
47
+ models: Record<string, unknown>[];
48
+ conflicted: boolean;
49
+ };
50
+ /**
51
+ * Three-way compat merge: locally changed keys stay local; everything else
52
+ * follows incoming. Conflict only when a locally dirty key also moved in originals.
53
+ */
54
+ export declare function mergeCompat(args: {
55
+ prev: Record<string, unknown>;
56
+ prevOriginal: Record<string, unknown>;
57
+ incoming: Record<string, unknown>;
58
+ incomingOriginal: Record<string, unknown>;
59
+ }): {
60
+ compat: Record<string, unknown>;
61
+ conflicted: boolean;
62
+ };
63
+ /**
64
+ * Apply a freshly loaded table. Membership and metadata follow incoming;
65
+ * unsaved reasoningEfforts / dirty compat keys overlay by id. Conflict only
66
+ * when a locally dirty field also changed in originals (revision-only bumps
67
+ * and sibling-card saves do not warn).
36
68
  */
37
69
  export declare function mergeLoadedDrafts(current: readonly RouteDraft[], incoming: readonly RouteDraft[], options: {
38
70
  preserveDirty: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"drafts.d.ts","sourceRoot":"","sources":["../../../src/core/drafts.ts"],"names":[],"mappings":"AAQA,iEAAiE;AACjE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IACzC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACvC,gFAAgF;IAChF,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,iFAAiF;AACjF,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,UAAU,CAgBb;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAQrD;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAOxD;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzC,UAAU,EAAE,CAcd;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,QAAQ,EAAE,SAAS,UAAU,EAAE,EAC/B,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAkBhD;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGlE;AAED,6DAA6D;AAC7D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAE5F;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAK5F"}
1
+ {"version":3,"file":"drafts.d.ts","sourceRoot":"","sources":["../../../src/core/drafts.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"efforts.d.ts","sourceRoot":"","sources":["../../../src/core/efforts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAElE,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAA;AAE5E,mEAAmE;AACnE,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;AAElD,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,KAAK,GAAG,SAAS,CAAA;AAExE,kDAAkD;AAClD,wBAAgB,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAM/F;AAED,wEAAwE;AACxE,wBAAgB,QAAQ,CACtB,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAMlB;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,OAAO,CAE7G;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CACzB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,EACpC,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,gBAAgB,CAKlB;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,EACpC,IAAI,EAAE,MAAM,GACX,gBAAgB,CAGlB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAwBxG;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI3F;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,SAAS,CAKtF;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,gBAAgB,GAAG,SAAS,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB"}
1
+ {"version":3,"file":"efforts.d.ts","sourceRoot":"","sources":["../../../src/core/efforts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAElE,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAA;AAE5E,mEAAmE;AACnE,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;AAElD,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,KAAK,GAAG,SAAS,CAAA;AAExE,kDAAkD;AAClD,wBAAgB,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAM/F;AAED,wEAAwE;AACxE,wBAAgB,QAAQ,CACtB,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,GACZ,gBAAgB,CASlB;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,OAAO,CAE7G;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CACzB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,EACpC,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,gBAAgB,CAKlB;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,EACpC,IAAI,EAAE,MAAM,GACX,gBAAgB,CAGlB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAwBxG;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI3F;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,SAAS,CAKtF;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,gBAAgB,GAAG,SAAS,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-effort-declare",
3
3
  "description": "Settings page to declare per-model reasoningEfforts and openai-completions compat on hand-declared llm-pi-ai routes. Writes the official namespace; does not intercept llm/stream or replace the Models page.",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "repository": {
package/src/README.en.md CHANGED
@@ -16,7 +16,7 @@ Install: [INSTALL.en.md](../INSTALL.en.md). Usage: root [README.en.md](../README
16
16
  | Path | Role |
17
17
  | --- | --- |
18
18
  | [`index.ts`](./index.ts) | Host entry. Exports `name` and `apply`. v1 registers no host services or adapters; the bundle still needs this entry so `dsh plugin add` can load the package. |
19
- | [`core/`](./core/) | UI-free pure functions: efforts, presets, save ops, draft merge, route filtering. Shared by the client and tests; the v1 host entry does not import them (`tsconfig.host` still compiles `core` for types). |
19
+ | [`core/`](./core/) | UI-free pure functions: efforts, presets, save ops, draft merge, route filtering, footer attribution. Shared by the client and tests; the v1 host entry does not import them (`tsconfig.host` still compiles `core` for types). |
20
20
  | [`client/`](./client/) | Browser half: locale, settings page, `settings.mutate` against `llm-pi-ai`. |
21
21
  | [`css-modules.d.ts`](./css-modules.d.ts) | Types for `*.module.css` (class map plus `cssText` / `cssTagId`). |
22
22
 
package/src/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  | 路径 | 说明 |
17
17
  | --- | --- |
18
18
  | [`index.ts`](./index.ts) | Host 入口。导出 `name` 与 `apply`。v1 不注册 host 服务、不挂适配器;组合包仍需要这一入口才能被 `dsh plugin add` 加载。 |
19
- | [`core/`](./core/) | 与 UI 无关的纯函数:档位、预设、保存 ops、草稿合并、路由过滤。client 与单测共用;Host 入口 v1 不引用这些函数(`tsconfig.host` 仍编译 `core` 以产出类型)。 |
19
+ | [`core/`](./core/) | 与 UI 无关的纯函数:档位、预设、保存 ops、草稿合并、路由过滤、页脚归因格式。client 与单测共用;Host 入口 v1 不引用这些函数(`tsconfig.host` 仍编译 `core` 以产出类型)。 |
20
20
  | [`client/`](./client/) | 浏览器半区:locale、设置页、对 `llm-pi-ai` 的 `settings.mutate`。 |
21
21
  | [`css-modules.d.ts`](./css-modules.d.ts) | `*.module.css` 模块类型(class map + `cssText` / `cssTagId`)。 |
22
22
 
@@ -30,18 +30,32 @@ import { applyPresetCompat, applyPresetEfforts, PRESETS, type PresetId } from '.
30
30
  import { buildSaveOps } from '../core/path-ops.ts'
31
31
  import { cloneModels, cloneObject } from '../core/paths.ts'
32
32
  import { modelEffortError } from '../core/validate.ts'
33
- import { loadDrafts } from './load-drafts.ts'
33
+ import { PLUGIN_FOOTER_TEXT } from './build-info.ts'
34
+ import {
35
+ foldReloadNotices,
36
+ isOwnDocumentEcho,
37
+ loadDrafts,
38
+ waitForNamespaceRevision,
39
+ waitForNamespaceRevisionChange,
40
+ type LoadDraftsMode,
41
+ } from './load-drafts.ts'
34
42
  import { validateSaveDraft, type SchemaOps } from './schema-ops.ts'
35
43
  import type { EffortDeclareKey } from './locales.ts'
36
44
  import css from './effort-declare.module.css'
37
45
 
38
- export type InvalidationSource = 'settings' | 'directory'
46
+ export type InvalidationSource = 'settings' | 'directory' | 'reset' | 'writable'
47
+
48
+ export type Invalidation =
49
+ | { source: 'writable' }
50
+ | { source: 'directory' }
51
+ | { source: 'reset' }
52
+ | { source: 'settings'; revision: number }
39
53
 
40
54
  export interface EffortDeclareSectionInjected {
41
55
  api: Pick<IApiClient, 'settings' | 'llm'>
42
56
  describe: SettingsDescribeFace
43
57
  schema: SchemaOps
44
- subscribeInvalidate: (listener: (source: InvalidationSource) => void) => () => void
58
+ subscribeInvalidate: (listener: (event: Invalidation) => void) => () => void
45
59
  }
46
60
 
47
61
  export interface EffortDeclareSectionProps extends Partial<EffortDeclareSectionInjected> {
@@ -165,18 +179,20 @@ function RouteCard(props: {
165
179
  draft: RouteDraft
166
180
  formats: readonly string[]
167
181
  writable: boolean
182
+ /** This card is the in-flight mutate — show “Saving…”. */
168
183
  busy: boolean
169
- reloading: boolean
184
+ /** Namespace-level lock: any mutate, read-only, or first load. */
185
+ saveLocked: boolean
170
186
  notice: CardNotice | undefined
171
187
  t: (key: EffortDeclareKey) => string
172
188
  onChange: (draft: RouteDraft) => void
173
189
  onSave: (draft: RouteDraft) => void
174
190
  onCancel: (draft: RouteDraft) => void
175
191
  }): ReactNode {
176
- const { draft, writable, busy, reloading, t, onChange } = props
192
+ const { draft, writable, busy, saveLocked, t, onChange } = props
177
193
  const noModels = draft.models.length === 0
178
194
  const editDisabled = !writable || busy || noModels
179
- const saveLocked = editDisabled || reloading
195
+ const saveDisabled = saveLocked || noModels
180
196
  const formats = thinkingFormatChoices(props.formats, draft.compat.thinkingFormat)
181
197
  const summary = compatSummary(draft.compat)
182
198
  const sameWire = draft.compat.supportsReasoningEffort === false
@@ -302,7 +318,7 @@ function RouteCard(props: {
302
318
  <button
303
319
  type="button"
304
320
  className={css.primaryButton}
305
- disabled={saveLocked || !dirty || clientError !== undefined}
321
+ disabled={saveDisabled || !dirty || clientError !== undefined}
306
322
  onClick={() => { props.onSave(draft) }}
307
323
  >
308
324
  {busy ? t('saving') : t('save')}
@@ -326,53 +342,133 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
326
342
  const [notices, setNotices] = useState<Record<string, CardNotice>>({})
327
343
  const generationRef = useRef(0)
328
344
  const draftsRef = useRef(drafts)
345
+ const echoedRevisionRef = useRef<number | undefined>(undefined)
346
+ const pendingRevisionRef = useRef<number | undefined>(undefined)
347
+ const busyRouteRef = useRef<string | null>(null)
348
+ const abortRef = useRef<AbortController | null>(null)
329
349
  draftsRef.current = drafts
330
350
 
331
- const reload = useCallback((preserveDirty: boolean): void => {
351
+ const applyDrafts = (next: RouteDraft[] | ((current: RouteDraft[]) => RouteDraft[])): void => {
352
+ const resolved = typeof next === 'function' ? next(draftsRef.current) : next
353
+ draftsRef.current = resolved
354
+ setDrafts(resolved)
355
+ }
356
+
357
+ const snapshotMode = (): LoadDraftsMode => (
358
+ describe === undefined || describe.getSnapshot().status === 'idle' ? 'ensure' : 'snapshot'
359
+ )
360
+
361
+ const beginGeneration = (): { generation: number; signal: AbortSignal } => {
362
+ abortRef.current?.abort()
363
+ const abort = new AbortController()
364
+ abortRef.current = abort
365
+ return { generation: nextGeneration(generationRef), signal: abort.signal }
366
+ }
367
+
368
+ const failGeneration = (generation: number, failure: unknown): void => {
369
+ if (!generationIsCurrent(generationRef, generation)) return
370
+ setStatus('error')
371
+ setError(failure instanceof Error ? failure.message : t('loadError'))
372
+ }
373
+
374
+ const settleReload = (
375
+ generation: number,
376
+ preserveDirty: boolean,
377
+ result: Awaited<ReturnType<typeof loadDrafts>>,
378
+ ): void => {
379
+ if (!generationIsCurrent(generationRef, generation)) return
380
+ setWritable(result.writable)
381
+ setFormats(result.formats)
382
+ if (result.error !== undefined) {
383
+ setStatus('error')
384
+ setError(result.error)
385
+ return
386
+ }
387
+ const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty })
388
+ applyDrafts(merged.drafts)
389
+ setNotices(current => foldReloadNotices(current, {
390
+ conflicted: merged.conflicted,
391
+ conflictNotice: { kind: 'conflict', text: t('dirtyConflict') },
392
+ liveProviders: merged.drafts.map(draft => draft.provider),
393
+ }))
394
+ setStatus('ready')
395
+ }
396
+
397
+ const loadSnapshotThenSettle = async (generation: number, preserveDirty: boolean): Promise<void> => {
398
+ if (api === undefined || describe === undefined || schema === undefined) return
399
+ if (!generationIsCurrent(generationRef, generation)) return
400
+ try {
401
+ const result = await loadDrafts(api, describe, schema, 'snapshot')
402
+ settleReload(generation, preserveDirty, result)
403
+ } catch (failure) {
404
+ failGeneration(generation, failure)
405
+ }
406
+ }
407
+
408
+ const reload = useCallback((preserveDirty: boolean, mode: LoadDraftsMode = 'ensure'): void => {
332
409
  if (api === undefined || describe === undefined || schema === undefined) {
333
410
  setStatus('error')
334
411
  setError(t('loadError'))
335
412
  return
336
413
  }
337
- const generation = nextGeneration(generationRef)
338
- setStatus('loading')
414
+ const { generation } = beginGeneration()
415
+ if (draftsRef.current.length === 0) setStatus('loading')
339
416
  setError('')
340
- void loadDrafts(api, describe, schema).then((result) => {
341
- if (!generationIsCurrent(generationRef, generation)) return
342
- setWritable(result.writable)
343
- setFormats(result.formats)
344
- if (result.error !== undefined) {
345
- setStatus('error')
346
- setError(result.error)
347
- return
348
- }
349
- const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty })
350
- setDrafts(merged.drafts)
351
- if (merged.conflicted.length > 0) {
352
- setNotices(current => {
353
- const next = { ...current }
354
- for (const provider of merged.conflicted) {
355
- next[provider] = { kind: 'conflict', text: t('dirtyConflict') }
356
- }
357
- return next
358
- })
359
- }
360
- setStatus('ready')
417
+ void loadDrafts(api, describe, schema, mode).then((result) => {
418
+ settleReload(generation, preserveDirty, result)
361
419
  }, (failure: unknown) => {
362
- if (!generationIsCurrent(generationRef, generation)) return
363
- setStatus('error')
364
- setError(failure instanceof Error ? failure.message : t('loadError'))
420
+ failGeneration(generation, failure)
365
421
  })
366
422
  }, [api, describe, schema, t])
367
423
 
368
- useEffect(() => { reload(false) }, [reload])
424
+ const refreshAtRevision = useCallback((revision: number, preserveDirty: boolean): void => {
425
+ if (api === undefined || describe === undefined || schema === undefined) return
426
+ const { generation, signal } = beginGeneration()
427
+ if (draftsRef.current.length === 0) setStatus('loading')
428
+ setError('')
429
+ void waitForNamespaceRevision(describe, LLM_PI_AI_NS, revision, signal).then((outcome) => {
430
+ if (outcome === 'aborted' || !generationIsCurrent(generationRef, generation)) return
431
+ return loadSnapshotThenSettle(generation, preserveDirty)
432
+ }, (failure: unknown) => {
433
+ failGeneration(generation, failure)
434
+ })
435
+ }, [api, describe, schema, t])
436
+
437
+ const flushPendingSettings = (refresh: (revision: number, preserveDirty: boolean) => void): void => {
438
+ const pending = pendingRevisionRef.current
439
+ pendingRevisionRef.current = undefined
440
+ if (pending === undefined) return
441
+ if (isOwnDocumentEcho(echoedRevisionRef.current, pending)) return
442
+ refresh(pending, true)
443
+ }
444
+
445
+ useEffect(() => { reload(false, 'ensure') }, [reload])
446
+ useEffect(() => () => {
447
+ abortRef.current?.abort()
448
+ nextGeneration(generationRef)
449
+ }, [])
369
450
 
370
451
  useEffect(() => {
371
452
  if (props.subscribeInvalidate === undefined) return undefined
372
- return props.subscribeInvalidate((source) => {
373
- if (source === 'settings' || source === 'directory') reload(true)
453
+ return props.subscribeInvalidate((event) => {
454
+ if (event.source === 'writable') {
455
+ const view = describe?.getSnapshot().view
456
+ if (view !== undefined) setWritable(view.writable)
457
+ return
458
+ }
459
+ if (event.source === 'settings') {
460
+ if (busyRouteRef.current !== null) {
461
+ pendingRevisionRef.current = event.revision
462
+ return
463
+ }
464
+ if (isOwnDocumentEcho(echoedRevisionRef.current, event.revision)) return
465
+ refreshAtRevision(event.revision, true)
466
+ return
467
+ }
468
+ if (event.source === 'directory') reload(true, snapshotMode())
469
+ if (event.source === 'reset') reload(true, 'ensure')
374
470
  })
375
- }, [props.subscribeInvalidate, reload])
471
+ }, [describe, props.subscribeInvalidate, refreshAtRevision, reload])
376
472
 
377
473
  const patchNotice = (provider: string, notice: CardNotice | undefined): void => {
378
474
  setNotices(current => {
@@ -385,7 +481,10 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
385
481
 
386
482
  const save = async (draft: RouteDraft): Promise<void> => {
387
483
  if (api === undefined || describe === undefined || schema === undefined) return
388
- if (status === 'loading' || busyRoute !== null) return
484
+ if (status === 'loading' || busyRouteRef.current !== null) {
485
+ patchNotice(draft.provider, { kind: 'error', text: t('saveBusy') })
486
+ return
487
+ }
389
488
  const blocking = draft.models
390
489
  .map(row => errorText(modelEffortError(row), t))
391
490
  .find(text => text !== undefined)
@@ -393,6 +492,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
393
492
  patchNotice(draft.provider, { kind: 'error', text: blocking })
394
493
  return
395
494
  }
495
+ busyRouteRef.current = draft.provider
396
496
  setBusyRoute(draft.provider)
397
497
  patchNotice(draft.provider, undefined)
398
498
  try {
@@ -404,7 +504,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
404
504
  afterCompat: draft.compat,
405
505
  })
406
506
  if (ops.length === 0) {
407
- setDrafts(current => current.map(row => row.provider === draft.provider ? alignDraft(row) : row))
507
+ applyDrafts(current => current.map(row => row.provider === draft.provider ? alignDraft(row) : row))
408
508
  return
409
509
  }
410
510
  const willWriteCompat = ops.some(op => (
@@ -444,12 +544,21 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
444
544
  kind: conflict ? 'conflict' : 'error',
445
545
  text: conflict ? t('conflict') : response.result.error.message,
446
546
  })
447
- if (conflict) reload(true)
547
+ if (conflict) {
548
+ const { generation, signal } = beginGeneration()
549
+ void waitForNamespaceRevisionChange(describe, LLM_PI_AI_NS, draft.revision, signal).then((outcome) => {
550
+ if (outcome === 'aborted' || !generationIsCurrent(generationRef, generation)) return
551
+ return loadSnapshotThenSettle(generation, true)
552
+ }, (failure: unknown) => {
553
+ failGeneration(generation, failure)
554
+ })
555
+ }
448
556
  return
449
557
  }
450
558
  const view = response.result.value
559
+ echoedRevisionRef.current = view.revision
451
560
  describe.acceptView(view)
452
- setDrafts(current => applySaveSuccess(current, draft.provider, {
561
+ applyDrafts(applySaveSuccess(draftsRef.current, draft.provider, {
453
562
  user: view.user ?? {},
454
563
  revision: view.revision,
455
564
  }))
@@ -460,7 +569,9 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
460
569
  text: failure instanceof Error ? failure.message : t('loadError'),
461
570
  })
462
571
  } finally {
572
+ busyRouteRef.current = null
463
573
  setBusyRoute(null)
574
+ flushPendingSettings(refreshAtRevision)
464
575
  }
465
576
  }
466
577
 
@@ -481,7 +592,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
481
592
  {status === 'error' ? <p className={css.error}>{error}</p> : null}
482
593
  {showReload
483
594
  ? (
484
- <button type="button" className={css.secondaryButton} onClick={() => { reload(true) }}>{t('reload')}</button>
595
+ <button type="button" className={css.secondaryButton} onClick={() => { reload(true, snapshotMode()) }}>{t('reload')}</button>
485
596
  )
486
597
  : null}
487
598
  {showEmpty
@@ -502,17 +613,17 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
502
613
  formats={formats.length > 0 ? formats : []}
503
614
  writable={writable}
504
615
  busy={busyRoute === draft.provider}
505
- reloading={status === 'loading'}
616
+ saveLocked={!writable || busyRoute !== null || status === 'loading'}
506
617
  notice={notices[draft.provider]}
507
618
  t={t}
508
619
  onChange={(next) => {
509
620
  patchNotice(next.provider, undefined)
510
- setDrafts(current => current.map(row => row.provider === next.provider ? next : row))
621
+ applyDrafts(current => current.map(row => row.provider === next.provider ? next : row))
511
622
  }}
512
623
  onSave={(next) => { void save(next) }}
513
624
  onCancel={(next) => {
514
625
  patchNotice(next.provider, undefined)
515
- setDrafts(current => current.map(row => row.provider === next.provider
626
+ applyDrafts(current => current.map(row => row.provider === next.provider
516
627
  ? {
517
628
  ...row,
518
629
  models: cloneModels(row.originalModels),
@@ -525,6 +636,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
525
636
  </ul>
526
637
  )
527
638
  : null}
639
+ <p className={css.footer}>{PLUGIN_FOOTER_TEXT}</p>
528
640
  </div>
529
641
  )
530
642
  }
@@ -17,9 +17,11 @@ Cross-plugin work uses Cordis services only (`connection`, `settingsScope`, `set
17
17
 
18
18
  | File | Role |
19
19
  | --- | --- |
20
- | [`index.ts`](./index.ts) | Registers zh/en copy, CSS (`ctx.effect` insert/remove), and `settings.section` (id `effort-declare`, order 12). Subscribes in `apply` to the mirror `describe.subscribe`, `settings/document-updated` (`llm-pi-ai` only), `llm/adapters-updated`, and `connection/reset`. `locale: NS` lets the framework inject `t`; `inject` only returns `api` / `describe` / `schema` / `subscribeInvalidate`. |
21
- | [`EffortDeclareSection.tsx`](./EffortDeclareSection.tsx) | Route cards, presets, Off tri-state, advanced protocol switches, save and cancel. |
22
- | [`load-drafts.ts`](./load-drafts.ts) | Builds drafts from `llm.providers` + the settings mirror; drafts come from `user`, protocol classification may use `value`. `thinkingFormat` choices come only from the live schema union. |
20
+ | [`index.ts`](./index.ts) | Registers zh/en copy, CSS (`ctx.effect` insert/remove), and `settings.section` (id `effort-declare`, order 12). `describe.subscribe` only syncs `writable` (it fires for every namespace, including this page’s own `acceptView`). `settings/document-updated` (`llm-pi-ai` only) carries the Host revision; the section skips its own mutate echo, waits until the mirror revision has **caught up**, then `getSnapshot()` — it does not use `ensure()` as refresh. Provider directory changes come from `llm/adapters-updated`; `connection/reset` uses `ensure()` so an in-flight official `load()` is awaited. `locale: NS` lets the framework inject `t`; `inject` only returns `api` / `describe` / `schema` / `subscribeInvalidate`. |
21
+ | [`EffortDeclareSection.tsx`](./EffortDeclareSection.tsx) | Route cards, presets, Off tri-state, advanced protocol switches, save and cancel. Save is exclusive for the whole `llm-pi-ai` namespace (ref-guarded against double-click); `busy` is “Saving…” on the in-flight card, `saveLocked` disables the others. A successful write syncs `draftsRef` immediately and folds `acceptView` + `applySaveSuccess`. External `document-updated` events that arrive during save are replayed afterwards. A reload with no conflict clears that card’s conflict/error notice. Footer shows the version and copyright years frozen at pack time. |
22
+ | [`build-info.ts`](./build-info.ts) | Reads `package.json` version and UTC pack year injected by tsdown `define`, and formats the footer line. |
23
+ | [`globals.d.ts`](./globals.d.ts) | Types for `__PLUGIN_VERSION__` and `__COPYRIGHT_TO__`. |
24
+ | [`load-drafts.ts`](./load-drafts.ts) | Builds drafts from `llm.providers` + the settings mirror; drafts come from `user`, protocol classification may use `value`. `thinkingFormat` choices come only from the live schema union. First paint uses `ensure()`; refresh reads `getSnapshot()` only, with helpers that wait for the namespace revision. |
23
25
  | [`locales.ts`](./locales.ts) | Copy namespace `plugin-effort-declare`. |
24
26
  | [`effort-declare.module.css`](./effort-declare.module.css) | `--dsw-alias-*` tokens only, so dark theme stays correct. |
25
27
  | [`schema-ops.ts`](./schema-ops.ts) | Binds `settingsSchema` as plain callbacks (including `validate`) so the service identity is not passed into React. |
@@ -17,9 +17,11 @@
17
17
 
18
18
  | 文件 | 说明 |
19
19
  | --- | --- |
20
- | [`index.ts`](./index.ts) | 注册 zh/en 文案、CSS(`ctx.effect` 插入/移除)与 `settings.section`(id `effort-declare`,order 12)。在 `apply` 里订阅镜像 `describe.subscribe`、`settings/document-updated`(仅 `llm-pi-ai`)、`llm/adapters-updated`、`connection/reset`。`locale: NS` 由框架注入 `t`,inject 只传 `api` / `describe` / `schema` / `subscribeInvalidate`。 |
21
- | [`EffortDeclareSection.tsx`](./EffortDeclareSection.tsx) | 路由卡片、预设、Off 三态、高级协议开关、保存与取消。 |
22
- | [`load-drafts.ts`](./load-drafts.ts) | `llm.providers` + 镜像组装草稿;草稿取自 `user`,协议分类可用 `value`。`thinkingFormat` 可选项只来自现场 schema union。 |
20
+ | [`index.ts`](./index.ts) | 注册 zh/en 文案、CSS(`ctx.effect` 插入/移除)与 `settings.section`(id `effort-declare`,order 12)。`describe.subscribe` 只同步 `writable`(任意 namespace 都会触发,含自己的 `acceptView`)。`settings/document-updated`(仅 `llm-pi-ai`)带上 Host 的 revision;设置页跳过自己这次 mutate 的回声,等镜像 revision **追上**后再 `getSnapshot()`,不把 `ensure()` 当刷新。提供方目录来自 `llm/adapters-updated`;`connection/reset` 走 `ensure()` 以便等待官方镜像的 in-flight `load()`。`locale: NS` 由框架注入 `t`,inject 只传 `api` / `describe` / `schema` / `subscribeInvalidate`。 |
21
+ | [`EffortDeclareSection.tsx`](./EffortDeclareSection.tsx) | 路由卡片、预设、Off 三态、高级协议开关、保存与取消。保存互斥到整个 `llm-pi-ai` namespace(以 ref 防连点);`busy` 只给正在 mutate 的卡显示「保存中」,其它卡 `saveLocked`。自己写入成功立刻同步 `draftsRef` 并走 `acceptView` + `applySaveSuccess`。保存过程中到达的外部 `document-updated` 会在结束后补刷。无冲突的 reload 清掉该卡 conflict/error notice。页脚显示打包时冻结的版本与版权年。 |
22
+ | [`build-info.ts`](./build-info.ts) | 读取 tsdown `define` 注入的 `package.json` version UTC 打包年,拼出页脚文案。 |
23
+ | [`globals.d.ts`](./globals.d.ts) | `__PLUGIN_VERSION__` / `__COPYRIGHT_TO__` 的类型声明。 |
24
+ | [`load-drafts.ts`](./load-drafts.ts) | 从 `llm.providers` + 镜像组装草稿;草稿取自 `user`,协议分类可用 `value`。`thinkingFormat` 可选项只来自现场 schema union。首屏 `ensure()`;刷新只读 `getSnapshot()`,并提供等 namespace revision 的 helper。 |
23
25
  | [`locales.ts`](./locales.ts) | 文案 namespace `plugin-effort-declare`。 |
24
26
  | [`effort-declare.module.css`](./effort-declare.module.css) | 仅使用 `--dsw-alias-*` 设计令牌,保证暗色主题正确。 |
25
27
  | [`schema-ops.ts`](./schema-ops.ts) | 将 `settingsSchema` 收成普通回调(含 `validate`),避免把服务身份带进 React。 |
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Footer line frozen into the client bundle. Do not read the clock or
3
+ * package.json at DSH startup — host apply is empty and the settings page
4
+ * runs in the browser.
5
+ */
6
+ import { COPYRIGHT_FROM, formatAttribution } from '../core/attribution.ts'
7
+
8
+ export const PLUGIN_FOOTER_TEXT = formatAttribution(
9
+ __PLUGIN_VERSION__,
10
+ COPYRIGHT_FROM,
11
+ __COPYRIGHT_TO__,
12
+ )
@@ -276,3 +276,11 @@
276
276
  font-size: 12px;
277
277
  line-height: 18px;
278
278
  }
279
+
280
+ .footer {
281
+ margin: 16px 0 0;
282
+ font-size: 11px;
283
+ line-height: 16px;
284
+ color: var(--dsw-alias-label-tertiary);
285
+ opacity: 0.7;
286
+ }
@@ -0,0 +1,5 @@
1
+ /** `package.json` version, substituted by tsdown `define` at pack time. */
2
+ declare const __PLUGIN_VERSION__: string
3
+
4
+ /** UTC full year at pack time, substituted by tsdown `define`. */
5
+ declare const __COPYRIGHT_TO__: number
@@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
8
8
  import type {} from '@deepseek-ai/dsh-client-locale/client'
9
9
  import type {} from '@deepseek-ai/dsh-api-remotes/client'
10
10
  import { EffortDeclareSection } from './EffortDeclareSection.tsx'
11
- import type { EffortDeclareSectionInjected, InvalidationSource } from './EffortDeclareSection.tsx'
11
+ import type { EffortDeclareSectionInjected, Invalidation } from './EffortDeclareSection.tsx'
12
12
  import { bindSchema } from './schema-ops.ts'
13
13
  import { LLM_PI_AI_NS } from '../core/catalog.ts'
14
14
  import { NS, en, zh, type EffortDeclareKey } from './locales.ts'
@@ -60,20 +60,25 @@ export function apply(ctx: ClientContext): void {
60
60
  })
61
61
  const t = ctx.locale.bind(NS)
62
62
  const describe = ctx.settingsScope.describe()
63
- const invalidation = new Set<(source: InvalidationSource) => void>()
63
+ const invalidation = new Set<(event: Invalidation) => void>()
64
64
 
65
65
  ctx.effect(() => {
66
- const emit = (source: InvalidationSource) => {
67
- for (const listener of invalidation) listener(source)
66
+ const emit = (event: Invalidation) => {
67
+ for (const listener of invalidation) listener(event)
68
68
  }
69
+ // Mirror subscribe fires on every namespace (theme, locale, own acceptView).
70
+ // Only fold writable from that. llm-pi-ai document changes come from
71
+ // settings/document-updated; the section waits until the mirror revision
72
+ // has caught up instead of calling ensure(), and skips the echo of its
73
+ // own mutate. connection/reset uses ensure() so an in-flight load is awaited.
69
74
  const disposers = [
70
- describe.subscribe(() => { emit('settings') }),
71
- ctx.remote.$on('settings/document-updated', (ns: string) => {
75
+ describe.subscribe(() => { emit({ source: 'writable' }) }),
76
+ ctx.remote.$on('settings/document-updated', (ns: string, revision: number) => {
72
77
  if (ns !== LLM_PI_AI_NS) return
73
- emit('settings')
78
+ emit({ source: 'settings', revision })
74
79
  }),
75
- ctx.remote.$on('llm/adapters-updated', () => { emit('directory') }),
76
- ctx.on('connection/reset', () => { emit('directory') }),
80
+ ctx.remote.$on('llm/adapters-updated', () => { emit({ source: 'directory' }) }),
81
+ ctx.on('connection/reset', () => { emit({ source: 'reset' }) }),
77
82
  ]
78
83
  return () => {
79
84
  for (const dispose of disposers) dispose()