@khorsheed/dsh-room 0.1.0

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 (95) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +48 -0
  4. package/README.zh.md +48 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/client.js +11481 -0
  7. package/lib/index.js +3231 -0
  8. package/lib/invariant.js +26 -0
  9. package/lib/journal-CuRnWk66.js +435 -0
  10. package/lib/tool.js +350 -0
  11. package/lib/typert.host.d.ts +3 -0
  12. package/lib/typert.host.js +2615 -0
  13. package/lib/typert.remote-client.d.ts +55 -0
  14. package/lib/typert.remote-client.js +1711 -0
  15. package/lib/types/adapter.d.ts +42 -0
  16. package/lib/types/adapter.js +41 -0
  17. package/lib/types/agent-setup.d.ts +141 -0
  18. package/lib/types/agent-setup.js +145 -0
  19. package/lib/types/client/InviteAgentAction.d.ts +13 -0
  20. package/lib/types/client/InviteAgentAction.js +45 -0
  21. package/lib/types/client/InviteDialog.d.ts +90 -0
  22. package/lib/types/client/InviteDialog.js +216 -0
  23. package/lib/types/client/MemberCard.d.ts +33 -0
  24. package/lib/types/client/MemberCard.js +32 -0
  25. package/lib/types/client/MembersView.d.ts +5 -0
  26. package/lib/types/client/MembersView.js +168 -0
  27. package/lib/types/client/RoomComposer.d.ts +21 -0
  28. package/lib/types/client/RoomComposer.js +211 -0
  29. package/lib/types/client/RoomDockCapsules.d.ts +64 -0
  30. package/lib/types/client/RoomDockCapsules.js +277 -0
  31. package/lib/types/client/RoomEventView.d.ts +11 -0
  32. package/lib/types/client/RoomEventView.js +17 -0
  33. package/lib/types/client/RoomModelPicker.d.ts +18 -0
  34. package/lib/types/client/RoomModelPicker.js +134 -0
  35. package/lib/types/client/RoomPlanView.d.ts +17 -0
  36. package/lib/types/client/RoomPlanView.js +88 -0
  37. package/lib/types/client/RoomQueueStrip.d.ts +26 -0
  38. package/lib/types/client/RoomQueueStrip.js +28 -0
  39. package/lib/types/client/RoomRecoveryView.d.ts +11 -0
  40. package/lib/types/client/RoomRecoveryView.js +38 -0
  41. package/lib/types/client/RoomRelayView.d.ts +12 -0
  42. package/lib/types/client/RoomRelayView.js +20 -0
  43. package/lib/types/client/RoomRunView.d.ts +15 -0
  44. package/lib/types/client/RoomRunView.js +50 -0
  45. package/lib/types/client/RoomSpeechView.d.ts +18 -0
  46. package/lib/types/client/RoomSpeechView.js +62 -0
  47. package/lib/types/client/RoomStatsLine.d.ts +61 -0
  48. package/lib/types/client/RoomStatsLine.js +138 -0
  49. package/lib/types/client/RoomTaskLineView.d.ts +13 -0
  50. package/lib/types/client/RoomTaskLineView.js +21 -0
  51. package/lib/types/client/RoomTodoStrip.d.ts +24 -0
  52. package/lib/types/client/RoomTodoStrip.js +64 -0
  53. package/lib/types/client/format.d.ts +31 -0
  54. package/lib/types/client/format.js +48 -0
  55. package/lib/types/client/icons.d.ts +27 -0
  56. package/lib/types/client/icons.js +11 -0
  57. package/lib/types/client/index.d.ts +39 -0
  58. package/lib/types/client/index.js +471 -0
  59. package/lib/types/client/locales.d.ts +422 -0
  60. package/lib/types/client/locales.js +413 -0
  61. package/lib/types/client/member-color.d.ts +13 -0
  62. package/lib/types/client/member-color.js +22 -0
  63. package/lib/types/client/name-pool.d.ts +17 -0
  64. package/lib/types/client/name-pool.js +25 -0
  65. package/lib/types/client/nodes.d.ts +136 -0
  66. package/lib/types/client/nodes.js +212 -0
  67. package/lib/types/client/preset-visibility.d.ts +91 -0
  68. package/lib/types/client/preset-visibility.js +113 -0
  69. package/lib/types/client/request-ids.d.ts +7 -0
  70. package/lib/types/client/request-ids.js +21 -0
  71. package/lib/types/client/room-store.d.ts +137 -0
  72. package/lib/types/client/room-store.js +244 -0
  73. package/lib/types/client/slots.d.ts +319 -0
  74. package/lib/types/client/slots.js +1 -0
  75. package/lib/types/deliveries.d.ts +5 -0
  76. package/lib/types/deliveries.js +20 -0
  77. package/lib/types/dispatch.d.ts +105 -0
  78. package/lib/types/dispatch.js +467 -0
  79. package/lib/types/index.d.ts +301 -0
  80. package/lib/types/index.js +1200 -0
  81. package/lib/types/invariant.d.ts +16 -0
  82. package/lib/types/invariant.js +24 -0
  83. package/lib/types/journal.d.ts +117 -0
  84. package/lib/types/journal.js +434 -0
  85. package/lib/types/plan-service.d.ts +34 -0
  86. package/lib/types/plan-service.js +266 -0
  87. package/lib/types/plan.d.ts +168 -0
  88. package/lib/types/plan.js +494 -0
  89. package/lib/types/tool.d.ts +56 -0
  90. package/lib/types/tool.js +389 -0
  91. package/lib/types/types.d.ts +813 -0
  92. package/lib/types/types.js +1 -0
  93. package/locale/en.json +6 -0
  94. package/locale/zh.json +6 -0
  95. package/package.json +131 -0
@@ -0,0 +1,212 @@
1
+ /** The shared final-node materialization (single shape across the three kinds). */
2
+ function viewNode(context, kind, visibility = 'visible') {
3
+ if (context.state === undefined)
4
+ return null;
5
+ return {
6
+ key: context.key,
7
+ kind,
8
+ id: context.id,
9
+ target: 'chat',
10
+ anchorSeq: context.state.seq,
11
+ location: context.start?.location ?? { kind: 'unresolved' },
12
+ visibility,
13
+ data: context.state,
14
+ };
15
+ }
16
+ /**
17
+ * The member-speech Definition: one single-event Context per room/speech
18
+ * event (a CLI member's reply mirrored onto the blackboard).
19
+ */
20
+ export const roomSpeechDefinition = {
21
+ kind: 'room-speech',
22
+ target: 'chat',
23
+ match: event => event.type === 'room/speech'
24
+ ? { id: String(event.seq), role: 'start' }
25
+ : null,
26
+ start: (_context, match) => {
27
+ const event = match.event;
28
+ if (event.type !== 'room/speech')
29
+ throw new Error('room-speech start requires a room/speech event');
30
+ return {
31
+ seq: event.seq,
32
+ time: event.time,
33
+ member: event.data.member,
34
+ text: event.data.text,
35
+ ...event.data.interrupted === undefined ? {} : { interrupted: event.data.interrupted },
36
+ ...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
37
+ ...event.data.durationMs === undefined ? {} : { durationMs: event.data.durationMs },
38
+ };
39
+ },
40
+ update: context => context.state,
41
+ buildViewNode: context => viewNode(context, 'room-speech'),
42
+ };
43
+ /**
44
+ * The member-run Definition: one Context per run (matched on
45
+ * member+startedAt), started by the running edge and updated by its terminal
46
+ * edge. A running run renders the live row; a failed run stays as a dim
47
+ * error row; done/cancelled hide the row IN PLACE — the assembler's live
48
+ * incremental flush forbids withdrawing a materialized node with null (the
49
+ * withdrawal contract is the same key with `visibility: 'hidden'`; a null
50
+ * there throws and freezes the session's whole chat pipeline), while the
51
+ * speech row or the cancel's own affordances carry the outcome.
52
+ */
53
+ export const roomRunDefinition = {
54
+ kind: 'room-run',
55
+ target: 'chat',
56
+ match: event => event.type === 'room/run-state'
57
+ ? {
58
+ id: `${event.data.member}:${event.data.startedAt}`,
59
+ role: event.data.state === 'running' ? 'start' : 'update',
60
+ }
61
+ : null,
62
+ start: (_context, match) => {
63
+ const event = match.event;
64
+ if (event.type !== 'room/run-state')
65
+ throw new Error('room-run start requires a room/run-state event');
66
+ return {
67
+ seq: event.seq,
68
+ time: event.time,
69
+ member: event.data.member,
70
+ startedAt: event.data.startedAt,
71
+ state: event.data.state,
72
+ };
73
+ },
74
+ update: (context, match) => {
75
+ const event = match.event;
76
+ if (event.type !== 'room/run-state')
77
+ return context.state;
78
+ const { error: _error, elapsedMs: _elapsed, ...previous } = context.state;
79
+ return {
80
+ ...previous,
81
+ state: event.data.state,
82
+ ...event.data.elapsedMs === undefined ? {} : { elapsedMs: event.data.elapsedMs },
83
+ ...event.data.error === undefined ? {} : { error: event.data.error },
84
+ };
85
+ },
86
+ buildViewNode: context => viewNode(context, 'room-run', context.state !== undefined && (context.state.state === 'done' || context.state.state === 'cancelled')
87
+ ? 'hidden'
88
+ : 'visible'),
89
+ };
90
+ /**
91
+ * The boundary Definition: one single-event Context per member-added (cli
92
+ * members only — the auto-seated main agent is skipped) or member-removed
93
+ * event. `room/dispatch` stays unclaimed: the human's @-message renders
94
+ * through the official user node (it is also appended as `user/message`).
95
+ */
96
+ export const roomEventDefinition = {
97
+ kind: 'room-event',
98
+ target: 'chat',
99
+ match: (event) => {
100
+ switch (event.type) {
101
+ case 'room/member-added':
102
+ return event.data.kind === 'main-agent' ? null : { id: String(event.seq), role: 'start' };
103
+ case 'room/member-removed':
104
+ return { id: String(event.seq), role: 'start' };
105
+ default:
106
+ return null;
107
+ }
108
+ },
109
+ start: (_context, match) => {
110
+ const event = match.event;
111
+ switch (event.type) {
112
+ case 'room/member-added':
113
+ return {
114
+ seq: event.seq,
115
+ time: event.time,
116
+ sub: 'member-added',
117
+ member: event.data.name,
118
+ invitedBy: event.data.invitedBy,
119
+ ...event.data.provider === undefined ? {} : { provider: event.data.provider },
120
+ };
121
+ case 'room/member-removed':
122
+ return { seq: event.seq, time: event.time, sub: 'member-removed', member: event.data.name };
123
+ default:
124
+ throw new Error('room-event start requires a room boundary event');
125
+ }
126
+ },
127
+ update: context => context.state,
128
+ buildViewNode: context => viewNode(context, 'room-event'),
129
+ };
130
+ /**
131
+ * The relay Definition: one Context per relay (matched on the relay id),
132
+ * started by the `room/relay` event (pending) and updated by each
133
+ * `room/relay-resolved` edge — the gate row's state folds in place.
134
+ */
135
+ export const roomRelayDefinition = {
136
+ kind: 'room-relay',
137
+ target: 'chat',
138
+ match: (event) => {
139
+ switch (event.type) {
140
+ case 'room/relay':
141
+ return { id: event.data.id, role: 'start' };
142
+ case 'room/relay-resolved':
143
+ return { id: event.data.id, role: 'update' };
144
+ default:
145
+ return null;
146
+ }
147
+ },
148
+ start: (_context, match) => {
149
+ const event = match.event;
150
+ if (event.type !== 'room/relay')
151
+ throw new Error('room-relay start requires a room/relay event');
152
+ return {
153
+ seq: event.seq,
154
+ time: event.time,
155
+ relayId: event.data.id,
156
+ from: event.data.from,
157
+ to: event.data.to,
158
+ content: event.data.content,
159
+ state: 'pending',
160
+ };
161
+ },
162
+ update: (context, match) => {
163
+ const event = match.event;
164
+ if (event.type !== 'room/relay-resolved')
165
+ return context.state;
166
+ return { ...context.state, seq: event.seq, state: event.data.state };
167
+ },
168
+ buildViewNode: context => viewNode(context, 'room-relay'),
169
+ };
170
+ /**
171
+ * The task-advance Definition: one Context per task (matched on the task id),
172
+ * started by the `room/task-added` event and updated by each
173
+ * `room/task-updated` edge. The node stays HIDDEN while the task is open or
174
+ * cancelled and materializes — at the closing event's seq, so the line sits
175
+ * at the moment of completion — only when the task closes done: the dim
176
+ * advance line `✓ ada 完成了「API 定稿」── 目标进度 2/5`. (The goal progress
177
+ * suffix is computed by the renderer from the room store, not folded here.)
178
+ */
179
+ export const roomTaskLineDefinition = {
180
+ kind: 'room-task-line',
181
+ target: 'chat',
182
+ match: (event) => {
183
+ switch (event.type) {
184
+ case 'room/task-added':
185
+ return { id: event.data.id, role: 'start' };
186
+ case 'room/task-updated':
187
+ return { id: event.data.id, role: 'update' };
188
+ default:
189
+ return null;
190
+ }
191
+ },
192
+ start: (_context, match) => {
193
+ const event = match.event;
194
+ if (event.type !== 'room/task-added')
195
+ throw new Error('room-task-line start requires a room/task-added event');
196
+ return {
197
+ seq: event.seq,
198
+ time: event.time,
199
+ taskId: event.data.id,
200
+ member: event.data.member,
201
+ title: event.data.title,
202
+ status: event.data.status,
203
+ };
204
+ },
205
+ update: (context, match) => {
206
+ const event = match.event;
207
+ if (event.type !== 'room/task-updated')
208
+ return context.state;
209
+ return { ...context.state, seq: event.seq, time: event.time, status: event.data.status };
210
+ },
211
+ buildViewNode: context => viewNode(context, 'room-task-line', context.state !== undefined && context.state.status === 'done' ? 'visible' : 'hidden'),
212
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The preset-composition visibility of room's SESSION chrome (M3' self-hide,
3
+ * the mode-switcher proposal's "组合里有我的行"): the invite chip and the
4
+ * members tab show exactly when the CURRENT session's preset composition
5
+ * names the `@khorsheed/dsh-room-tool` row — the preset composition file is
6
+ * the single source of truth, read from the official `pluginInventory`
7
+ * Remote (the same criterion the worktrees badge implements; inlined here
8
+ * rather than extracted into a helper package, per the proposal's M3'
9
+ * deferral). Every unreadable path fails OPEN (visible): a host without the
10
+ * pluginInventory namespace, a pending/failed RPC, a missing or `broken`
11
+ * preset group, and sessions with no preset at all. An ACTUAL room session
12
+ * always shows its chrome regardless of preset — the gate hides the dev
13
+ * affordance in sessions that never granted it, never breaks an existing
14
+ * room (the E1 regression rule).
15
+ * @module @khorsheed/dsh-room/client/preset-visibility
16
+ */
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
19
+ /** The composition criterion's row: room's companion tool package. */
20
+ export declare const ROOM_TOOL_ROW_MODULE = "@khorsheed/dsh-room-tool";
21
+ /**
22
+ * Minimal structural mirror of the official pluginInventory snapshot — only
23
+ * the agentPresets slice the criterion reads (duck-typed, host-version
24
+ * tolerant: an extra field never breaks a structural read).
25
+ */
26
+ export interface RoomPluginInventorySnapshot {
27
+ readonly agentPresets?: readonly {
28
+ readonly id: string;
29
+ readonly broken?: string;
30
+ readonly rows: readonly {
31
+ readonly moduleName: string;
32
+ }[];
33
+ }[];
34
+ }
35
+ /** The visibility face the gated surfaces consume. */
36
+ export interface RoomChromeVisibility {
37
+ /**
38
+ * Whether room's session chrome shows for one session.
39
+ * @param sessionId - the session being rendered (undefined = no session
40
+ * context — fail-open, so registration-shape probes keep their entries).
41
+ */
42
+ show: (sessionId: SessionId | undefined) => boolean;
43
+ /** Subscribe to criterion-input changes (session switches, the inventory answer). */
44
+ subscribe: (listener: () => void) => () => void;
45
+ }
46
+ /**
47
+ * The session-chrome visibility controller: one inventory fetch per page
48
+ * (composition data changes only with preset files, which a reload re-reads
49
+ * anyway), re-evaluated by its consumers on every session switch.
50
+ */
51
+ export declare class RoomPresetVisibility implements RoomChromeVisibility {
52
+ private readonly ctx;
53
+ private readonly isRoom;
54
+ /** The inventory answer; null while pending/failed and on namespace-less hosts. */
55
+ private composition;
56
+ private readonly listeners;
57
+ /**
58
+ * @param ctx - client root context.
59
+ * @param isRoom - the RoomStore's cached room verdict (the E1 escape).
60
+ */
61
+ constructor(ctx: Context, isRoom: (sessionId: SessionId) => boolean);
62
+ show(sessionId: SessionId | undefined): boolean;
63
+ subscribe(listener: () => void): () => void;
64
+ private notify;
65
+ }
66
+ /**
67
+ * The members tab's registration toggle: the `conversation.view` tab strip's
68
+ * BUTTONS enumerate the slot's registrations (ui-conversation's `viewTabs()`
69
+ * has no per-session predicate), so a hidden tab means NO registration — a
70
+ * component returning null would leave the button with an empty body. The
71
+ * toggle registers exactly while the criterion passes for the current
72
+ * session and disposes otherwise; the slot's own subscription re-renders the
73
+ * strip (the same re-registration mechanism the composer takeover's
74
+ * promotion bump uses).
75
+ */
76
+ export declare class RegistrationToggle {
77
+ private readonly register;
78
+ private readonly show;
79
+ private dispose;
80
+ private ready;
81
+ /**
82
+ * @param register - mounts the entry and returns its disposer.
83
+ * @param show - the current decision (re-read on every sync).
84
+ */
85
+ constructor(register: () => () => void, show: () => boolean);
86
+ /** The slot declaration appeared (slots.inject's arm) — decisions may register now. */
87
+ setReady(ready: boolean): void;
88
+ /** Re-evaluate: register while shown and ready, dispose otherwise. */
89
+ sync(): void;
90
+ }
91
+ //# sourceMappingURL=preset-visibility.d.ts.map
@@ -0,0 +1,113 @@
1
+ /** The composition criterion's row: room's companion tool package. */
2
+ export const ROOM_TOOL_ROW_MODULE = '@khorsheed/dsh-room-tool';
3
+ /** The current session's agent preset, the badge's dual read per host line. */
4
+ function presetOf(row) {
5
+ const value = row
6
+ ?.projectionValues?.agentPreset
7
+ ?? row?.agentPreset;
8
+ return typeof value === 'string' ? value : undefined;
9
+ }
10
+ /**
11
+ * The session-chrome visibility controller: one inventory fetch per page
12
+ * (composition data changes only with preset files, which a reload re-reads
13
+ * anyway), re-evaluated by its consumers on every session switch.
14
+ */
15
+ export class RoomPresetVisibility {
16
+ ctx;
17
+ isRoom;
18
+ /** The inventory answer; null while pending/failed and on namespace-less hosts. */
19
+ composition = null;
20
+ listeners = new Set();
21
+ /**
22
+ * @param ctx - client root context.
23
+ * @param isRoom - the RoomStore's cached room verdict (the E1 escape).
24
+ */
25
+ constructor(ctx, isRoom) {
26
+ this.ctx = ctx;
27
+ this.isRoom = isRoom;
28
+ // The OFFICIAL pluginInventory namespace, probed through ctx.get (never
29
+ // injected — declaring 'remote.pluginInventory' would pend the whole
30
+ // client on a host without it, and the criterion must fail open there).
31
+ const pluginInventory = ctx.get('remote.pluginInventory');
32
+ if (pluginInventory !== undefined) {
33
+ void pluginInventory.list().then(result => {
34
+ if (!result.ok)
35
+ return;
36
+ this.composition = result.value;
37
+ this.notify();
38
+ }).catch(() => { });
39
+ }
40
+ // Session switches re-decide the members tab's registration (the tab
41
+ // strip's buttons come from the registration, not the component).
42
+ ctx.effect(() => ctx.sessions.list.subscribe(() => { this.notify(); }), 'room: preset-visibility session follower');
43
+ }
44
+ show(sessionId) {
45
+ if (sessionId === undefined)
46
+ return true;
47
+ // An actual room keeps its chrome whatever its preset granted (E1).
48
+ if (this.isRoom(sessionId))
49
+ return true;
50
+ const preset = presetOf(this.ctx.sessions.list.getSnapshot().byId[sessionId]);
51
+ if (preset === undefined)
52
+ return true;
53
+ if (this.composition === null)
54
+ return true;
55
+ const group = this.composition.agentPresets?.find(candidate => candidate.id === preset);
56
+ if (group === undefined || group.broken !== undefined)
57
+ return true;
58
+ return group.rows.some(row => row.moduleName === ROOM_TOOL_ROW_MODULE);
59
+ }
60
+ subscribe(listener) {
61
+ this.listeners.add(listener);
62
+ return () => { this.listeners.delete(listener); };
63
+ }
64
+ notify() {
65
+ for (const listener of this.listeners)
66
+ listener();
67
+ }
68
+ }
69
+ /**
70
+ * The members tab's registration toggle: the `conversation.view` tab strip's
71
+ * BUTTONS enumerate the slot's registrations (ui-conversation's `viewTabs()`
72
+ * has no per-session predicate), so a hidden tab means NO registration — a
73
+ * component returning null would leave the button with an empty body. The
74
+ * toggle registers exactly while the criterion passes for the current
75
+ * session and disposes otherwise; the slot's own subscription re-renders the
76
+ * strip (the same re-registration mechanism the composer takeover's
77
+ * promotion bump uses).
78
+ */
79
+ export class RegistrationToggle {
80
+ register;
81
+ show;
82
+ dispose;
83
+ ready = false;
84
+ /**
85
+ * @param register - mounts the entry and returns its disposer.
86
+ * @param show - the current decision (re-read on every sync).
87
+ */
88
+ constructor(register, show) {
89
+ this.register = register;
90
+ this.show = show;
91
+ }
92
+ /** The slot declaration appeared (slots.inject's arm) — decisions may register now. */
93
+ setReady(ready) {
94
+ this.ready = ready;
95
+ if (!ready && this.dispose !== undefined) {
96
+ this.dispose();
97
+ this.dispose = undefined;
98
+ }
99
+ this.sync();
100
+ }
101
+ /** Re-evaluate: register while shown and ready, dispose otherwise. */
102
+ sync() {
103
+ if (!this.ready)
104
+ return;
105
+ if (this.show()) {
106
+ this.dispose ??= this.register();
107
+ }
108
+ else if (this.dispose !== undefined) {
109
+ this.dispose();
110
+ this.dispose = undefined;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,7 @@
1
+ /** Keep one retry identity per room draft until the server acknowledges it. */
2
+ export declare class RoomRequestIds {
3
+ private readonly pending;
4
+ forInput(sessionId: string, text: string, targets?: readonly string[]): string;
5
+ complete(sessionId: string, id: string): void;
6
+ }
7
+ //# sourceMappingURL=request-ids.d.ts.map
@@ -0,0 +1,21 @@
1
+ /** Keep one retry identity per room draft until the server acknowledges it. */
2
+ export class RoomRequestIds {
3
+ pending = new Map();
4
+ forInput(sessionId, text, targets = []) {
5
+ const signature = JSON.stringify({ text: text.trim(), targets: [...new Set(targets)].sort() });
6
+ const known = this.pending.get(sessionId);
7
+ if (known?.signature === signature)
8
+ return known.id;
9
+ const id = typeof globalThis.crypto.randomUUID === 'function' ? globalThis.crypto.randomUUID()
10
+ : Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), value => value.toString(16).padStart(2, '0')).join('');
11
+ this.pending.delete(sessionId);
12
+ this.pending.set(sessionId, { signature, id });
13
+ if (this.pending.size > 128)
14
+ this.pending.delete(this.pending.keys().next().value);
15
+ return id;
16
+ }
17
+ complete(sessionId, id) {
18
+ if (this.pending.get(sessionId)?.id === id)
19
+ this.pending.delete(sessionId);
20
+ }
21
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Client-side room state store: the synchronous cache the composer chain
3
+ * selector reads (selectors cannot await), plus the per-session RoomState
4
+ * the room components render. Fed by the mounted room Remote: a first pull
5
+ * (`isRoom`, then `getState` when true) per session, a refresh after every
6
+ * own mutation, a realtime refresh trigger off the current session's live
7
+ * conversation feed, and a light 2s poll of `getState` only while the
8
+ * CURRENT session is a room with a running member (no running members, no
9
+ * polling). The live feed: the host pushes EVERY session event — room
10
+ * journal appends included — as `session/event` frames, which the runtime
11
+ * folds into the client Session's conversation snapshot; subscribing that
12
+ * snapshot and refreshing (300ms debounced) is how host-side mutations the
13
+ * client never initiated (the `room_task`/`room_invite` tools, the
14
+ * local-agent member bridge) land in the dock within a beat. A cache miss
15
+ * reads as `undefined`, and the composer selector declines on a miss: the
16
+ * first instant of a freshly opened room shows the official input bar until
17
+ * the first pull lands (accepted, documented).
18
+ * @module @khorsheed/dsh-room/client/room-store
19
+ */
20
+ import type { Context } from '@deepseek-ai/cordis';
21
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
22
+ import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
23
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
24
+ import type { RoomGetStateRequest, RoomGetStateResult, RoomIsRoomRequest, RoomState } from '../types.ts';
25
+ /**
26
+ * The session the main view shows, read off both host lines: alpha.2 deleted
27
+ * the list's `current` for the per-row `retainedBy.mainView` reference count;
28
+ * 0.1.5 carries only `current`.
29
+ * @param list - the sessions list snapshot.
30
+ * @returns the main-view session id, or undefined when no session is shown.
31
+ */
32
+ export declare function mainSessionId(list: SessionListState): SessionId | undefined;
33
+ /** The slice of the mounted room Remote the store reads. */
34
+ export interface RoomGateway {
35
+ isRoom(request: RoomIsRoomRequest): Promise<RemoteResult<boolean>>;
36
+ getState(request: RoomGetStateRequest): Promise<RemoteResult<RoomGetStateResult>>;
37
+ }
38
+ /** Poll cadence while the current room has running members. */
39
+ export declare const ROOM_POLL_INTERVAL_MS = 2000;
40
+ /**
41
+ * Debounce coalescing the live-feed refresh: one room journal append usually
42
+ * arrives inside a burst of session events (the tool call, its result, the
43
+ * task/task-update edges), so the refresh waits for a quiet beat.
44
+ */
45
+ export declare const ROOM_LIVE_REFRESH_DEBOUNCE_MS = 300;
46
+ export declare class RoomStore {
47
+ private readonly ctx;
48
+ private readonly gateway;
49
+ private readonly room;
50
+ private readonly states;
51
+ private readonly listeners;
52
+ private poller;
53
+ private current;
54
+ /** The session the live feed is subscribed to (undefined = unattached). */
55
+ private liveAttached;
56
+ private liveUnsubscribe;
57
+ private liveTimer;
58
+ /**
59
+ * @param ctx - client root context (the sessions list feed).
60
+ * @param gateway - the mounted room Remote, undefined when the mount failed.
61
+ */
62
+ constructor(ctx: Context, gateway: RoomGateway | undefined);
63
+ /**
64
+ * Follow the current session: a new current room session gets its first
65
+ * pull and its live-feed subscription, and polling re-evaluates on every
66
+ * switch.
67
+ * @returns disposer (unsubscribe everything + stop polling).
68
+ */
69
+ start(): () => void;
70
+ /**
71
+ * The composer selector's synchronous read.
72
+ * @param sessionId - session to probe.
73
+ * @returns cached verdict, or undefined while the first pull is in flight.
74
+ */
75
+ isRoomCached(sessionId: SessionId): boolean | undefined;
76
+ /**
77
+ * The cached room state (stable reference between refreshes — safe as a
78
+ * useSyncExternalStore snapshot).
79
+ * @param sessionId - room session.
80
+ * @returns the cached state, or undefined before the first successful pull.
81
+ */
82
+ getCached(sessionId: SessionId): RoomState | undefined;
83
+ /**
84
+ * Subscribe to cache changes.
85
+ * @param listener - called after any map mutation.
86
+ * @returns unsubscribe.
87
+ */
88
+ readonly subscribe: (listener: () => void) => (() => void);
89
+ /**
90
+ * First pull for a session: isRoom, then getState when it is one.
91
+ * No-ops on a cached verdict.
92
+ * @param sessionId - session to ensure.
93
+ */
94
+ ensure(sessionId: SessionId): Promise<void>;
95
+ /**
96
+ * Re-pull the room state (after an own mutation, a live-feed nudge, or a
97
+ * poll tick).
98
+ * @param sessionId - room session.
99
+ */
100
+ refresh(sessionId: SessionId): Promise<void>;
101
+ /**
102
+ * Subscribe the CURRENT session's live conversation feed so host-side
103
+ * journal appends nudge a refresh. The session binding is minted lazily by
104
+ * the runtime (it can lag the list's current flip), so every completed pull
105
+ * re-attempts the attach; an unattached store simply keeps its other
106
+ * triggers until one lands.
107
+ */
108
+ private attachLive;
109
+ /** Drop the live-feed subscription and any pending debounced refresh. */
110
+ private detachLive;
111
+ /**
112
+ * One live-feed nudge, debounced so an event burst costs one probe: a known
113
+ * room refreshes its state; a session cached NOT a room re-probes `isRoom`
114
+ * (a promotion lands as a host-side `room/created` append the client never
115
+ * initiated — the room_invite/room_message tools promote in place); a
116
+ * unknown verdict retries ensure after the same debounce.
117
+ */
118
+ private onLiveEvent;
119
+ /**
120
+ * Re-probe a cached-non-room session's verdict: a promotion flips the cache
121
+ * and pulls the state (the composer takeover follows on the next notify).
122
+ * @param sessionId - the session whose verdict may have changed.
123
+ */
124
+ private reprobe;
125
+ private notify;
126
+ /**
127
+ * Called when a session's verdict flips unknown/false → true (an in-place
128
+ * promotion): the composer chain elects at RENDER time and nothing
129
+ * re-renders the outlet for a promotion of an idle session, so the client
130
+ * wires this to re-register the composer entry (a slot version bump
131
+ * re-renders — and re-elects — the outlet).
132
+ */
133
+ onPromoted: (() => void) | undefined;
134
+ private adjustPolling;
135
+ private stopPolling;
136
+ }
137
+ //# sourceMappingURL=room-store.d.ts.map