@khorsheed/dsh-room 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.en.md +29 -62
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +28 -61
  5. package/lib/client.js +34057 -6483
  6. package/lib/index.js +4045 -730
  7. package/lib/journal-B4FOGO6S.js +482 -0
  8. package/lib/journal-Rid9AAnD.js +485 -0
  9. package/lib/tool.js +112 -166
  10. package/lib/typert.host.js +2517 -363
  11. package/lib/typert.remote-client.d.ts +53 -9
  12. package/lib/typert.remote-client.js +1909 -280
  13. package/lib/types/agent-setup.d.ts +1 -1
  14. package/lib/types/client/InviteAgentAction.js +4 -16
  15. package/lib/types/client/InviteDialog.d.ts +28 -6
  16. package/lib/types/client/InviteDialog.js +76 -12
  17. package/lib/types/client/MemberCard.d.ts +5 -1
  18. package/lib/types/client/MemberCard.js +4 -4
  19. package/lib/types/client/MembersView.d.ts +1 -1
  20. package/lib/types/client/MembersView.js +58 -22
  21. package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
  22. package/lib/types/client/RemoveMemberConfirm.js +85 -0
  23. package/lib/types/client/RoomApprovalView.d.ts +11 -0
  24. package/lib/types/client/RoomApprovalView.js +28 -0
  25. package/lib/types/client/RoomComposer.d.ts +1 -1
  26. package/lib/types/client/RoomComposer.js +228 -37
  27. package/lib/types/client/RoomDecisionView.d.ts +22 -0
  28. package/lib/types/client/RoomDecisionView.js +35 -0
  29. package/lib/types/client/RoomHelpView.d.ts +17 -0
  30. package/lib/types/client/RoomHelpView.js +56 -0
  31. package/lib/types/client/RoomInboxView.d.ts +10 -0
  32. package/lib/types/client/RoomInboxView.js +14 -0
  33. package/lib/types/client/RoomQuestionView.d.ts +13 -0
  34. package/lib/types/client/RoomQuestionView.js +61 -0
  35. package/lib/types/client/RoomRelayView.js +1 -1
  36. package/lib/types/client/RoomRunView.d.ts +3 -1
  37. package/lib/types/client/RoomRunView.js +12 -5
  38. package/lib/types/client/RoomSettingsView.d.ts +19 -0
  39. package/lib/types/client/RoomSettingsView.js +30 -0
  40. package/lib/types/client/RoomSpeechView.d.ts +22 -8
  41. package/lib/types/client/RoomSpeechView.js +94 -11
  42. package/lib/types/client/RoomStatsLine.d.ts +26 -5
  43. package/lib/types/client/RoomStatsLine.js +53 -7
  44. package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
  45. package/lib/types/client/RoomTaskCapsule.js +37 -0
  46. package/lib/types/client/TasksTab.d.ts +41 -0
  47. package/lib/types/client/TasksTab.js +43 -0
  48. package/lib/types/client/TasksView.d.ts +44 -0
  49. package/lib/types/client/TasksView.js +149 -0
  50. package/lib/types/client/TeamView.d.ts +33 -0
  51. package/lib/types/client/TeamView.js +210 -0
  52. package/lib/types/client/at-menu.d.ts +127 -0
  53. package/lib/types/client/at-menu.js +173 -0
  54. package/lib/types/client/draft-chips.d.ts +67 -0
  55. package/lib/types/client/draft-chips.js +95 -0
  56. package/lib/types/client/executions.d.ts +0 -30
  57. package/lib/types/client/executions.js +4 -34
  58. package/lib/types/client/icons-local.d.ts +25 -0
  59. package/lib/types/client/icons-local.js +10 -0
  60. package/lib/types/client/icons.d.ts +2 -1
  61. package/lib/types/client/icons.js +1 -0
  62. package/lib/types/client/index.js +276 -72
  63. package/lib/types/client/locales.d.ts +482 -52
  64. package/lib/types/client/locales.js +490 -60
  65. package/lib/types/client/nodes.d.ts +112 -17
  66. package/lib/types/client/nodes.js +197 -26
  67. package/lib/types/client/preset-visibility.d.ts +12 -2
  68. package/lib/types/client/preset-visibility.js +54 -13
  69. package/lib/types/client/slots.d.ts +124 -51
  70. package/lib/types/client/task-tag.d.ts +32 -0
  71. package/lib/types/client/task-tag.js +33 -0
  72. package/lib/types/client/tasks.d.ts +50 -0
  73. package/lib/types/client/tasks.js +82 -0
  74. package/lib/types/client/team.d.ts +73 -0
  75. package/lib/types/client/team.js +187 -0
  76. package/lib/types/decisions.d.ts +49 -0
  77. package/lib/types/decisions.js +101 -0
  78. package/lib/types/deliveries.d.ts +17 -1
  79. package/lib/types/deliveries.js +82 -6
  80. package/lib/types/dispatch.d.ts +92 -4
  81. package/lib/types/dispatch.js +477 -109
  82. package/lib/types/help.d.ts +137 -0
  83. package/lib/types/help.js +216 -0
  84. package/lib/types/helps.d.ts +26 -0
  85. package/lib/types/helps.js +47 -0
  86. package/lib/types/index.d.ts +292 -52
  87. package/lib/types/index.js +1415 -199
  88. package/lib/types/jobs.d.ts +22 -0
  89. package/lib/types/jobs.js +77 -0
  90. package/lib/types/journal.d.ts +17 -27
  91. package/lib/types/journal.js +120 -43
  92. package/lib/types/messaging.d.ts +65 -0
  93. package/lib/types/messaging.js +160 -0
  94. package/lib/types/plan-service.js +7 -1
  95. package/lib/types/report-gate.d.ts +81 -0
  96. package/lib/types/report-gate.js +89 -0
  97. package/lib/types/tool-definition.d.ts +43 -20
  98. package/lib/types/tool-definition.js +145 -212
  99. package/lib/types/tool.d.ts +2 -2
  100. package/lib/types/tool.js +3 -2
  101. package/lib/types/types.d.ts +755 -78
  102. package/locale/zh.json +1 -1
  103. package/package.json +27 -29
  104. package/lib/invariant.js +0 -26
  105. package/lib/types/client/RoomActivityDock.d.ts +0 -19
  106. package/lib/types/client/RoomActivityDock.js +0 -105
  107. package/lib/types/client/RoomDockCapsules.d.ts +0 -64
  108. package/lib/types/client/RoomDockCapsules.js +0 -277
  109. package/lib/types/client/RoomPlanTab.d.ts +0 -15
  110. package/lib/types/client/RoomPlanTab.js +0 -13
  111. package/lib/types/client/RoomTaskLineView.d.ts +0 -13
  112. package/lib/types/client/RoomTaskLineView.js +0 -21
  113. package/lib/types/invariant.d.ts +0 -16
  114. package/lib/types/invariant.js +0 -24
@@ -11,12 +11,15 @@
11
11
  * RoomService.postMessage) and renders through the official user node.
12
12
  * @module @khorsheed/dsh-room/client/nodes
13
13
  */
14
+ import type { RoomDecisionEvent, RoomHelp, RoomQuestionAnswer, RoomQuestionItem } from '../types.ts';
14
15
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
15
16
  import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
16
17
  /** Chat node data of one member speech row. */
17
18
  export interface RoomSpeechData {
18
19
  /** Partial output from a stopped or failed run; never a completed answer. */
19
20
  readonly interrupted?: 'cancelled' | 'failed';
21
+ /** The member this reply was also reported to. */
22
+ readonly reportedTo?: string;
20
23
  /** Seq of the room/speech event. */
21
24
  readonly seq: number;
22
25
  /** Unix epoch ms from the event. */
@@ -27,6 +30,10 @@ export interface RoomSpeechData {
27
30
  readonly childSessionId?: SessionId;
28
31
  /** Dispatch→settle milliseconds, when measured. */
29
32
  readonly durationMs?: number;
33
+ /** The run this speech closed: its execution metadata (tokens) is keyed by it. */
34
+ readonly runId?: string;
35
+ /** The member's closing message in its own session. */
36
+ readonly messageId?: string;
30
37
  }
31
38
  /** Chat node data of one member run row. */
32
39
  export interface RoomRunData {
@@ -57,6 +64,55 @@ export interface RoomEventData {
57
64
  readonly invitedBy?: 'human' | 'agent';
58
65
  }
59
66
  /** Chat node data of one member-to-member notification relay row. */
67
+ /** Chat node data of a member's tool approval (the 允许 / 拒绝 line). */
68
+ export interface RoomApprovalData {
69
+ readonly seq: number;
70
+ readonly time: number;
71
+ readonly approvalId: string;
72
+ readonly member: string;
73
+ readonly tool: string;
74
+ readonly reason?: string;
75
+ readonly state: 'pending' | 'allowed-once' | 'rejected' | 'cancelled';
76
+ }
77
+ /** Chat node data of one decision the room made on its own (a timeline row). */
78
+ export interface RoomDecisionData {
79
+ readonly seq: number;
80
+ readonly time: number;
81
+ readonly decision: RoomDecisionEvent;
82
+ }
83
+ /** Chat node data of a cross-session help thread, in the asking room. */
84
+ export interface RoomHelpData {
85
+ readonly seq: number;
86
+ readonly time: number;
87
+ readonly help: RoomHelp;
88
+ }
89
+ /** Chat node data of a help request (or a follow-up on it), in the helper's own session. */
90
+ export interface RoomHelpRequestData {
91
+ readonly seq: number;
92
+ readonly time: number;
93
+ readonly help: string;
94
+ /** The asking room's session and title. */
95
+ readonly room: string;
96
+ readonly roomTitle: string;
97
+ /** The asking member. */
98
+ readonly from: string;
99
+ readonly op: 'request' | 'message';
100
+ /** The asker's own words. */
101
+ readonly text: string;
102
+ /** Another room asked this room: the member it went to, and whether the paused room holds it. */
103
+ readonly member?: string;
104
+ readonly held?: boolean;
105
+ }
106
+ /** Chat node data of a member's question card (pick-to-answer). */
107
+ export interface RoomQuestionData {
108
+ readonly seq: number;
109
+ readonly time: number;
110
+ readonly questionId: string;
111
+ readonly member: string;
112
+ readonly questions: readonly RoomQuestionItem[];
113
+ readonly state: 'pending' | 'answered' | 'cancelled';
114
+ readonly answers?: readonly RoomQuestionAnswer[];
115
+ }
60
116
  export interface RoomRelayData {
61
117
  /** Seq of the relay's latest journal event. */
62
118
  readonly seq: number;
@@ -68,17 +124,19 @@ export interface RoomRelayData {
68
124
  readonly to: string;
69
125
  readonly content: string;
70
126
  readonly state: 'pending' | 'confirmed' | 'dismissed' | 'sent';
127
+ /** Why the message waits (absent on legacy relays). */
128
+ readonly reason?: 'confirm' | 'hop-limit' | 'paused';
71
129
  }
72
- /** Chat node data of one task-advance line (the dim "✓ ada 完成了「…」" row). */
73
- export interface RoomTaskLineData {
74
- /** Seq of the task's LATEST journal event (add or the closing update). */
130
+ /** Chat node data of the held-reports line for one member. */
131
+ export interface RoomInboxData {
132
+ /** Seq of the latest hold (the line sits there). */
75
133
  readonly seq: number;
76
- /** Unix epoch ms of that event. */
77
134
  readonly time: number;
78
- readonly taskId: string;
79
135
  readonly member: string;
80
- readonly title: string;
81
- readonly status: 'pending' | 'in_progress' | 'done' | 'cancelled' | 'failed';
136
+ /** Held dispatch seqs not yet released. */
137
+ readonly held: readonly number[];
138
+ /** The model's yes-probability on the latest hold, when it answered. */
139
+ readonly probability?: number;
82
140
  }
83
141
  declare module '@deepseek-ai/dsh-client-ui-chat/client' {
84
142
  interface ChatNodeDataMap {
@@ -90,8 +148,18 @@ declare module '@deepseek-ai/dsh-client-ui-chat/client' {
90
148
  'room-event': RoomEventData;
91
149
  /** room: a member-to-member notification relay (gate row). */
92
150
  'room-relay': RoomRelayData;
93
- /** room: a task closure's advance line (visible only once done). */
94
- 'room-task-line': RoomTaskLineData;
151
+ /** room: a member's mode tool waiting for the human's approval. */
152
+ 'room-approval': RoomApprovalData;
153
+ /** room: a decision the room made on its own (rule or decision model). */
154
+ 'room-decision': RoomDecisionData;
155
+ /** room: a member's questions for the human (pick-to-answer card). */
156
+ 'room-question': RoomQuestionData;
157
+ /** room: a help thread this room opened with another session. */
158
+ 'room-help': RoomHelpData;
159
+ /** room: a help request another session's room sent this session. */
160
+ 'room-help-request': RoomHelpRequestData;
161
+ /** room: reports the gate holds in a member's inbox. */
162
+ 'room-inbox': RoomInboxData;
95
163
  }
96
164
  }
97
165
  /**
@@ -117,6 +185,18 @@ export declare const roomRunDefinition: ConversationNodeDefinition<RoomRunData>;
117
185
  * through the official user node (it is also appended as `user/message`).
118
186
  */
119
187
  export declare const roomEventDefinition: ConversationNodeDefinition<RoomEventData>;
188
+ /** A room decision: one single-event Context per `room/decision`. */
189
+ export declare const roomDecisionDefinition: ConversationNodeDefinition<RoomDecisionData>;
190
+ /**
191
+ * A member's tool approval: one Context per approval, started by
192
+ * `room/approval` and settled in place by `room/approval-resolved`.
193
+ */
194
+ export declare const roomApprovalDefinition: ConversationNodeDefinition<RoomApprovalData>;
195
+ /**
196
+ * A member's question card: one Context per card, started by `room/question`
197
+ * and settled in place (with the answers) by `room/question-resolved`.
198
+ */
199
+ export declare const roomQuestionDefinition: ConversationNodeDefinition<RoomQuestionData>;
120
200
  /**
121
201
  * The relay Definition: one Context per relay (matched on the relay id),
122
202
  * started by the `room/relay` event (pending) and updated by each
@@ -124,13 +204,28 @@ export declare const roomEventDefinition: ConversationNodeDefinition<RoomEventDa
124
204
  */
125
205
  export declare const roomRelayDefinition: ConversationNodeDefinition<RoomRelayData>;
126
206
  /**
127
- * The task-advance Definition: one Context per task (matched on the task id),
128
- * started by the `room/task-added` event and updated by each
129
- * `room/task-updated` edge. The node stays HIDDEN while the task is open or
130
- * cancelled and materializes — at the closing event's seq, so the line sits
131
- * at the moment of completion — only when the task closes done: the dim
132
- * advance line `✓ ada 完成了「API 定稿」── 目标进度 2/5`. (The goal progress
133
- * suffix is computed by the renderer from the room store, not folded here.)
207
+ * The held-reports line: one Context per member (its gate verdicts), visible
208
+ * while the gate holds reports for it — 「📥 dsh 的收件箱里有 2 条回报,先攒着」
209
+ * with a 现在交给 button — and hidden once they are released. It moves to the
210
+ * latest hold, so it always sits after the reply that was held.
211
+ */
212
+ export declare const roomInboxDefinition: ConversationNodeDefinition<RoomInboxData>;
213
+ /**
214
+ * A help thread in the asking room: one Context per thread, started by
215
+ * `room/help` and folded in place by its `room/help-message` and
216
+ * `room/help-state` events (one row whose state and messages update).
217
+ */
218
+ export declare const roomHelpDefinition: ConversationNodeDefinition<RoomHelpData>;
219
+ /**
220
+ * A help request in the helper's session: the `user/message` the room queued
221
+ * there (its `room-help` source) renders as a card naming the asking room
222
+ * instead of a plain injected-context row.
223
+ */
224
+ export declare const roomHelpRequestDefinition: ConversationNodeDefinition<RoomHelpRequestData>;
225
+ /**
226
+ * Another room asked this room for help (`room/help-in`): the same request
227
+ * card, naming the member it went to (and that it waits while this room is
228
+ * paused), with a jump back to the asking room.
134
229
  */
135
- export declare const roomTaskLineDefinition: ConversationNodeDefinition<RoomTaskLineData>;
230
+ export declare const roomHelpInDefinition: ConversationNodeDefinition<RoomHelpRequestData>;
136
231
  //# sourceMappingURL=nodes.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
1
2
  /** The shared final-node materialization (single shape across the three kinds). */
2
3
  function viewNode(context, kind, visibility = 'visible') {
3
4
  if (context.state === undefined)
@@ -33,8 +34,11 @@ export const roomSpeechDefinition = {
33
34
  member: event.data.member,
34
35
  text: event.data.text,
35
36
  ...event.data.interrupted === undefined ? {} : { interrupted: event.data.interrupted },
37
+ ...event.data.reportedTo === undefined ? {} : { reportedTo: event.data.reportedTo },
36
38
  ...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
37
39
  ...event.data.durationMs === undefined ? {} : { durationMs: event.data.durationMs },
40
+ ...event.data.runId === undefined ? {} : { runId: event.data.runId },
41
+ ...event.data.messageId === undefined ? {} : { messageId: event.data.messageId },
38
42
  };
39
43
  },
40
44
  update: context => context.state,
@@ -127,6 +131,86 @@ export const roomEventDefinition = {
127
131
  update: context => context.state,
128
132
  buildViewNode: context => viewNode(context, 'room-event'),
129
133
  };
134
+ /** A room decision: one single-event Context per `room/decision`. */
135
+ export const roomDecisionDefinition = {
136
+ kind: 'room-decision',
137
+ target: 'chat',
138
+ match: event => event.type === 'room/decision' ? { id: event.data.id, role: 'start' } : null,
139
+ start: (_context, match) => {
140
+ const event = match.event;
141
+ if (event.type !== 'room/decision')
142
+ throw new Error('room-decision start requires a room/decision event');
143
+ return { seq: event.seq, time: event.time, decision: event.data };
144
+ },
145
+ update: context => context.state,
146
+ buildViewNode: context => viewNode(context, 'room-decision'),
147
+ };
148
+ /**
149
+ * A member's tool approval: one Context per approval, started by
150
+ * `room/approval` and settled in place by `room/approval-resolved`.
151
+ */
152
+ export const roomApprovalDefinition = {
153
+ kind: 'room-approval',
154
+ target: 'chat',
155
+ match: (event) => {
156
+ switch (event.type) {
157
+ case 'room/approval':
158
+ return { id: event.data.id, role: 'start' };
159
+ case 'room/approval-resolved':
160
+ return { id: event.data.id, role: 'update' };
161
+ default:
162
+ return null;
163
+ }
164
+ },
165
+ start: (_context, match) => {
166
+ const event = match.event;
167
+ if (event.type !== 'room/approval')
168
+ throw new Error('room-approval start requires a room/approval event');
169
+ return {
170
+ seq: event.seq, time: event.time, approvalId: event.data.id, member: event.data.member, tool: event.data.tool,
171
+ ...event.data.reason === undefined ? {} : { reason: event.data.reason },
172
+ state: 'pending',
173
+ };
174
+ },
175
+ update: (context, match) => {
176
+ const event = match.event;
177
+ if (event.type !== 'room/approval-resolved')
178
+ return context.state;
179
+ return { ...context.state, state: event.data.outcome };
180
+ },
181
+ buildViewNode: context => viewNode(context, 'room-approval'),
182
+ };
183
+ /**
184
+ * A member's question card: one Context per card, started by `room/question`
185
+ * and settled in place (with the answers) by `room/question-resolved`.
186
+ */
187
+ export const roomQuestionDefinition = {
188
+ kind: 'room-question',
189
+ target: 'chat',
190
+ match: (event) => {
191
+ switch (event.type) {
192
+ case 'room/question':
193
+ return { id: event.data.id, role: 'start' };
194
+ case 'room/question-resolved':
195
+ return { id: event.data.id, role: 'update' };
196
+ default:
197
+ return null;
198
+ }
199
+ },
200
+ start: (_context, match) => {
201
+ const event = match.event;
202
+ if (event.type !== 'room/question')
203
+ throw new Error('room-question start requires a room/question event');
204
+ return { seq: event.seq, time: event.time, questionId: event.data.id, member: event.data.member, questions: event.data.questions, state: 'pending' };
205
+ },
206
+ update: (context, match) => {
207
+ const event = match.event;
208
+ if (event.type !== 'room/question-resolved')
209
+ return context.state;
210
+ return { ...context.state, state: event.data.outcome, ...event.data.answers === undefined ? {} : { answers: event.data.answers } };
211
+ },
212
+ buildViewNode: context => viewNode(context, 'room-question'),
213
+ };
130
214
  /**
131
215
  * The relay Definition: one Context per relay (matched on the relay id),
132
216
  * started by the `room/relay` event (pending) and updated by each
@@ -156,6 +240,7 @@ export const roomRelayDefinition = {
156
240
  from: event.data.from,
157
241
  to: event.data.to,
158
242
  content: event.data.content,
243
+ ...event.data.reason === undefined ? {} : { reason: event.data.reason },
159
244
  state: 'pending',
160
245
  };
161
246
  },
@@ -168,45 +253,131 @@ export const roomRelayDefinition = {
168
253
  buildViewNode: context => viewNode(context, 'room-relay'),
169
254
  };
170
255
  /**
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.)
256
+ * The held-reports line: one Context per member (its gate verdicts), visible
257
+ * while the gate holds reports for it — 「📥 dsh 的收件箱里有 2 条回报,先攒着」
258
+ * with a 现在交给 button — and hidden once they are released. It moves to the
259
+ * latest hold, so it always sits after the reply that was held.
178
260
  */
179
- export const roomTaskLineDefinition = {
180
- kind: 'room-task-line',
261
+ export const roomInboxDefinition = {
262
+ kind: 'room-inbox',
263
+ target: 'chat',
264
+ match: (event) => {
265
+ if (event.type !== 'room/report-gate' || event.data.verdict === 'deliver')
266
+ return null;
267
+ return { id: event.data.memberId, role: event.data.verdict === 'hold' ? 'start' : 'update' };
268
+ },
269
+ start: (context, match) => {
270
+ const event = match.event;
271
+ if (event.type !== 'room/report-gate')
272
+ throw new Error('room-inbox requires a room/report-gate event');
273
+ const previous = context.state?.held ?? [];
274
+ return {
275
+ seq: event.seq, time: event.time, member: event.data.member,
276
+ held: [...previous.filter(seq => seq !== event.data.dispatchSeq), event.data.dispatchSeq],
277
+ ...event.data.probability === undefined ? {} : { probability: event.data.probability },
278
+ };
279
+ },
280
+ update: (context, match) => {
281
+ const event = match.event;
282
+ if (event.type !== 'room/report-gate' || context.state === undefined)
283
+ return context.state;
284
+ if (event.data.verdict === 'hold') {
285
+ return { ...context.state, seq: event.seq, time: event.time, held: [...context.state.held.filter(seq => seq !== event.data.dispatchSeq), event.data.dispatchSeq],
286
+ ...event.data.probability === undefined ? {} : { probability: event.data.probability } };
287
+ }
288
+ return { ...context.state, held: context.state.held.filter(seq => seq !== event.data.dispatchSeq) };
289
+ },
290
+ buildViewNode: context => viewNode(context, 'room-inbox', context.state !== undefined && context.state.held.length > 0 ? 'visible' : 'hidden'),
291
+ };
292
+ /**
293
+ * A help thread in the asking room: one Context per thread, started by
294
+ * `room/help` and folded in place by its `room/help-message` and
295
+ * `room/help-state` events (one row whose state and messages update).
296
+ */
297
+ export const roomHelpDefinition = {
298
+ kind: 'room-help',
181
299
  target: 'chat',
182
300
  match: (event) => {
183
301
  switch (event.type) {
184
- case 'room/task-added':
302
+ case 'room/help':
185
303
  return { id: event.data.id, role: 'start' };
186
- case 'room/task-updated':
187
- return { id: event.data.id, role: 'update' };
304
+ case 'room/help-message':
305
+ case 'room/help-state':
306
+ return { id: event.data.help, role: 'update' };
188
307
  default:
189
308
  return null;
190
309
  }
191
310
  },
192
311
  start: (_context, match) => {
193
312
  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
- };
313
+ if (event.type !== 'room/help')
314
+ throw new Error('room-help start requires a room/help event');
315
+ return { seq: event.seq, time: event.time, help: { ...event.data, state: 'queued', since: event.time, messages: [], time: event.time } };
204
316
  },
205
317
  update: (context, match) => {
206
318
  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 };
319
+ const help = context.state.help;
320
+ if (event.type === 'room/help-message') {
321
+ return { ...context.state, help: { ...help, messages: [...help.messages, { ...event.data, time: event.time }] } };
322
+ }
323
+ if (event.type === 'room/help-state') {
324
+ const { error: _previous, ...rest } = help;
325
+ return { ...context.state, help: { ...rest, state: event.data.state, since: help.state === event.data.state ? help.since : event.time, ...event.data.error === undefined ? {} : { error: event.data.error } } };
326
+ }
327
+ return context.state;
328
+ },
329
+ buildViewNode: context => viewNode(context, 'room-help'),
330
+ };
331
+ /** A message source's help fields, when it is a room help source. */
332
+ function helpSourceOf(source) {
333
+ const value = source;
334
+ if (value?.kind !== 'room-help' || typeof value.help !== 'string' || typeof value.room !== 'string')
335
+ return undefined;
336
+ const text = (key) => typeof value[key] === 'string' ? value[key] : '';
337
+ return { help: value.help, room: value.room, roomTitle: text('roomTitle'), from: text('from'), op: value.op === 'message' ? 'message' : 'request', text: text('text') };
338
+ }
339
+ /**
340
+ * A help request in the helper's session: the `user/message` the room queued
341
+ * there (its `room-help` source) renders as a card naming the asking room
342
+ * instead of a plain injected-context row.
343
+ */
344
+ export const roomHelpRequestDefinition = {
345
+ kind: 'room-help-request',
346
+ target: 'chat',
347
+ match: (event) => {
348
+ if (event.type !== 'user/message' || !isAppendSurfaceEvent(event) || helpSourceOf(event.data.source) === undefined)
349
+ return null;
350
+ return { id: String(event.seq), role: 'start' };
351
+ },
352
+ start: (_context, match) => {
353
+ const event = match.event;
354
+ const source = event.type === 'user/message' ? helpSourceOf(event.data.source) : undefined;
355
+ if (source === undefined)
356
+ throw new Error('room-help-request start requires a room-help message');
357
+ return { seq: event.seq, time: event.time, ...source };
210
358
  },
211
- buildViewNode: context => viewNode(context, 'room-task-line', context.state !== undefined && context.state.status === 'done' ? 'visible' : 'hidden'),
359
+ update: context => context.state,
360
+ buildViewNode: context => viewNode(context, 'room-help-request'),
361
+ };
362
+ /**
363
+ * Another room asked this room for help (`room/help-in`): the same request
364
+ * card, naming the member it went to (and that it waits while this room is
365
+ * paused), with a jump back to the asking room.
366
+ */
367
+ export const roomHelpInDefinition = {
368
+ kind: 'room-help-in',
369
+ target: 'chat',
370
+ match: event => event.type === 'room/help-in' ? { id: String(event.seq), role: 'start' } : null,
371
+ start: (_context, match) => {
372
+ const event = match.event;
373
+ if (event.type !== 'room/help-in')
374
+ throw new Error('room-help-in start requires a room/help-in event');
375
+ const data = event.data;
376
+ return {
377
+ seq: event.seq, time: event.time, help: data.id, room: data.room, roomTitle: data.roomTitle, from: data.from,
378
+ op: data.op, text: data.text, member: data.member, ...data.held === true ? { held: true } : {},
379
+ };
380
+ },
381
+ update: context => context.state,
382
+ buildViewNode: context => viewNode(context, 'room-help-request'),
212
383
  };
@@ -72,20 +72,30 @@ export declare class RoomPresetVisibility implements RoomChromeVisibility {
72
72
  * session and disposes otherwise; the slot's own subscription re-renders the
73
73
  * strip (the same re-registration mechanism the composer takeover's
74
74
  * promotion bump uses).
75
+ *
76
+ * Criterion-driven switches apply on the NEXT frame (see {@link nextFrame}),
77
+ * coalesced — repeated syncs collapse into the one scheduled frame, which
78
+ * reads the LATEST decision. The readiness gate's teardown arm is the
79
+ * exception: setReady(false) cancels a pending frame and unmounts a live
80
+ * registration SYNCHRONOUSLY, so nothing can register after it. (The plan
81
+ * tab-type toggle on sidebarRightTabs shares this class — its registry never
82
+ * fed refreshViews, and the same deferral is harmless there.)
75
83
  */
76
84
  export declare class RegistrationToggle {
77
85
  private readonly register;
78
86
  private readonly show;
79
87
  private dispose;
80
88
  private ready;
89
+ /** The coalesced pending re-evaluation's canceler; undefined when no frame is scheduled. */
90
+ private pending;
81
91
  /**
82
92
  * @param register - mounts the entry and returns its disposer.
83
- * @param show - the current decision (re-read on every sync).
93
+ * @param show - the current decision (re-read when the scheduled frame runs).
84
94
  */
85
95
  constructor(register: () => () => void, show: () => boolean);
86
96
  /** The slot declaration appeared (slots.inject's arm) — decisions may register now. */
87
97
  setReady(ready: boolean): void;
88
- /** Re-evaluate: register while shown and ready, dispose otherwise. */
98
+ /** Re-evaluate on the next frame: register while shown and ready, dispose otherwise. */
89
99
  sync(): void;
90
100
  }
91
101
  //# sourceMappingURL=preset-visibility.d.ts.map
@@ -66,6 +66,27 @@ export class RoomPresetVisibility {
66
66
  listener();
67
67
  }
68
68
  }
69
+ /**
70
+ * The register/unregister beat: the NEXT animation frame (a zero timeout
71
+ * where no rAF exists — headless tests), cancellable.
72
+ *
73
+ * Why not synchronous: every change of a `conversation.view` registration
74
+ * re-triggers ui-conversation's `refreshViews`, which re-reads every mounted
75
+ * session binding and throws UNCAUGHT on one whose session record is already
76
+ * released — the window a session switch opens, and this toggle's sync() rides
77
+ * exactly that flush (upstream proposal
78
+ * docs/upstream-proposals/2026-10-20-refreshviews-stale-binding-robustness.md;
79
+ * the production incident killed the message timeline, which shared the
80
+ * aborted flush). One frame later the flush has settled.
81
+ */
82
+ function nextFrame(callback) {
83
+ if (typeof requestAnimationFrame === 'function') {
84
+ const id = requestAnimationFrame(callback);
85
+ return () => { cancelAnimationFrame(id); };
86
+ }
87
+ const id = setTimeout(callback, 0);
88
+ return () => { clearTimeout(id); };
89
+ }
69
90
  /**
70
91
  * The members tab's registration toggle: the `conversation.view` tab strip's
71
92
  * BUTTONS enumerate the slot's registrations (ui-conversation's `viewTabs()`
@@ -75,15 +96,25 @@ export class RoomPresetVisibility {
75
96
  * session and disposes otherwise; the slot's own subscription re-renders the
76
97
  * strip (the same re-registration mechanism the composer takeover's
77
98
  * promotion bump uses).
99
+ *
100
+ * Criterion-driven switches apply on the NEXT frame (see {@link nextFrame}),
101
+ * coalesced — repeated syncs collapse into the one scheduled frame, which
102
+ * reads the LATEST decision. The readiness gate's teardown arm is the
103
+ * exception: setReady(false) cancels a pending frame and unmounts a live
104
+ * registration SYNCHRONOUSLY, so nothing can register after it. (The plan
105
+ * tab-type toggle on sidebarRightTabs shares this class — its registry never
106
+ * fed refreshViews, and the same deferral is harmless there.)
78
107
  */
79
108
  export class RegistrationToggle {
80
109
  register;
81
110
  show;
82
111
  dispose;
83
112
  ready = false;
113
+ /** The coalesced pending re-evaluation's canceler; undefined when no frame is scheduled. */
114
+ pending;
84
115
  /**
85
116
  * @param register - mounts the entry and returns its disposer.
86
- * @param show - the current decision (re-read on every sync).
117
+ * @param show - the current decision (re-read when the scheduled frame runs).
87
118
  */
88
119
  constructor(register, show) {
89
120
  this.register = register;
@@ -92,22 +123,32 @@ export class RegistrationToggle {
92
123
  /** The slot declaration appeared (slots.inject's arm) — decisions may register now. */
93
124
  setReady(ready) {
94
125
  this.ready = ready;
95
- if (!ready && this.dispose !== undefined) {
96
- this.dispose();
97
- this.dispose = undefined;
126
+ if (!ready) {
127
+ this.pending?.();
128
+ this.pending = undefined;
129
+ if (this.dispose !== undefined) {
130
+ this.dispose();
131
+ this.dispose = undefined;
132
+ }
133
+ return;
98
134
  }
99
135
  this.sync();
100
136
  }
101
- /** Re-evaluate: register while shown and ready, dispose otherwise. */
137
+ /** Re-evaluate on the next frame: register while shown and ready, dispose otherwise. */
102
138
  sync() {
103
- if (!this.ready)
139
+ if (!this.ready || this.pending !== undefined)
104
140
  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
- }
141
+ this.pending = nextFrame(() => {
142
+ this.pending = undefined;
143
+ if (!this.ready)
144
+ return;
145
+ if (this.show()) {
146
+ this.dispose ??= this.register();
147
+ }
148
+ else if (this.dispose !== undefined) {
149
+ this.dispose();
150
+ this.dispose = undefined;
151
+ }
152
+ });
112
153
  }
113
154
  }