@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.
Files changed (99) hide show
  1. package/README.en.md +181 -0
  2. package/README.i18n.yaml +2 -0
  3. package/README.md +181 -0
  4. package/cordis.patch.yml +21 -0
  5. package/lib/client.js +8832 -0
  6. package/lib/index.js +5281 -0
  7. package/lib/invariant.js +85 -0
  8. package/lib/member-bridge.js +298 -0
  9. package/lib/typert.host.d.ts +3 -0
  10. package/lib/typert.host.js +3137 -0
  11. package/lib/typert.remote-client.d.ts +67 -0
  12. package/lib/typert.remote-client.js +1971 -0
  13. package/lib/types/cli-version.d.ts +82 -0
  14. package/lib/types/cli-version.js +165 -0
  15. package/lib/types/client/AuthStatusDot.d.ts +17 -0
  16. package/lib/types/client/AuthStatusDot.js +9 -0
  17. package/lib/types/client/HarnessModelPicker.d.ts +17 -0
  18. package/lib/types/client/HarnessModelPicker.js +48 -0
  19. package/lib/types/client/LocalAgentRecordsAction.d.ts +41 -0
  20. package/lib/types/client/LocalAgentRecordsAction.js +74 -0
  21. package/lib/types/client/MemberComposer.d.ts +79 -0
  22. package/lib/types/client/MemberComposer.js +308 -0
  23. package/lib/types/client/MemberConfiguration.d.ts +9 -0
  24. package/lib/types/client/MemberConfiguration.js +46 -0
  25. package/lib/types/client/MemberInboxView.d.ts +12 -0
  26. package/lib/types/client/MemberInboxView.js +38 -0
  27. package/lib/types/client/MemberLiveNode.d.ts +14 -0
  28. package/lib/types/client/MemberLiveNode.js +74 -0
  29. package/lib/types/client/MemberPaintDiagnostics.d.ts +7 -0
  30. package/lib/types/client/MemberPaintDiagnostics.js +20 -0
  31. package/lib/types/client/ModelConfigurationFields.d.ts +15 -0
  32. package/lib/types/client/ModelConfigurationFields.js +23 -0
  33. package/lib/types/client/ProviderAuthBlock.d.ts +59 -0
  34. package/lib/types/client/ProviderAuthBlock.js +164 -0
  35. package/lib/types/client/auth-status.d.ts +28 -0
  36. package/lib/types/client/auth-status.js +56 -0
  37. package/lib/types/client/icons.d.ts +22 -0
  38. package/lib/types/client/icons.js +6 -0
  39. package/lib/types/client/index.d.ts +62 -0
  40. package/lib/types/client/index.js +135 -0
  41. package/lib/types/client/live-node.d.ts +14 -0
  42. package/lib/types/client/live-node.js +34 -0
  43. package/lib/types/client/live-output.d.ts +34 -0
  44. package/lib/types/client/live-output.js +94 -0
  45. package/lib/types/client/live-paint.d.ts +40 -0
  46. package/lib/types/client/live-paint.js +74 -0
  47. package/lib/types/client/locales.d.ts +120 -0
  48. package/lib/types/client/locales.js +227 -0
  49. package/lib/types/client/member-configuration.d.ts +41 -0
  50. package/lib/types/client/member-configuration.js +114 -0
  51. package/lib/types/client/member-dock.d.ts +94 -0
  52. package/lib/types/client/member-dock.js +102 -0
  53. package/lib/types/client/member-feed.d.ts +24 -0
  54. package/lib/types/client/member-feed.js +134 -0
  55. package/lib/types/client/request-id.d.ts +3 -0
  56. package/lib/types/client/request-id.js +6 -0
  57. package/lib/types/client/settings-scope.d.ts +74 -0
  58. package/lib/types/client/settings-scope.js +98 -0
  59. package/lib/types/client/use-configuration-menu.d.ts +10 -0
  60. package/lib/types/client/use-configuration-menu.js +61 -0
  61. package/lib/types/container.d.ts +69 -0
  62. package/lib/types/container.js +129 -0
  63. package/lib/types/env.d.ts +17 -0
  64. package/lib/types/env.js +89 -0
  65. package/lib/types/gateway.d.ts +162 -0
  66. package/lib/types/gateway.js +470 -0
  67. package/lib/types/index.d.ts +1463 -0
  68. package/lib/types/index.js +2704 -0
  69. package/lib/types/invariant.d.ts +16 -0
  70. package/lib/types/invariant.js +105 -0
  71. package/lib/types/live-flush.d.ts +21 -0
  72. package/lib/types/live-flush.js +59 -0
  73. package/lib/types/live-stream.d.ts +41 -0
  74. package/lib/types/live-stream.js +178 -0
  75. package/lib/types/member-bridge.d.ts +50 -0
  76. package/lib/types/member-bridge.js +216 -0
  77. package/lib/types/member-channel.d.ts +86 -0
  78. package/lib/types/member-channel.js +232 -0
  79. package/lib/types/member-control-storage.d.ts +10 -0
  80. package/lib/types/member-control-storage.js +100 -0
  81. package/lib/types/member-control.d.ts +62 -0
  82. package/lib/types/member-control.js +359 -0
  83. package/lib/types/member-controls.d.ts +23 -0
  84. package/lib/types/member-controls.js +143 -0
  85. package/lib/types/member-feed.d.ts +4 -0
  86. package/lib/types/member-feed.js +45 -0
  87. package/lib/types/member-inbox.d.ts +30 -0
  88. package/lib/types/member-inbox.js +225 -0
  89. package/lib/types/model-directory.d.ts +43 -0
  90. package/lib/types/model-directory.js +192 -0
  91. package/lib/types/prepared-members.d.ts +15 -0
  92. package/lib/types/prepared-members.js +61 -0
  93. package/lib/types/settings-face.d.ts +61 -0
  94. package/lib/types/settings-face.js +61 -0
  95. package/lib/types/types.d.ts +959 -0
  96. package/lib/types/types.js +24 -0
  97. package/locale/en.json +6 -0
  98. package/locale/zh.json +6 -0
  99. 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,3 @@
1
+ /** getRandomValues also works on LAN HTTP origins where randomUUID is unavailable. */
2
+ export declare function requestId(): string;
3
+ //# sourceMappingURL=request-id.d.ts.map
@@ -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