@khorsheed/dsh-local-agent 0.1.0-rc.6
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 +181 -0
- package/README.i18n.yaml +2 -0
- package/README.md +181 -0
- package/cordis.patch.yml +21 -0
- package/lib/client.js +8832 -0
- package/lib/index.js +5281 -0
- package/lib/invariant.js +85 -0
- package/lib/member-bridge.js +298 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3137 -0
- package/lib/typert.remote-client.d.ts +67 -0
- package/lib/typert.remote-client.js +1971 -0
- package/lib/types/cli-version.d.ts +82 -0
- package/lib/types/cli-version.js +165 -0
- package/lib/types/client/AuthStatusDot.d.ts +17 -0
- package/lib/types/client/AuthStatusDot.js +9 -0
- package/lib/types/client/HarnessModelPicker.d.ts +17 -0
- package/lib/types/client/HarnessModelPicker.js +48 -0
- package/lib/types/client/LocalAgentRecordsAction.d.ts +41 -0
- package/lib/types/client/LocalAgentRecordsAction.js +74 -0
- package/lib/types/client/MemberComposer.d.ts +79 -0
- package/lib/types/client/MemberComposer.js +308 -0
- package/lib/types/client/MemberConfiguration.d.ts +9 -0
- package/lib/types/client/MemberConfiguration.js +46 -0
- package/lib/types/client/MemberInboxView.d.ts +12 -0
- package/lib/types/client/MemberInboxView.js +38 -0
- package/lib/types/client/MemberLiveNode.d.ts +14 -0
- package/lib/types/client/MemberLiveNode.js +74 -0
- package/lib/types/client/MemberPaintDiagnostics.d.ts +7 -0
- package/lib/types/client/MemberPaintDiagnostics.js +20 -0
- package/lib/types/client/ModelConfigurationFields.d.ts +15 -0
- package/lib/types/client/ModelConfigurationFields.js +23 -0
- package/lib/types/client/ProviderAuthBlock.d.ts +59 -0
- package/lib/types/client/ProviderAuthBlock.js +164 -0
- package/lib/types/client/auth-status.d.ts +28 -0
- package/lib/types/client/auth-status.js +56 -0
- package/lib/types/client/icons.d.ts +22 -0
- package/lib/types/client/icons.js +6 -0
- package/lib/types/client/index.d.ts +62 -0
- package/lib/types/client/index.js +135 -0
- package/lib/types/client/live-node.d.ts +14 -0
- package/lib/types/client/live-node.js +34 -0
- package/lib/types/client/live-output.d.ts +34 -0
- package/lib/types/client/live-output.js +94 -0
- package/lib/types/client/live-paint.d.ts +40 -0
- package/lib/types/client/live-paint.js +74 -0
- package/lib/types/client/locales.d.ts +120 -0
- package/lib/types/client/locales.js +227 -0
- package/lib/types/client/member-configuration.d.ts +41 -0
- package/lib/types/client/member-configuration.js +114 -0
- package/lib/types/client/member-dock.d.ts +94 -0
- package/lib/types/client/member-dock.js +102 -0
- package/lib/types/client/member-feed.d.ts +24 -0
- package/lib/types/client/member-feed.js +134 -0
- package/lib/types/client/request-id.d.ts +3 -0
- package/lib/types/client/request-id.js +6 -0
- package/lib/types/client/settings-scope.d.ts +74 -0
- package/lib/types/client/settings-scope.js +98 -0
- package/lib/types/client/use-configuration-menu.d.ts +10 -0
- package/lib/types/client/use-configuration-menu.js +61 -0
- package/lib/types/container.d.ts +69 -0
- package/lib/types/container.js +129 -0
- package/lib/types/env.d.ts +17 -0
- package/lib/types/env.js +89 -0
- package/lib/types/gateway.d.ts +162 -0
- package/lib/types/gateway.js +470 -0
- package/lib/types/index.d.ts +1463 -0
- package/lib/types/index.js +2704 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +105 -0
- package/lib/types/live-flush.d.ts +21 -0
- package/lib/types/live-flush.js +59 -0
- package/lib/types/live-stream.d.ts +41 -0
- package/lib/types/live-stream.js +178 -0
- package/lib/types/member-bridge.d.ts +50 -0
- package/lib/types/member-bridge.js +216 -0
- package/lib/types/member-channel.d.ts +86 -0
- package/lib/types/member-channel.js +232 -0
- package/lib/types/member-control-storage.d.ts +10 -0
- package/lib/types/member-control-storage.js +100 -0
- package/lib/types/member-control.d.ts +62 -0
- package/lib/types/member-control.js +359 -0
- package/lib/types/member-controls.d.ts +23 -0
- package/lib/types/member-controls.js +143 -0
- package/lib/types/member-feed.d.ts +4 -0
- package/lib/types/member-feed.js +45 -0
- package/lib/types/member-inbox.d.ts +30 -0
- package/lib/types/member-inbox.js +225 -0
- package/lib/types/model-directory.d.ts +43 -0
- package/lib/types/model-directory.js +192 -0
- package/lib/types/prepared-members.d.ts +15 -0
- package/lib/types/prepared-members.js +61 -0
- package/lib/types/settings-face.d.ts +61 -0
- package/lib/types/settings-face.js +61 -0
- package/lib/types/types.d.ts +959 -0
- package/lib/types/types.js +24 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +112 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** One shared source for every visible entry to the same member. */
|
|
2
|
+
export class MemberConfigurationStore {
|
|
3
|
+
id;
|
|
4
|
+
face;
|
|
5
|
+
onIdle;
|
|
6
|
+
snapshot = { connected: false };
|
|
7
|
+
listeners = new Set();
|
|
8
|
+
controller;
|
|
9
|
+
constructor(id, face, onIdle = () => { }) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.face = face;
|
|
12
|
+
this.onIdle = onIdle;
|
|
13
|
+
}
|
|
14
|
+
getSnapshot = () => this.snapshot;
|
|
15
|
+
subscribe = (listener) => {
|
|
16
|
+
this.listeners.add(listener);
|
|
17
|
+
if (this.controller === undefined) {
|
|
18
|
+
const controller = this.controller = new AbortController();
|
|
19
|
+
void this.follow(controller.signal);
|
|
20
|
+
void this.followDirectory(controller.signal);
|
|
21
|
+
}
|
|
22
|
+
return () => {
|
|
23
|
+
this.listeners.delete(listener);
|
|
24
|
+
if (this.listeners.size === 0) {
|
|
25
|
+
this.controller?.abort();
|
|
26
|
+
this.controller = undefined;
|
|
27
|
+
this.onIdle();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
publish(patch) {
|
|
32
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
33
|
+
for (const listener of this.listeners)
|
|
34
|
+
listener();
|
|
35
|
+
}
|
|
36
|
+
accept(state) {
|
|
37
|
+
if (state.memberId !== this.id || state.revision < (this.snapshot.state?.revision ?? -1))
|
|
38
|
+
return;
|
|
39
|
+
this.publish({ state, connected: true, error: undefined });
|
|
40
|
+
}
|
|
41
|
+
async follow(signal) {
|
|
42
|
+
while (!signal.aborted) {
|
|
43
|
+
try {
|
|
44
|
+
for await (const state of this.face.follow(this.id, signal)) {
|
|
45
|
+
if (signal.aborted)
|
|
46
|
+
return;
|
|
47
|
+
this.accept(state);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!signal.aborted)
|
|
52
|
+
this.publish({ error: error instanceof Error ? error.message : String(error) });
|
|
53
|
+
}
|
|
54
|
+
if (signal.aborted)
|
|
55
|
+
return;
|
|
56
|
+
this.publish({ connected: false });
|
|
57
|
+
await pause(signal);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async followDirectory(signal) {
|
|
61
|
+
while (!signal.aborted) {
|
|
62
|
+
try {
|
|
63
|
+
for await (const directory of this.face.followDirectory(this.id, signal)) {
|
|
64
|
+
if (signal.aborted)
|
|
65
|
+
return;
|
|
66
|
+
this.publish({ directory });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { /* The independent configuration subscription stays available. */ }
|
|
70
|
+
if (signal.aborted)
|
|
71
|
+
return;
|
|
72
|
+
await pause(signal);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async refreshDirectory() {
|
|
76
|
+
const directory = await this.face.directory(this.id, true);
|
|
77
|
+
if (directory !== null)
|
|
78
|
+
this.publish({ directory });
|
|
79
|
+
}
|
|
80
|
+
async refresh() {
|
|
81
|
+
const before = this.snapshot;
|
|
82
|
+
const state = await this.face.read(this.id);
|
|
83
|
+
// A response cannot roll back a stream update received while it was in flight.
|
|
84
|
+
if (this.snapshot === before && state.memberId === this.id && state.revision >= (before.state?.revision ?? -1))
|
|
85
|
+
this.publish({ state });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function pause(signal) {
|
|
89
|
+
return new Promise(resolve => {
|
|
90
|
+
if (signal.aborted) {
|
|
91
|
+
resolve();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const done = () => { clearTimeout(timer); signal.removeEventListener('abort', done); resolve(); };
|
|
95
|
+
const timer = setTimeout(done, 1_000);
|
|
96
|
+
signal.addEventListener('abort', done, { once: true });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export class MemberConfigurationStores {
|
|
100
|
+
face;
|
|
101
|
+
stores = new Map();
|
|
102
|
+
constructor(face) {
|
|
103
|
+
this.face = face;
|
|
104
|
+
}
|
|
105
|
+
get(id) {
|
|
106
|
+
let store = this.stores.get(id);
|
|
107
|
+
if (store === undefined)
|
|
108
|
+
this.stores.set(id, store = new MemberConfigurationStore(id, this.face, () => {
|
|
109
|
+
if (this.stores.get(id) === store)
|
|
110
|
+
this.stores.delete(id);
|
|
111
|
+
}));
|
|
112
|
+
return store;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The member dock: a unified projection-row stack rendered below the member
|
|
3
|
+
* composer card. Each ambient state (token stats now; tasks / run progress
|
|
4
|
+
* later) is one CONTRIBUTOR — a pure function from the projection bag to a
|
|
5
|
+
* line descriptor or null — and registered contributors render in
|
|
6
|
+
* registration order as separate rows sharing the official StatsLine metrics.
|
|
7
|
+
* A contributor with no data returns null and its row is omitted; an all-null
|
|
8
|
+
* stack renders nothing.
|
|
9
|
+
*
|
|
10
|
+
* Why a registry: the fallback-hiding lesson (the official dock lives inside
|
|
11
|
+
* the composer-chain fallback that `overlay: true` hides on election) makes
|
|
12
|
+
* self-rendered ambient state a repeating pattern in this composer — a second
|
|
13
|
+
* ad-hoc row would already be a maintenance hazard, so the first refactor
|
|
14
|
+
* extracts the pattern.
|
|
15
|
+
* @module @khorsheed/dsh-local-agent/client/member-dock
|
|
16
|
+
*/
|
|
17
|
+
import type { TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client';
|
|
18
|
+
import type { TodoItem } from '@deepseek-ai/dsh-tool-todo/client';
|
|
19
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
20
|
+
import type { NS } from './locales.ts';
|
|
21
|
+
/** The locale seat contributors write copy through. */
|
|
22
|
+
type DockTranslate = PropsLocale<typeof NS>['t'];
|
|
23
|
+
/** One rendered dock row. */
|
|
24
|
+
export interface MemberDockLine {
|
|
25
|
+
/** Stable row id (the contributor's name; also the row's test hook stem). */
|
|
26
|
+
id: string;
|
|
27
|
+
/** The fully formatted row text. */
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The projection bag contributors read. Sourced from the slot kit's
|
|
32
|
+
* `useProjection` seat — one entry per projection key a contributor needs
|
|
33
|
+
* (`tokenUsage` now; `todos` / run progress later). An absent unit reads as
|
|
34
|
+
* undefined, and the contributor drops out whole.
|
|
35
|
+
*/
|
|
36
|
+
export interface MemberDockProjections {
|
|
37
|
+
/** Session token accounting (the token-meter unit's durable projection). */
|
|
38
|
+
tokenUsage?: TokenUsageProjection | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The member's current whole todo list (the latest `todo/write` snapshot —
|
|
41
|
+
* the dsh mirror passes it through; other providers' translation lands in
|
|
42
|
+
* later milestones), or null before the first write.
|
|
43
|
+
*/
|
|
44
|
+
todos?: readonly TodoItem[] | null | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One dock contributor: a pure function from the projection bag to a row, or
|
|
48
|
+
* null when its data is absent (the row is then omitted).
|
|
49
|
+
*/
|
|
50
|
+
export type MemberDockContributor = (projections: MemberDockProjections, t: DockTranslate) => MemberDockLine | null;
|
|
51
|
+
/**
|
|
52
|
+
* Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
|
|
53
|
+
* digits) — the official StatsLine's rule, mirrored so the member composer's
|
|
54
|
+
* stats read identically.
|
|
55
|
+
* @param n - token count.
|
|
56
|
+
* @returns display string.
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatTokens(n: number): string;
|
|
59
|
+
/**
|
|
60
|
+
* Cache-hit share of prompt-side input (cacheRead over the three disjoint
|
|
61
|
+
* billing buckets), the official StatsLine's formula.
|
|
62
|
+
* @param usage - the session's token-usage projection value.
|
|
63
|
+
* @returns rounded integer percent, or null when no input was billed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function cacheHitPercent(usage: TokenUsageProjection): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* The stats contributor: the member-channel stats line (cache-hit share over
|
|
68
|
+
* the three billing buckets + compact input/output totals), behavior
|
|
69
|
+
* byte-identical to the pre-dock implementation — it just returns a dock line
|
|
70
|
+
* now instead of rendering in place. While a run is in flight the projection
|
|
71
|
+
* holds the last settled usage — exactly the desired "no live ticking".
|
|
72
|
+
*/
|
|
73
|
+
export declare function statsContributor(projections: MemberDockProjections, t: DockTranslate): MemberDockLine | null;
|
|
74
|
+
/**
|
|
75
|
+
* The tasks contributor: a compact summary of the member's todo list —
|
|
76
|
+
* `任务 <done>/<total>` plus the in-progress task's title when one is active
|
|
77
|
+
* (an all-completed list shows the summary alone). Declines when the session
|
|
78
|
+
* has no todos at all (unit absent, pre-first-write null, or an empty list).
|
|
79
|
+
*/
|
|
80
|
+
export declare function tasksContributor(projections: MemberDockProjections, t: DockTranslate): MemberDockLine | null;
|
|
81
|
+
/** The registered contributors, in render order. Registration IS inclusion. */
|
|
82
|
+
export declare const MEMBER_DOCK_CONTRIBUTORS: readonly MemberDockContributor[];
|
|
83
|
+
/**
|
|
84
|
+
* Evaluate contributors over the current projections.
|
|
85
|
+
* @param projections - the projection bag from the slot kit.
|
|
86
|
+
* @param t - the locale seat.
|
|
87
|
+
* @param contributors - the registry; the parameter exists so tests exercise
|
|
88
|
+
* the evaluation contract (null rows omitted, registration order) without
|
|
89
|
+
* mutating the real registry.
|
|
90
|
+
* @returns the rows to render, in registration order (possibly empty).
|
|
91
|
+
*/
|
|
92
|
+
export declare function memberDockLines(projections: MemberDockProjections, t: DockTranslate, contributors?: readonly MemberDockContributor[]): MemberDockLine[];
|
|
93
|
+
export {};
|
|
94
|
+
//# sourceMappingURL=member-dock.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The member dock: a unified projection-row stack rendered below the member
|
|
3
|
+
* composer card. Each ambient state (token stats now; tasks / run progress
|
|
4
|
+
* later) is one CONTRIBUTOR — a pure function from the projection bag to a
|
|
5
|
+
* line descriptor or null — and registered contributors render in
|
|
6
|
+
* registration order as separate rows sharing the official StatsLine metrics.
|
|
7
|
+
* A contributor with no data returns null and its row is omitted; an all-null
|
|
8
|
+
* stack renders nothing.
|
|
9
|
+
*
|
|
10
|
+
* Why a registry: the fallback-hiding lesson (the official dock lives inside
|
|
11
|
+
* the composer-chain fallback that `overlay: true` hides on election) makes
|
|
12
|
+
* self-rendered ambient state a repeating pattern in this composer — a second
|
|
13
|
+
* ad-hoc row would already be a maintenance hazard, so the first refactor
|
|
14
|
+
* extracts the pattern.
|
|
15
|
+
* @module @khorsheed/dsh-local-agent/client/member-dock
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
|
|
19
|
+
* digits) — the official StatsLine's rule, mirrored so the member composer's
|
|
20
|
+
* stats read identically.
|
|
21
|
+
* @param n - token count.
|
|
22
|
+
* @returns display string.
|
|
23
|
+
*/
|
|
24
|
+
export function formatTokens(n) {
|
|
25
|
+
const scaled = (v) => v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10);
|
|
26
|
+
if (n < 1_000)
|
|
27
|
+
return String(n);
|
|
28
|
+
if (n < 1_000_000)
|
|
29
|
+
return `${scaled(n / 1_000)}K`;
|
|
30
|
+
return `${scaled(n / 1_000_000)}M`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Cache-hit share of prompt-side input (cacheRead over the three disjoint
|
|
34
|
+
* billing buckets), the official StatsLine's formula.
|
|
35
|
+
* @param usage - the session's token-usage projection value.
|
|
36
|
+
* @returns rounded integer percent, or null when no input was billed.
|
|
37
|
+
*/
|
|
38
|
+
export function cacheHitPercent(usage) {
|
|
39
|
+
const billed = usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
40
|
+
return billed === 0 ? null : Math.round(usage.cacheReadTokens / billed * 100);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The stats contributor: the member-channel stats line (cache-hit share over
|
|
44
|
+
* the three billing buckets + compact input/output totals), behavior
|
|
45
|
+
* byte-identical to the pre-dock implementation — it just returns a dock line
|
|
46
|
+
* now instead of rendering in place. While a run is in flight the projection
|
|
47
|
+
* holds the last settled usage — exactly the desired "no live ticking".
|
|
48
|
+
*/
|
|
49
|
+
export function statsContributor(projections, t) {
|
|
50
|
+
const usage = projections.tokenUsage;
|
|
51
|
+
if (usage === undefined)
|
|
52
|
+
return null;
|
|
53
|
+
const billed = usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
54
|
+
if (billed === 0 && usage.outputTokens === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const groups = [];
|
|
57
|
+
const cacheHit = cacheHitPercent(usage);
|
|
58
|
+
if (cacheHit !== null)
|
|
59
|
+
groups.push(t('member.stats.cacheHit', { percent: cacheHit }));
|
|
60
|
+
groups.push(t('member.stats.tokens', { input: formatTokens(billed), output: formatTokens(usage.outputTokens) }));
|
|
61
|
+
return { id: 'stats', text: groups.join(' | ') };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The tasks contributor: a compact summary of the member's todo list —
|
|
65
|
+
* `任务 <done>/<total>` plus the in-progress task's title when one is active
|
|
66
|
+
* (an all-completed list shows the summary alone). Declines when the session
|
|
67
|
+
* has no todos at all (unit absent, pre-first-write null, or an empty list).
|
|
68
|
+
*/
|
|
69
|
+
export function tasksContributor(projections, t) {
|
|
70
|
+
const todos = projections.todos;
|
|
71
|
+
if (todos === undefined || todos === null || todos.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
const done = todos.filter(todo => todo.status === 'completed').length;
|
|
74
|
+
const active = todos.find(todo => todo.status === 'in_progress');
|
|
75
|
+
let text = t('member.tasks.summary', { done, total: todos.length });
|
|
76
|
+
if (active !== undefined)
|
|
77
|
+
text += t('member.tasks.active', { title: active.content });
|
|
78
|
+
return { id: 'tasks', text };
|
|
79
|
+
}
|
|
80
|
+
/** The registered contributors, in render order. Registration IS inclusion. */
|
|
81
|
+
export const MEMBER_DOCK_CONTRIBUTORS = [
|
|
82
|
+
statsContributor,
|
|
83
|
+
tasksContributor,
|
|
84
|
+
];
|
|
85
|
+
/**
|
|
86
|
+
* Evaluate contributors over the current projections.
|
|
87
|
+
* @param projections - the projection bag from the slot kit.
|
|
88
|
+
* @param t - the locale seat.
|
|
89
|
+
* @param contributors - the registry; the parameter exists so tests exercise
|
|
90
|
+
* the evaluation contract (null rows omitted, registration order) without
|
|
91
|
+
* mutating the real registry.
|
|
92
|
+
* @returns the rows to render, in registration order (possibly empty).
|
|
93
|
+
*/
|
|
94
|
+
export function memberDockLines(projections, t, contributors = MEMBER_DOCK_CONTRIBUTORS) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
for (const contributor of contributors) {
|
|
97
|
+
const line = contributor(projections, t);
|
|
98
|
+
if (line !== null)
|
|
99
|
+
lines.push(line);
|
|
100
|
+
}
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LocalAgentMemberFeedEvent, LocalAgentMemberFeedRequest, LocalAgentStreamFrame } from '../types.ts';
|
|
2
|
+
type Channel = LocalAgentMemberFeedRequest['channel'];
|
|
3
|
+
type Value<C extends Channel> = Extract<LocalAgentMemberFeedEvent, {
|
|
4
|
+
channel: C;
|
|
5
|
+
}>['value'];
|
|
6
|
+
type Follow = (requests: readonly LocalAgentMemberFeedRequest[], signal: AbortSignal) => AsyncIterable<LocalAgentMemberFeedEvent>;
|
|
7
|
+
/** Coalesce unsent suffixes without losing text; no per-token event queue. */
|
|
8
|
+
export declare function coalesceOutput(previous: LocalAgentStreamFrame, next: LocalAgentStreamFrame): LocalAgentStreamFrame;
|
|
9
|
+
/** All member renderers in one browser plugin instance share one Remote stream. */
|
|
10
|
+
export declare class MemberFeeds {
|
|
11
|
+
private readonly source;
|
|
12
|
+
private readonly entries;
|
|
13
|
+
private controller;
|
|
14
|
+
private scheduled;
|
|
15
|
+
private disposed;
|
|
16
|
+
constructor(source: Follow);
|
|
17
|
+
follow<C extends Channel>(memberId: string, channel: C, signal: AbortSignal): AsyncIterable<Value<C>>;
|
|
18
|
+
private schedule;
|
|
19
|
+
private deliver;
|
|
20
|
+
private run;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=member-feed.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const keyOf = (id, channel) => JSON.stringify([id, channel]);
|
|
2
|
+
/** Coalesce unsent suffixes without losing text; no per-token event queue. */
|
|
3
|
+
export function coalesceOutput(previous, next) {
|
|
4
|
+
if (next.baseline)
|
|
5
|
+
return next;
|
|
6
|
+
const updates = new Map(previous.updates.map(update => [update.id, update]));
|
|
7
|
+
for (const update of next.updates) {
|
|
8
|
+
const old = updates.get(update.id);
|
|
9
|
+
if (old !== undefined && update.revision <= old.revision)
|
|
10
|
+
continue;
|
|
11
|
+
updates.set(update.id, update.append && old !== undefined
|
|
12
|
+
? { ...update, text: old.text + update.text, append: old.append } : update);
|
|
13
|
+
}
|
|
14
|
+
return { baseline: previous.baseline, updates: [...updates.values()], removed: [...new Set([...previous.removed, ...next.removed])] };
|
|
15
|
+
}
|
|
16
|
+
/** All member renderers in one browser plugin instance share one Remote stream. */
|
|
17
|
+
export class MemberFeeds {
|
|
18
|
+
source;
|
|
19
|
+
entries = new Map();
|
|
20
|
+
controller;
|
|
21
|
+
scheduled = false;
|
|
22
|
+
disposed = false;
|
|
23
|
+
constructor(source) {
|
|
24
|
+
this.source = source;
|
|
25
|
+
}
|
|
26
|
+
async *follow(memberId, channel, signal) {
|
|
27
|
+
if (signal.aborted || this.disposed)
|
|
28
|
+
return;
|
|
29
|
+
const key = keyOf(memberId, channel);
|
|
30
|
+
let entry = this.entries.get(key);
|
|
31
|
+
if (entry === undefined) {
|
|
32
|
+
this.entries.set(key, entry = { request: { memberId, channel }, subscribers: new Set() });
|
|
33
|
+
this.schedule();
|
|
34
|
+
}
|
|
35
|
+
const subscriber = { pending: entry.cached };
|
|
36
|
+
const wake = () => { subscriber.wake?.(); subscriber.wake = undefined; };
|
|
37
|
+
entry.subscribers.add(subscriber);
|
|
38
|
+
signal.addEventListener('abort', wake, { once: true });
|
|
39
|
+
try {
|
|
40
|
+
while (!signal.aborted && !this.disposed) {
|
|
41
|
+
if (subscriber.pending === undefined)
|
|
42
|
+
await new Promise(resolve => { subscriber.wake = resolve; });
|
|
43
|
+
if (signal.aborted || this.disposed)
|
|
44
|
+
return;
|
|
45
|
+
const event = subscriber.pending;
|
|
46
|
+
subscriber.pending = undefined;
|
|
47
|
+
if (event === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
if (event.channel === 'error')
|
|
50
|
+
throw new Error(event.message);
|
|
51
|
+
yield event.value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
signal.removeEventListener('abort', wake);
|
|
56
|
+
entry.subscribers.delete(subscriber);
|
|
57
|
+
if (entry.subscribers.size === 0 && this.entries.get(key) === entry) {
|
|
58
|
+
this.entries.delete(key);
|
|
59
|
+
this.schedule();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
schedule() {
|
|
64
|
+
if (this.scheduled || this.disposed)
|
|
65
|
+
return;
|
|
66
|
+
this.scheduled = true;
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
this.scheduled = false;
|
|
69
|
+
this.controller?.abort();
|
|
70
|
+
this.controller = undefined;
|
|
71
|
+
if (this.disposed || this.entries.size === 0)
|
|
72
|
+
return;
|
|
73
|
+
const controller = this.controller = new AbortController();
|
|
74
|
+
void this.run(controller);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
deliver(event) {
|
|
78
|
+
const entry = this.entries.get(keyOf(event.memberId, event.channel === 'error' ? event.source : event.channel));
|
|
79
|
+
if (entry === undefined)
|
|
80
|
+
return;
|
|
81
|
+
if (event.channel === 'output') {
|
|
82
|
+
// A late local subscriber receives a full active baseline, never a suffix.
|
|
83
|
+
const old = entry.cached?.channel === 'output' ? entry.cached.value : undefined;
|
|
84
|
+
const items = new Map(event.value.baseline ? [] : old?.updates.map(item => [item.id, item]) ?? []);
|
|
85
|
+
for (const update of event.value.updates) {
|
|
86
|
+
const previous = items.get(update.id);
|
|
87
|
+
if (update.append && previous === undefined)
|
|
88
|
+
throw new Error('Member feed suffix without baseline');
|
|
89
|
+
if (previous !== undefined && update.revision <= previous.revision)
|
|
90
|
+
continue;
|
|
91
|
+
items.set(update.id, { ...update, text: update.append ? previous.text + update.text : update.text });
|
|
92
|
+
}
|
|
93
|
+
for (const id of event.value.removed)
|
|
94
|
+
items.delete(id);
|
|
95
|
+
entry.cached = { ...event, value: { baseline: true, updates: [...items.values()].map(item => ({ ...item, append: false })), removed: [] } };
|
|
96
|
+
}
|
|
97
|
+
else if (event.channel !== 'error')
|
|
98
|
+
entry.cached = event;
|
|
99
|
+
for (const subscriber of entry.subscribers) {
|
|
100
|
+
subscriber.pending = event.channel === 'output' && subscriber.pending?.channel === 'output'
|
|
101
|
+
? { ...event, value: coalesceOutput(subscriber.pending.value, event.value) } : event;
|
|
102
|
+
subscriber.wake?.();
|
|
103
|
+
subscriber.wake = undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async run(controller) {
|
|
107
|
+
try {
|
|
108
|
+
for await (const event of this.source([...this.entries.values()].map(entry => entry.request), controller.signal)) {
|
|
109
|
+
if (controller.signal.aborted)
|
|
110
|
+
return;
|
|
111
|
+
this.deliver(event);
|
|
112
|
+
}
|
|
113
|
+
if (!controller.signal.aborted)
|
|
114
|
+
throw new Error('Member feed disconnected');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (controller.signal.aborted)
|
|
118
|
+
return;
|
|
119
|
+
for (const entry of this.entries.values())
|
|
120
|
+
this.deliver({
|
|
121
|
+
memberId: entry.request.memberId, channel: 'error', source: entry.request.channel,
|
|
122
|
+
message: error instanceof Error ? error.message : String(error),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
dispose() {
|
|
127
|
+
this.disposed = true;
|
|
128
|
+
this.controller?.abort();
|
|
129
|
+
for (const entry of this.entries.values())
|
|
130
|
+
for (const subscriber of entry.subscribers)
|
|
131
|
+
subscriber.wake?.();
|
|
132
|
+
this.entries.clear();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** getRandomValues also works on LAN HTTP origins where randomUUID is unavailable. */
|
|
2
|
+
export function requestId() {
|
|
3
|
+
if (typeof crypto.randomUUID === 'function')
|
|
4
|
+
return crypto.randomUUID();
|
|
5
|
+
return Array.from(crypto.getRandomValues(new Uint32Array(4)), value => value.toString(16).padStart(8, '0')).join('');
|
|
6
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-line client settings scope for the family settings cards: one
|
|
3
|
+
* snapshot/write contract over the two host client generations, probed by
|
|
4
|
+
* capability, never by version.
|
|
5
|
+
*
|
|
6
|
+
* - 0.1.5: `ctx.settingsScope.bind({ namespace })` — the removed
|
|
7
|
+
* `SettingsScope` face.
|
|
8
|
+
* - 0.1.7-rc.1: `ctx.configForms.get(entryId)` — the `ConfigForm` controller
|
|
9
|
+
* (`packages/client/ui-settings/src/client/config-form.ts`), keyed by the
|
|
10
|
+
* plugin row id, which is the same string the 0.1.5 namespace used.
|
|
11
|
+
*
|
|
12
|
+
* Both are snapshot stores with ordered field writes, so the cards consume
|
|
13
|
+
* one structural type. The 0.1.5 card keeps reading old `settings.yaml`
|
|
14
|
+
* values through the legacy arm; on rc.1 the official one-shot import has
|
|
15
|
+
* already moved the same-named section into the row's config.
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
/**
|
|
19
|
+
* Client-side sync state of one settings namespace. The field set is the
|
|
20
|
+
* intersection both host lines serve; the cards read `status`, `value`, and
|
|
21
|
+
* `writable`, the rest ride along for editors that layer on top.
|
|
22
|
+
*/
|
|
23
|
+
export interface SettingsScopeSnapshot<T> {
|
|
24
|
+
/** `loading` until the first accepted section, `ready` while one stands, `unavailable` when the namespace is not exposed. */
|
|
25
|
+
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
26
|
+
/** Last accepted schema-resolved section; undefined before the first acceptance. */
|
|
27
|
+
readonly value: T | undefined;
|
|
28
|
+
/** Composition layer a cleared field reverts to, when the owning plugin declared one. */
|
|
29
|
+
readonly base: unknown;
|
|
30
|
+
/** Raw user layer as stored; a field's PRESENCE here marks it overridden. */
|
|
31
|
+
readonly user: unknown;
|
|
32
|
+
/** Namespace revision fencing the next write. */
|
|
33
|
+
readonly revision: number | undefined;
|
|
34
|
+
/** Whether the Host document accepts writes. */
|
|
35
|
+
readonly writable: boolean;
|
|
36
|
+
/** `host` syncs with the Host document; `memory` keeps a remote browser process-local. */
|
|
37
|
+
readonly mode: 'host' | 'memory';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The card's settings face: a hooks source (the renderer's `hooks.settings`
|
|
41
|
+
* compartment turns `getSnapshot`/`subscribe` into the `useSettings` hook)
|
|
42
|
+
* plus ordered field writes. `set`/`unset` settle after the write and any
|
|
43
|
+
* latest-write recovery read on both lines (rc.1's `ConfigForm` additionally
|
|
44
|
+
* resolves whether the Host accepted the write — the cards ignore it).
|
|
45
|
+
*/
|
|
46
|
+
export interface SettingsScope<T> {
|
|
47
|
+
getSnapshot(): SettingsScopeSnapshot<T>;
|
|
48
|
+
subscribe(listener: () => void): () => void;
|
|
49
|
+
set(field: string, value: unknown): Promise<unknown>;
|
|
50
|
+
unset(field: string): Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* An inert scope for a composition whose settings client never came up:
|
|
54
|
+
* reads report `unavailable` (the cards disable their controls, exactly like
|
|
55
|
+
* an unregistered namespace) and writes never settle a value change.
|
|
56
|
+
* @returns the degraded scope.
|
|
57
|
+
*/
|
|
58
|
+
export declare function unavailableSettingsScope<T>(): SettingsScope<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Bind the provider row's settings scope on the host line actually serving.
|
|
61
|
+
* Returns the channel synchronously; both line probes ride deferred injects,
|
|
62
|
+
* so the serving line's arm fires as soon as its settings service is up (the
|
|
63
|
+
* settings client package is a declared client inject of every family
|
|
64
|
+
* provider, so in a normal composition arming lands immediately) and a
|
|
65
|
+
* provider mounting first still gets armed later. Absent both — a broken
|
|
66
|
+
* composition — the channel keeps the card on the degraded face, alive and
|
|
67
|
+
* disabled.
|
|
68
|
+
* @param ctx - the provider's client context.
|
|
69
|
+
* @param entryId - the plugin row id (`local-agent-dsh` and friends), the
|
|
70
|
+
* same string the 0.1.5 namespace used.
|
|
71
|
+
* @returns the bound scope channel.
|
|
72
|
+
*/
|
|
73
|
+
export declare function bindSettingsScope<T>(ctx: Context, entryId: string): SettingsScope<T>;
|
|
74
|
+
//# sourceMappingURL=settings-scope.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-line client settings scope for the family settings cards: one
|
|
3
|
+
* snapshot/write contract over the two host client generations, probed by
|
|
4
|
+
* capability, never by version.
|
|
5
|
+
*
|
|
6
|
+
* - 0.1.5: `ctx.settingsScope.bind({ namespace })` — the removed
|
|
7
|
+
* `SettingsScope` face.
|
|
8
|
+
* - 0.1.7-rc.1: `ctx.configForms.get(entryId)` — the `ConfigForm` controller
|
|
9
|
+
* (`packages/client/ui-settings/src/client/config-form.ts`), keyed by the
|
|
10
|
+
* plugin row id, which is the same string the 0.1.5 namespace used.
|
|
11
|
+
*
|
|
12
|
+
* Both are snapshot stores with ordered field writes, so the cards consume
|
|
13
|
+
* one structural type. The 0.1.5 card keeps reading old `settings.yaml`
|
|
14
|
+
* values through the legacy arm; on rc.1 the official one-shot import has
|
|
15
|
+
* already moved the same-named section into the row's config.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Observable channel behind the card's settings face: delegates to the armed
|
|
19
|
+
* line scope and republishes on arming, reporting the degraded face until
|
|
20
|
+
* either host line's deferred probe arms it. The channel exists because the
|
|
21
|
+
* line probe cannot run as a direct property read — cordis 4.0.4's
|
|
22
|
+
* service-access guard throws `cannot get property "<name>" without inject`
|
|
23
|
+
* on any undeclared `ctx.<service>` touch, and neither service name exists on
|
|
24
|
+
* the other host line, so a static inject would pend there; `ctx.inject` +
|
|
25
|
+
* `ctx.get` is the only probe that is safe on both.
|
|
26
|
+
*/
|
|
27
|
+
class SettingsScopeChannel {
|
|
28
|
+
inner = unavailableSettingsScope();
|
|
29
|
+
listeners = new Set();
|
|
30
|
+
getSnapshot = () => this.inner.getSnapshot();
|
|
31
|
+
subscribe = (listener) => {
|
|
32
|
+
this.listeners.add(listener);
|
|
33
|
+
return () => { this.listeners.delete(listener); };
|
|
34
|
+
};
|
|
35
|
+
set = (field, value) => this.inner.set(field, value);
|
|
36
|
+
unset = (field) => this.inner.unset(field);
|
|
37
|
+
/** Bind the line-served scope; the first arm wins (the two lines never coexist). */
|
|
38
|
+
arm(scope) {
|
|
39
|
+
this.inner = scope;
|
|
40
|
+
scope.subscribe(() => { this.emit(); });
|
|
41
|
+
this.emit();
|
|
42
|
+
}
|
|
43
|
+
emit() {
|
|
44
|
+
for (const listener of [...this.listeners])
|
|
45
|
+
listener();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* An inert scope for a composition whose settings client never came up:
|
|
50
|
+
* reads report `unavailable` (the cards disable their controls, exactly like
|
|
51
|
+
* an unregistered namespace) and writes never settle a value change.
|
|
52
|
+
* @returns the degraded scope.
|
|
53
|
+
*/
|
|
54
|
+
export function unavailableSettingsScope() {
|
|
55
|
+
const snapshot = {
|
|
56
|
+
status: 'unavailable',
|
|
57
|
+
value: undefined,
|
|
58
|
+
base: undefined,
|
|
59
|
+
user: undefined,
|
|
60
|
+
revision: undefined,
|
|
61
|
+
writable: false,
|
|
62
|
+
mode: 'memory',
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
getSnapshot: () => snapshot,
|
|
66
|
+
subscribe: () => () => { },
|
|
67
|
+
set: () => Promise.resolve(false),
|
|
68
|
+
unset: () => Promise.resolve(false),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Bind the provider row's settings scope on the host line actually serving.
|
|
73
|
+
* Returns the channel synchronously; both line probes ride deferred injects,
|
|
74
|
+
* so the serving line's arm fires as soon as its settings service is up (the
|
|
75
|
+
* settings client package is a declared client inject of every family
|
|
76
|
+
* provider, so in a normal composition arming lands immediately) and a
|
|
77
|
+
* provider mounting first still gets armed later. Absent both — a broken
|
|
78
|
+
* composition — the channel keeps the card on the degraded face, alive and
|
|
79
|
+
* disabled.
|
|
80
|
+
* @param ctx - the provider's client context.
|
|
81
|
+
* @param entryId - the plugin row id (`local-agent-dsh` and friends), the
|
|
82
|
+
* same string the 0.1.5 namespace used.
|
|
83
|
+
* @returns the bound scope channel.
|
|
84
|
+
*/
|
|
85
|
+
export function bindSettingsScope(ctx, entryId) {
|
|
86
|
+
const channel = new SettingsScopeChannel();
|
|
87
|
+
ctx.inject(['configForms'], (formsCtx) => {
|
|
88
|
+
const forms = formsCtx.get('configForms');
|
|
89
|
+
if (typeof forms?.get === 'function')
|
|
90
|
+
channel.arm(forms.get(entryId));
|
|
91
|
+
});
|
|
92
|
+
ctx.inject(['settingsScope'], (legacyCtx) => {
|
|
93
|
+
const legacy = legacyCtx.get('settingsScope');
|
|
94
|
+
if (typeof legacy?.bind === 'function')
|
|
95
|
+
channel.arm(legacy.bind({ namespace: entryId }));
|
|
96
|
+
});
|
|
97
|
+
return channel;
|
|
98
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
/** Shared dismissal and placement for settings and member configuration menus. */
|
|
3
|
+
export declare function useConfigurationMenu(preferred: 'above' | 'below'): {
|
|
4
|
+
root: import("react").RefObject<HTMLDetailsElement>;
|
|
5
|
+
panel: import("react").RefObject<HTMLDivElement>;
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
8
|
+
style: CSSProperties | undefined;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=use-configuration-menu.d.ts.map
|