dsh-plugin-effort-declare 0.1.2 → 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.
- package/CONTRIBUTING.en.md +2 -0
- package/CONTRIBUTING.md +2 -0
- package/INSTALL.en.md +1 -1
- package/INSTALL.md +1 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/client.js +242 -53
- package/lib/client.js.map +1 -1
- package/lib/types/client/EffortDeclareSection.d.ts +12 -2
- package/lib/types/client/EffortDeclareSection.d.ts.map +1 -1
- package/lib/types/client/build-info.d.ts +2 -0
- package/lib/types/client/build-info.d.ts.map +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/load-drafts.d.ts +36 -6
- package/lib/types/client/load-drafts.d.ts.map +1 -1
- package/lib/types/core/attribution.d.ts +13 -0
- package/lib/types/core/attribution.d.ts.map +1 -0
- package/lib/types/core/efforts.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/README.en.md +1 -1
- package/src/README.md +1 -1
- package/src/client/EffortDeclareSection.tsx +143 -41
- package/src/client/README.en.md +5 -3
- package/src/client/README.md +5 -3
- package/src/client/build-info.ts +12 -0
- package/src/client/effort-declare.module.css +8 -0
- package/src/client/globals.d.ts +5 -0
- package/src/client/index.ts +13 -11
- package/src/client/load-drafts.ts +124 -8
- package/src/core/README.en.md +2 -1
- package/src/core/README.md +2 -1
- package/src/core/attribution.ts +24 -0
- package/src/core/efforts.ts +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-drafts.d.ts","sourceRoot":"","sources":["../../../src/client/load-drafts.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"load-drafts.d.ts","sourceRoot":"","sources":["../../../src/client/load-drafts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAyB,MAAM,qCAAqC,CAAA;AAC5F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4CAA4C,CAAA;AAKtF,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAG9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAOhD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,UAAU,CAAA;AAElD,KAAK,cAAc,GAAG,UAAU,CAAC,oBAAoB,CAAC,aAAa,CAAC,CAAC,CAAA;AACrE,KAAK,cAAc,GAAG,IAAI,CAAC,oBAAoB,EAAE,aAAa,GAAG,WAAW,CAAC,CAAA;AAE7E,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1F;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEvF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAC1D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAC1B,IAAI,EAAE;IACJ,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,cAAc,EAAE,CAAC,CAAA;IACjB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;CACjC,GACA,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAcnB;AA4BD,qFAAqF;AACrF,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,cAAc,EACxB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAUhC;AAED,6EAA6E;AAC7E,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,cAAc,EACxB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAMhC;AAqDD;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,EAC5B,QAAQ,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,aAAa,CAAC,EAC9D,MAAM,EAAE,SAAS,EACjB,IAAI,GAAE,cAAyB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAG3B"}
|
|
@@ -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"}
|
|
@@ -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,
|
|
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.
|
|
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
|
|
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 {
|
|
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' | 'writable'
|
|
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: (
|
|
58
|
+
subscribeInvalidate: (listener: (event: Invalidation) => void) => () => void
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
export interface EffortDeclareSectionProps extends Partial<EffortDeclareSectionInjected> {
|
|
@@ -328,58 +342,133 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
328
342
|
const [notices, setNotices] = useState<Record<string, CardNotice>>({})
|
|
329
343
|
const generationRef = useRef(0)
|
|
330
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)
|
|
331
349
|
draftsRef.current = drafts
|
|
332
350
|
|
|
333
|
-
const
|
|
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 => {
|
|
334
409
|
if (api === undefined || describe === undefined || schema === undefined) {
|
|
335
410
|
setStatus('error')
|
|
336
411
|
setError(t('loadError'))
|
|
337
412
|
return
|
|
338
413
|
}
|
|
339
|
-
const generation =
|
|
414
|
+
const { generation } = beginGeneration()
|
|
340
415
|
if (draftsRef.current.length === 0) setStatus('loading')
|
|
341
416
|
setError('')
|
|
342
|
-
void loadDrafts(api, describe, schema).then((result) => {
|
|
343
|
-
|
|
344
|
-
setWritable(result.writable)
|
|
345
|
-
setFormats(result.formats)
|
|
346
|
-
if (result.error !== undefined) {
|
|
347
|
-
setStatus('error')
|
|
348
|
-
setError(result.error)
|
|
349
|
-
return
|
|
350
|
-
}
|
|
351
|
-
const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty })
|
|
352
|
-
setDrafts(merged.drafts)
|
|
353
|
-
if (merged.conflicted.length > 0) {
|
|
354
|
-
setNotices(current => {
|
|
355
|
-
const next = { ...current }
|
|
356
|
-
for (const provider of merged.conflicted) {
|
|
357
|
-
next[provider] = { kind: 'conflict', text: t('dirtyConflict') }
|
|
358
|
-
}
|
|
359
|
-
return next
|
|
360
|
-
})
|
|
361
|
-
}
|
|
362
|
-
setStatus('ready')
|
|
417
|
+
void loadDrafts(api, describe, schema, mode).then((result) => {
|
|
418
|
+
settleReload(generation, preserveDirty, result)
|
|
363
419
|
}, (failure: unknown) => {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
420
|
+
failGeneration(generation, failure)
|
|
421
|
+
})
|
|
422
|
+
}, [api, describe, schema, t])
|
|
423
|
+
|
|
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)
|
|
367
434
|
})
|
|
368
435
|
}, [api, describe, schema, t])
|
|
369
436
|
|
|
370
|
-
|
|
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
|
+
}, [])
|
|
371
450
|
|
|
372
451
|
useEffect(() => {
|
|
373
452
|
if (props.subscribeInvalidate === undefined) return undefined
|
|
374
|
-
return props.subscribeInvalidate((
|
|
375
|
-
if (source === 'writable') {
|
|
453
|
+
return props.subscribeInvalidate((event) => {
|
|
454
|
+
if (event.source === 'writable') {
|
|
376
455
|
const view = describe?.getSnapshot().view
|
|
377
456
|
if (view !== undefined) setWritable(view.writable)
|
|
378
457
|
return
|
|
379
458
|
}
|
|
380
|
-
if (source === 'settings'
|
|
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')
|
|
381
470
|
})
|
|
382
|
-
}, [describe, props.subscribeInvalidate, reload])
|
|
471
|
+
}, [describe, props.subscribeInvalidate, refreshAtRevision, reload])
|
|
383
472
|
|
|
384
473
|
const patchNotice = (provider: string, notice: CardNotice | undefined): void => {
|
|
385
474
|
setNotices(current => {
|
|
@@ -392,7 +481,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
392
481
|
|
|
393
482
|
const save = async (draft: RouteDraft): Promise<void> => {
|
|
394
483
|
if (api === undefined || describe === undefined || schema === undefined) return
|
|
395
|
-
if (status === 'loading' ||
|
|
484
|
+
if (status === 'loading' || busyRouteRef.current !== null) {
|
|
396
485
|
patchNotice(draft.provider, { kind: 'error', text: t('saveBusy') })
|
|
397
486
|
return
|
|
398
487
|
}
|
|
@@ -403,6 +492,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
403
492
|
patchNotice(draft.provider, { kind: 'error', text: blocking })
|
|
404
493
|
return
|
|
405
494
|
}
|
|
495
|
+
busyRouteRef.current = draft.provider
|
|
406
496
|
setBusyRoute(draft.provider)
|
|
407
497
|
patchNotice(draft.provider, undefined)
|
|
408
498
|
try {
|
|
@@ -414,7 +504,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
414
504
|
afterCompat: draft.compat,
|
|
415
505
|
})
|
|
416
506
|
if (ops.length === 0) {
|
|
417
|
-
|
|
507
|
+
applyDrafts(current => current.map(row => row.provider === draft.provider ? alignDraft(row) : row))
|
|
418
508
|
return
|
|
419
509
|
}
|
|
420
510
|
const willWriteCompat = ops.some(op => (
|
|
@@ -454,12 +544,21 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
454
544
|
kind: conflict ? 'conflict' : 'error',
|
|
455
545
|
text: conflict ? t('conflict') : response.result.error.message,
|
|
456
546
|
})
|
|
457
|
-
if (conflict)
|
|
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
|
+
}
|
|
458
556
|
return
|
|
459
557
|
}
|
|
460
558
|
const view = response.result.value
|
|
559
|
+
echoedRevisionRef.current = view.revision
|
|
461
560
|
describe.acceptView(view)
|
|
462
|
-
|
|
561
|
+
applyDrafts(applySaveSuccess(draftsRef.current, draft.provider, {
|
|
463
562
|
user: view.user ?? {},
|
|
464
563
|
revision: view.revision,
|
|
465
564
|
}))
|
|
@@ -470,7 +569,9 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
470
569
|
text: failure instanceof Error ? failure.message : t('loadError'),
|
|
471
570
|
})
|
|
472
571
|
} finally {
|
|
572
|
+
busyRouteRef.current = null
|
|
473
573
|
setBusyRoute(null)
|
|
574
|
+
flushPendingSettings(refreshAtRevision)
|
|
474
575
|
}
|
|
475
576
|
}
|
|
476
577
|
|
|
@@ -491,7 +592,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
491
592
|
{status === 'error' ? <p className={css.error}>{error}</p> : null}
|
|
492
593
|
{showReload
|
|
493
594
|
? (
|
|
494
|
-
<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>
|
|
495
596
|
)
|
|
496
597
|
: null}
|
|
497
598
|
{showEmpty
|
|
@@ -517,12 +618,12 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
517
618
|
t={t}
|
|
518
619
|
onChange={(next) => {
|
|
519
620
|
patchNotice(next.provider, undefined)
|
|
520
|
-
|
|
621
|
+
applyDrafts(current => current.map(row => row.provider === next.provider ? next : row))
|
|
521
622
|
}}
|
|
522
623
|
onSave={(next) => { void save(next) }}
|
|
523
624
|
onCancel={(next) => {
|
|
524
625
|
patchNotice(next.provider, undefined)
|
|
525
|
-
|
|
626
|
+
applyDrafts(current => current.map(row => row.provider === next.provider
|
|
526
627
|
? {
|
|
527
628
|
...row,
|
|
528
629
|
models: cloneModels(row.originalModels),
|
|
@@ -535,6 +636,7 @@ export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNod
|
|
|
535
636
|
</ul>
|
|
536
637
|
)
|
|
537
638
|
: null}
|
|
639
|
+
<p className={css.footer}>{PLUGIN_FOOTER_TEXT}</p>
|
|
538
640
|
</div>
|
|
539
641
|
)
|
|
540
642
|
}
|
package/src/client/README.en.md
CHANGED
|
@@ -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). `describe.subscribe` only syncs `writable` (it fires for every namespace, including this page’s own `acceptView`).
|
|
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; `busy` is “Saving…” on the in-flight card, `saveLocked` disables the others. A successful write folds `acceptView` + `applySaveSuccess`
|
|
22
|
-
| [`
|
|
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. |
|
package/src/client/README.md
CHANGED
|
@@ -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)。`describe.subscribe` 只同步 `writable`(任意 namespace 都会触发,含自己的 `acceptView
|
|
21
|
-
| [`EffortDeclareSection.tsx`](./EffortDeclareSection.tsx) | 路由卡片、预设、Off 三态、高级协议开关、保存与取消。保存互斥到整个 `llm-pi-ai` namespace
|
|
22
|
-
| [`
|
|
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
|
+
)
|
package/src/client/index.ts
CHANGED
|
@@ -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,
|
|
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,23 +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<(
|
|
63
|
+
const invalidation = new Set<(event: Invalidation) => void>()
|
|
64
64
|
|
|
65
65
|
ctx.effect(() => {
|
|
66
|
-
const emit = (
|
|
67
|
-
for (const listener of invalidation) listener(
|
|
66
|
+
const emit = (event: Invalidation) => {
|
|
67
|
+
for (const listener of invalidation) listener(event)
|
|
68
68
|
}
|
|
69
69
|
// Mirror subscribe fires on every namespace (theme, locale, own acceptView).
|
|
70
|
-
// Only fold writable from that
|
|
71
|
-
//
|
|
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.
|
|
72
74
|
const disposers = [
|
|
73
|
-
describe.subscribe(() => { emit('writable') }),
|
|
74
|
-
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) => {
|
|
75
77
|
if (ns !== LLM_PI_AI_NS) return
|
|
76
|
-
emit('settings')
|
|
78
|
+
emit({ source: 'settings', revision })
|
|
77
79
|
}),
|
|
78
|
-
ctx.remote.$on('llm/adapters-updated', () => { emit('directory') }),
|
|
79
|
-
ctx.on('connection/reset', () => { emit('
|
|
80
|
+
ctx.remote.$on('llm/adapters-updated', () => { emit({ source: 'directory' }) }),
|
|
81
|
+
ctx.on('connection/reset', () => { emit({ source: 'reset' }) }),
|
|
80
82
|
]
|
|
81
83
|
return () => {
|
|
82
84
|
for (const dispose of disposers) dispose()
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Load editable route drafts from llm.providers + the llm-pi-ai namespace.
|
|
3
3
|
* Drafts come from the user layer; route protocol classification may use effective value.
|
|
4
|
+
*
|
|
5
|
+
* First paint uses `ensure()` (idle-only). Refresh never calls `ensure()`:
|
|
6
|
+
* wait until the mirror subscribe shows a namespace revision at least as new
|
|
7
|
+
* as the Host event, then `getSnapshot()`. Own mutate echoes are identified
|
|
8
|
+
* by revision (including older delayed echoes), not ignored as a generic
|
|
9
|
+
* document-updated.
|
|
4
10
|
*/
|
|
5
11
|
import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
|
6
12
|
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
@@ -25,19 +31,115 @@ export interface LoadDraftsResult {
|
|
|
25
31
|
error?: string
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
export type LoadDraftsMode = 'ensure' | 'snapshot'
|
|
35
|
+
|
|
36
|
+
type MirrorSnapshot = ReturnType<SettingsDescribeFace['getSnapshot']>
|
|
37
|
+
type MirrorDescribe = Pick<SettingsDescribeFace, 'getSnapshot' | 'subscribe'>
|
|
38
|
+
|
|
39
|
+
/** Namespace revision on a describe snapshot, if that row exists. */
|
|
40
|
+
export function namespaceRevision(snapshot: MirrorSnapshot, ns: string): number | undefined {
|
|
41
|
+
return snapshot.view?.namespaces.find(view => view.ns === ns)?.revision
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `formats` is the live schema union only. Empty means the dropdown has no
|
|
33
|
-
* writable choices (stored values stay visible via `thinkingFormatChoices`).
|
|
45
|
+
* True when `incoming` is the Host echo of a mutate this page already folded,
|
|
46
|
+
* or an older revision the snapshot has already passed. `echoed` is undefined
|
|
47
|
+
* until the first successful write.
|
|
34
48
|
*/
|
|
35
|
-
export
|
|
49
|
+
export function isOwnDocumentEcho(echoed: number | undefined, incoming: number): boolean {
|
|
50
|
+
return echoed !== undefined && incoming <= echoed
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* After a preserve-dirty reload: conflicted cards get a conflict notice;
|
|
55
|
+
* live cards drop leftover conflict/error; saved notices stay; gone cards drop.
|
|
56
|
+
*/
|
|
57
|
+
export function foldReloadNotices<T extends { kind: string }>(
|
|
58
|
+
current: Record<string, T>,
|
|
59
|
+
args: {
|
|
60
|
+
conflicted: readonly string[]
|
|
61
|
+
conflictNotice: T
|
|
62
|
+
liveProviders: readonly string[]
|
|
63
|
+
},
|
|
64
|
+
): Record<string, T> {
|
|
65
|
+
const live = new Set(args.liveProviders)
|
|
66
|
+
const conflicted = new Set(args.conflicted)
|
|
67
|
+
const next: Record<string, T> = {}
|
|
68
|
+
for (const [provider, notice] of Object.entries(current)) {
|
|
69
|
+
if (!live.has(provider)) continue
|
|
70
|
+
if (conflicted.has(provider)) continue
|
|
71
|
+
if (notice.kind === 'conflict' || notice.kind === 'error') continue
|
|
72
|
+
next[provider] = notice
|
|
73
|
+
}
|
|
74
|
+
for (const provider of args.conflicted) {
|
|
75
|
+
next[provider] = args.conflictNotice
|
|
76
|
+
}
|
|
77
|
+
return next
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function waitUntil(
|
|
81
|
+
describe: MirrorDescribe,
|
|
82
|
+
predicate: () => boolean,
|
|
83
|
+
signal?: AbortSignal,
|
|
84
|
+
): Promise<boolean> {
|
|
85
|
+
if (signal?.aborted) return Promise.resolve(false)
|
|
86
|
+
if (predicate()) return Promise.resolve(true)
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
let settled = false
|
|
89
|
+
const finish = (ok: boolean) => {
|
|
90
|
+
if (settled) return
|
|
91
|
+
settled = true
|
|
92
|
+
stop()
|
|
93
|
+
signal?.removeEventListener('abort', onAbort)
|
|
94
|
+
resolve(ok)
|
|
95
|
+
}
|
|
96
|
+
const onAbort = () => { finish(false) }
|
|
97
|
+
const stop = describe.subscribe(() => {
|
|
98
|
+
if (predicate()) finish(true)
|
|
99
|
+
})
|
|
100
|
+
signal?.addEventListener('abort', onAbort)
|
|
101
|
+
if (predicate()) finish(true)
|
|
102
|
+
else if (signal?.aborted) finish(false)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Resolve when the mirror's namespace revision is at least `revision`, or abort. */
|
|
107
|
+
export async function waitForNamespaceRevision(
|
|
108
|
+
describe: MirrorDescribe,
|
|
109
|
+
ns: string,
|
|
110
|
+
revision: number,
|
|
111
|
+
signal?: AbortSignal,
|
|
112
|
+
): Promise<'matched' | 'aborted'> {
|
|
113
|
+
const matched = await waitUntil(
|
|
114
|
+
describe,
|
|
115
|
+
() => {
|
|
116
|
+
const current = namespaceRevision(describe.getSnapshot(), ns)
|
|
117
|
+
return current !== undefined && current >= revision
|
|
118
|
+
},
|
|
119
|
+
signal,
|
|
120
|
+
)
|
|
121
|
+
return matched ? 'matched' : 'aborted'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Resolve when the namespace revision differs from `previous`, or abort. */
|
|
125
|
+
export async function waitForNamespaceRevisionChange(
|
|
126
|
+
describe: MirrorDescribe,
|
|
127
|
+
ns: string,
|
|
128
|
+
previous: number,
|
|
129
|
+
signal?: AbortSignal,
|
|
130
|
+
): Promise<'changed' | 'aborted'> {
|
|
131
|
+
const changed = await waitUntil(describe, () => {
|
|
132
|
+
const current = namespaceRevision(describe.getSnapshot(), ns)
|
|
133
|
+
return current !== undefined && current !== previous
|
|
134
|
+
}, signal)
|
|
135
|
+
return changed ? 'changed' : 'aborted'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function assembleDrafts(
|
|
36
139
|
api: Pick<IApiClient, 'llm'>,
|
|
37
|
-
describe: Pick<SettingsDescribeFace, '
|
|
140
|
+
describe: Pick<SettingsDescribeFace, 'getSnapshot'>,
|
|
38
141
|
schema: SchemaOps,
|
|
39
142
|
): Promise<LoadDraftsResult> {
|
|
40
|
-
await describe.ensure()
|
|
41
143
|
const mirrored = describe.getSnapshot()
|
|
42
144
|
if (mirrored.view === undefined) {
|
|
43
145
|
return { writable: false, formats: [], drafts: [], error: mirrored.error ?? undefined }
|
|
@@ -83,3 +185,17 @@ export async function loadDrafts(
|
|
|
83
185
|
}
|
|
84
186
|
return { writable: mirrored.view.writable, formats, drafts }
|
|
85
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `ensure`: first paint / idle recovery (official ensure only reads from idle).
|
|
191
|
+
* `snapshot`: refresh after the mirror revision already moved — do not ensure.
|
|
192
|
+
*/
|
|
193
|
+
export async function loadDrafts(
|
|
194
|
+
api: Pick<IApiClient, 'llm'>,
|
|
195
|
+
describe: Pick<SettingsDescribeFace, 'ensure' | 'getSnapshot'>,
|
|
196
|
+
schema: SchemaOps,
|
|
197
|
+
mode: LoadDraftsMode = 'ensure',
|
|
198
|
+
): Promise<LoadDraftsResult> {
|
|
199
|
+
if (mode === 'ensure') await describe.ensure()
|
|
200
|
+
return assembleDrafts(api, describe, schema)
|
|
201
|
+
}
|