@flamingo-stack/openframe-frontend-core 0.0.512-1640.4220.1 → 0.0.512

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 (107) hide show
  1. package/dist/{chunk-HCKICFSZ.cjs → chunk-2WWDZR4D.cjs} +31 -31
  2. package/dist/{chunk-HCKICFSZ.cjs.map → chunk-2WWDZR4D.cjs.map} +1 -1
  3. package/dist/{chunk-FAPHWUEM.cjs → chunk-2XL7UK5Y.cjs} +5 -5
  4. package/dist/{chunk-FAPHWUEM.cjs.map → chunk-2XL7UK5Y.cjs.map} +1 -1
  5. package/dist/{chunk-XHGL7XHL.js → chunk-4NFDSMB5.js} +4 -4
  6. package/dist/{chunk-YJRJCJRK.cjs → chunk-5JBDT2MQ.cjs} +7 -7
  7. package/dist/{chunk-YJRJCJRK.cjs.map → chunk-5JBDT2MQ.cjs.map} +1 -1
  8. package/dist/{chunk-VL6RXIPW.cjs → chunk-67YDSTH7.cjs} +9 -9
  9. package/dist/{chunk-VL6RXIPW.cjs.map → chunk-67YDSTH7.cjs.map} +1 -1
  10. package/dist/{chunk-7UYW6C4P.js → chunk-6LMTBNLN.js} +2 -2
  11. package/dist/{chunk-QXFJU5I2.cjs → chunk-7VBEOGCR.cjs} +61 -61
  12. package/dist/{chunk-QXFJU5I2.cjs.map → chunk-7VBEOGCR.cjs.map} +1 -1
  13. package/dist/{chunk-WOM5RZ6T.cjs → chunk-AWLFJMOJ.cjs} +37 -37
  14. package/dist/{chunk-WOM5RZ6T.cjs.map → chunk-AWLFJMOJ.cjs.map} +1 -1
  15. package/dist/{chunk-SIRV5BCG.cjs → chunk-DQDPDUI3.cjs} +4 -4
  16. package/dist/{chunk-SIRV5BCG.cjs.map → chunk-DQDPDUI3.cjs.map} +1 -1
  17. package/dist/{chunk-Y5QFN4WH.cjs → chunk-F2YI5N7E.cjs} +13 -13
  18. package/dist/{chunk-Y5QFN4WH.cjs.map → chunk-F2YI5N7E.cjs.map} +1 -1
  19. package/dist/{chunk-IYIO2OVS.js → chunk-FI35LJ3L.js} +2 -2
  20. package/dist/{chunk-NZR2U5VK.cjs → chunk-GOFTC2HV.cjs} +444 -479
  21. package/dist/chunk-GOFTC2HV.cjs.map +1 -0
  22. package/dist/{chunk-6SVI63CC.js → chunk-HGZPH7S5.js} +2 -2
  23. package/dist/{chunk-7LHDZK7M.js → chunk-IHNUMRMU.js} +2 -2
  24. package/dist/{chunk-YLVVZVK6.js → chunk-IK2WRYVA.js} +4 -4
  25. package/dist/{chunk-XDOZN745.cjs → chunk-JIW5JI3E.cjs} +3 -3
  26. package/dist/{chunk-XDOZN745.cjs.map → chunk-JIW5JI3E.cjs.map} +1 -1
  27. package/dist/{chunk-CLEAYFOX.js → chunk-JKAX5K7C.js} +5 -5
  28. package/dist/{chunk-4BUHSV4D.js → chunk-JO3NPEK6.js} +2 -2
  29. package/dist/{chunk-ARTN2P3B.js → chunk-JOFGJYIT.js} +2 -2
  30. package/dist/{chunk-ZI3GAXFN.js → chunk-OT4RV6WY.js} +6 -6
  31. package/dist/{chunk-QUT2X2LX.cjs → chunk-RJXI25TJ.cjs} +26 -26
  32. package/dist/{chunk-QUT2X2LX.cjs.map → chunk-RJXI25TJ.cjs.map} +1 -1
  33. package/dist/{chunk-EXDW3LB5.cjs → chunk-SG25J6LV.cjs} +87 -87
  34. package/dist/{chunk-EXDW3LB5.cjs.map → chunk-SG25J6LV.cjs.map} +1 -1
  35. package/dist/{chunk-DDWLGE6D.js → chunk-V5WLIFKN.js} +325 -360
  36. package/dist/chunk-V5WLIFKN.js.map +1 -0
  37. package/dist/{chunk-OOYBU46B.cjs → chunk-VVXUUBML.cjs} +26 -26
  38. package/dist/{chunk-OOYBU46B.cjs.map → chunk-VVXUUBML.cjs.map} +1 -1
  39. package/dist/{chunk-Q4XWNC33.js → chunk-VZX3SSFL.js} +2 -2
  40. package/dist/{chunk-WHKVCEAB.cjs → chunk-WVZHCDB5.cjs} +11 -11
  41. package/dist/{chunk-WHKVCEAB.cjs.map → chunk-WVZHCDB5.cjs.map} +1 -1
  42. package/dist/{chunk-UMUNWABO.js → chunk-XG7FLAKQ.js} +2 -2
  43. package/dist/{chunk-EYUANCRA.js → chunk-ZIQMVAGA.js} +2 -2
  44. package/dist/components/case-studies/index.cjs +8 -8
  45. package/dist/components/case-studies/index.js +2 -2
  46. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-chat-card-item.d.ts +3 -0
  48. package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
  49. package/dist/components/chat/index.cjs +2 -2
  50. package/dist/components/chat/index.js +1 -1
  51. package/dist/components/chat/remark-strip-citations.d.ts.map +1 -1
  52. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  53. package/dist/components/chat/stream/message-mutations.d.ts +42 -5
  54. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  55. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  56. package/dist/components/contact/index.cjs +3 -3
  57. package/dist/components/contact/index.js +2 -2
  58. package/dist/components/docs/index.cjs +5 -5
  59. package/dist/components/docs/index.js +4 -4
  60. package/dist/components/embeds/index.cjs +3 -3
  61. package/dist/components/embeds/index.js +2 -2
  62. package/dist/components/faq/index.cjs +3 -3
  63. package/dist/components/faq/index.js +2 -2
  64. package/dist/components/features/index.cjs +2 -2
  65. package/dist/components/features/index.js +1 -1
  66. package/dist/components/help-center-pages/index.cjs +22 -22
  67. package/dist/components/help-center-pages/index.js +13 -13
  68. package/dist/components/index.cjs +132 -132
  69. package/dist/components/index.js +11 -11
  70. package/dist/components/meeting-scheduler/index.cjs +34 -34
  71. package/dist/components/meeting-scheduler/index.js +3 -3
  72. package/dist/components/navigation/index.cjs +2 -2
  73. package/dist/components/navigation/index.js +1 -1
  74. package/dist/components/onboarding-guides/index.cjs +5 -5
  75. package/dist/components/onboarding-guides/index.js +4 -4
  76. package/dist/components/related-content/index.cjs +3 -3
  77. package/dist/components/related-content/index.js +2 -2
  78. package/dist/components/tickets/index.cjs +6 -6
  79. package/dist/components/tickets/index.js +5 -5
  80. package/dist/components/ui/index.cjs +2 -2
  81. package/dist/components/ui/index.js +1 -1
  82. package/dist/index.cjs +2 -2
  83. package/dist/index.js +1 -1
  84. package/package.json +1 -1
  85. package/src/components/chat/entity-cards/dispatch.tsx +92 -99
  86. package/src/components/chat/hooks/use-chat-card-item.ts +9 -0
  87. package/src/components/chat/hooks/use-sse-chat-adapter.ts +15 -15
  88. package/src/components/chat/remark-strip-citations.ts +7 -5
  89. package/src/components/chat/stream/__tests__/chat-stream-reducer-approval-batch.test.ts +18 -0
  90. package/src/components/chat/stream/chat-stream-reducer.ts +12 -3
  91. package/src/components/chat/stream/message-mutations.ts +153 -96
  92. package/src/components/chat/utils/message-segment-accumulator.ts +19 -27
  93. package/dist/chunk-DDWLGE6D.js.map +0 -1
  94. package/dist/chunk-NZR2U5VK.cjs.map +0 -1
  95. /package/dist/{chunk-XHGL7XHL.js.map → chunk-4NFDSMB5.js.map} +0 -0
  96. /package/dist/{chunk-7UYW6C4P.js.map → chunk-6LMTBNLN.js.map} +0 -0
  97. /package/dist/{chunk-IYIO2OVS.js.map → chunk-FI35LJ3L.js.map} +0 -0
  98. /package/dist/{chunk-6SVI63CC.js.map → chunk-HGZPH7S5.js.map} +0 -0
  99. /package/dist/{chunk-7LHDZK7M.js.map → chunk-IHNUMRMU.js.map} +0 -0
  100. /package/dist/{chunk-YLVVZVK6.js.map → chunk-IK2WRYVA.js.map} +0 -0
  101. /package/dist/{chunk-CLEAYFOX.js.map → chunk-JKAX5K7C.js.map} +0 -0
  102. /package/dist/{chunk-4BUHSV4D.js.map → chunk-JO3NPEK6.js.map} +0 -0
  103. /package/dist/{chunk-ARTN2P3B.js.map → chunk-JOFGJYIT.js.map} +0 -0
  104. /package/dist/{chunk-ZI3GAXFN.js.map → chunk-OT4RV6WY.js.map} +0 -0
  105. /package/dist/{chunk-Q4XWNC33.js.map → chunk-VZX3SSFL.js.map} +0 -0
  106. /package/dist/{chunk-UMUNWABO.js.map → chunk-XG7FLAKQ.js.map} +0 -0
  107. /package/dist/{chunk-EYUANCRA.js.map → chunk-ZIQMVAGA.js.map} +0 -0
@@ -134,40 +134,30 @@ export function appendToTrailingAssistant(
134
134
  return [...prev.slice(0, -1), { ...last, segments: merged }]
135
135
  }
136
136
 
137
+ // =============================================================================
138
+ // Segment-projection kernel — shared scaffolding + single-rule predicates
139
+ // =============================================================================
140
+
137
141
  /**
138
- * Mark ONE batch row's confirm as FAILED (expired proposal, network
139
- * error): tick its execution icon to the failure cross so the row's
140
- * loader doesn't spin forever. Batch status is untouched other rows
141
- * may still be resolving.
142
+ * Map every assistant message's segments through `fn`, preserving the
143
+ * REFERENTIAL-STABILITY CONTRACT in ONE place: `fn` returns the SAME
144
+ * segment reference to signal "unchanged"; untouched messages (and the
145
+ * whole array on a global no-op) keep their prior references. Every
146
+ * segment projection below rides this scaffold instead of re-implementing
147
+ * the changed-flag dance.
142
148
  */
143
- export function projectBatchRowFailureToMessages(
149
+ function mapSegments(
144
150
  prev: UnifiedChatMessage[],
145
- rowRequestId: string,
151
+ fn: (s: MessageSegment) => MessageSegment,
146
152
  ): UnifiedChatMessage[] {
147
153
  let changed = false
148
154
  const next = prev.map((m) => {
149
155
  if (m.role !== 'assistant' || !m.segments) return m
150
156
  let msgChanged = false
151
157
  const segs = m.segments.map((s) => {
152
- if (
153
- s.type !== 'approval_batch' ||
154
- !s.data.toolCalls?.some((c) => c.toolExecutionRequestId === rowRequestId)
155
- ) {
156
- return s
157
- }
158
- const existing = s.data.executions?.[rowRequestId]
159
- if (existing?.status === 'done') return s
160
- msgChanged = true
161
- return {
162
- ...s,
163
- data: {
164
- ...s.data,
165
- executions: {
166
- ...(s.data.executions ?? {}),
167
- [rowRequestId]: { status: 'done' as const, success: false },
168
- },
169
- },
170
- }
158
+ const out = fn(s)
159
+ if (out !== s) msgChanged = true
160
+ return out
171
161
  })
172
162
  if (!msgChanged) return m
173
163
  changed = true
@@ -176,6 +166,130 @@ export function projectBatchRowFailureToMessages(
176
166
  return changed ? next : prev
177
167
  }
178
168
 
169
+ /**
170
+ * THE approval-resolution rule for a single segment — one predicate for
171
+ * both containers (the message-array projections here AND the flat
172
+ * segment list in `MessageSegmentAccumulator.updateApprovalStatus`), so
173
+ * the two paths can never drift:
174
+ * - `approval_request` matched on `data.requestId` → status flip;
175
+ * - `approval_batch` anchor (`data.approvalRequestId`, the server's
176
+ * `batch:<first proposalId>` — never a row id) → status flip;
177
+ * - `approval_batch` ROW (`toolCalls[].toolExecutionRequestId`) →
178
+ * tick that row's execution (check on approved, cross otherwise)
179
+ * without touching the batch status.
180
+ * Returns the SAME reference when nothing matched or changed.
181
+ */
182
+ export function applyApprovalStatusToSegment(
183
+ s: MessageSegment,
184
+ requestId: string,
185
+ status: ChatApprovalStatus,
186
+ resolvedByName?: string | null,
187
+ ): MessageSegment {
188
+ if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {
189
+ return { ...s, status }
190
+ }
191
+ if (s.type !== 'approval_batch') return s
192
+ const isAnchor = s.data.approvalRequestId === requestId
193
+ const hasRow = s.data.toolCalls?.some((c) => c.toolExecutionRequestId === requestId)
194
+ if (!isAnchor && !hasRow) return s
195
+ const nextResolvedBy = resolvedByName ?? s.resolvedByName
196
+ const nextExecutions = hasRow
197
+ ? {
198
+ ...(s.data.executions ?? {}),
199
+ [requestId]: { status: 'done' as const, success: status === 'approved' },
200
+ }
201
+ : s.data.executions
202
+ const nextStatus = isAnchor ? status : s.status
203
+ if (
204
+ s.status === nextStatus &&
205
+ nextResolvedBy === s.resolvedByName &&
206
+ nextExecutions === s.data.executions
207
+ ) {
208
+ return s
209
+ }
210
+ return {
211
+ ...s,
212
+ status: nextStatus,
213
+ resolvedByName: nextResolvedBy,
214
+ data: {
215
+ ...s.data,
216
+ ...(nextExecutions ? { executions: nextExecutions } : {}),
217
+ },
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Next state for a batch row's execution slot from a tool-execution
223
+ * chunk — folds the never-downgrade guard (redelivered EXECUTING after
224
+ * EXECUTED landed) and the EXECUTED/EXECUTING ternary into ONE rule
225
+ * shared by the message projection and the accumulator. Returns null
226
+ * for the no-op case (caller keeps prior references).
227
+ */
228
+ export function nextBatchExecution(
229
+ prevExec: ApprovalBatchExecutionState | undefined,
230
+ toolData: ToolExecutionData,
231
+ ): ApprovalBatchExecutionState | null {
232
+ if (toolData.type === 'EXECUTING_TOOL' && prevExec?.status === 'done') return null
233
+ return toolData.type === 'EXECUTED_TOOL'
234
+ ? { status: 'done', result: toolData.result, success: toolData.success }
235
+ : { status: 'executing', result: prevExec?.result, success: prevExec?.success }
236
+ }
237
+
238
+ /** The executions-map triple-spread — one home for the write shape. */
239
+ export function withBatchExecution<S extends { data: { executions?: Record<string, ApprovalBatchExecutionState> } }>(
240
+ seg: S,
241
+ execId: string,
242
+ state: ApprovalBatchExecutionState,
243
+ ): S {
244
+ return {
245
+ ...seg,
246
+ data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: state } },
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Flip ONLY an approval_batch segment's status — no execution writes.
252
+ * The click-time optimistic flip uses this instead of
253
+ * `projectApprovalResolutionToMessages` as defense-in-depth: even if a
254
+ * (misbehaving) server emitted a batchId colliding with a row's
255
+ * proposal id, the flip could not pre-tick that row's execution to a
256
+ * false success before its confirm actually ran.
257
+ */
258
+ export function projectBatchStatusToMessages(
259
+ prev: UnifiedChatMessage[],
260
+ anchorId: string,
261
+ status: ChatApprovalStatus,
262
+ ): UnifiedChatMessage[] {
263
+ return mapSegments(prev, (s) => {
264
+ if (s.type !== 'approval_batch') return s
265
+ if (s.data.approvalRequestId !== anchorId || s.status === status) return s
266
+ return { ...s, status }
267
+ })
268
+ }
269
+
270
+ /**
271
+ * Mark ONE batch row's confirm as FAILED (expired proposal, network
272
+ * error): tick its execution icon to the failure cross so the row's
273
+ * loader doesn't spin forever. Batch status is untouched — other rows
274
+ * may still be resolving.
275
+ */
276
+ export function projectBatchRowFailureToMessages(
277
+ prev: UnifiedChatMessage[],
278
+ rowRequestId: string,
279
+ ): UnifiedChatMessage[] {
280
+ return mapSegments(prev, (s) => {
281
+ if (
282
+ s.type !== 'approval_batch' ||
283
+ !s.data.toolCalls?.some((c) => c.toolExecutionRequestId === rowRequestId)
284
+ ) {
285
+ return s
286
+ }
287
+ const existing = s.data.executions?.[rowRequestId]
288
+ if (existing?.status === 'done') return s
289
+ return withBatchExecution(s, rowRequestId, { status: 'done', success: false })
290
+ })
291
+ }
292
+
179
293
  /**
180
294
  * Upsert a standalone context-compaction segment into the trailing assistant
181
295
  * bubble. Compaction emissions arrive as the accumulator's CUMULATIVE array —
@@ -284,23 +398,16 @@ export function mergeToolExecutionIfPresent(
284
398
  seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)
285
399
  ) {
286
400
  const prevExec: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
287
- // Never downgrade a done batch slot back to executing (JetStream
288
- // redelivery of the EXECUTING chunk after EXECUTED landed). Returning
289
- // prev = matched-with-no-change, so the caller doesn't append either.
290
- if (toolData.type === 'EXECUTING_TOOL' && prevExec?.status === 'done') {
291
- return prev
292
- }
293
- const nextExec: ApprovalBatchExecutionState =
294
- toolData.type === 'EXECUTED_TOOL'
295
- ? { status: 'done', result: toolData.result, success: toolData.success }
296
- : { status: 'executing', result: prevExec?.result, success: prevExec?.success }
401
+ // Shared rule (`nextBatchExecution`): folds the never-downgrade
402
+ // guard (JetStream redelivery of EXECUTING after EXECUTED
403
+ // landed — null = matched-with-no-change, caller doesn't append
404
+ // either) and the EXECUTED/EXECUTING ternary.
405
+ const nextExec = nextBatchExecution(prevExec, toolData)
406
+ if (nextExec === null) return prev
297
407
  // Value-level no-op (replayed duplicate) → prior references.
298
408
  if (sameExecutionState(prevExec, nextExec)) return prev
299
409
  const nextSegments = [...message.segments]
300
- nextSegments[j] = {
301
- ...seg,
302
- data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: nextExec } },
303
- }
410
+ nextSegments[j] = withBatchExecution(seg, execId, nextExec)
304
411
  const next = [...prev]
305
412
  next[i] = { ...message, segments: nextSegments }
306
413
  return next
@@ -346,10 +453,10 @@ export function mergeToolExecutionIfPresent(
346
453
  }
347
454
 
348
455
  /**
349
- * Cross-message approval status flip (absorbed from the NATS adapter's
350
- * `onApprovalResolved` handler). Idempotent + reference-preserving: only the
351
- * messages/segments whose status (or resolvedByName) actually changes are
352
- * cloned; a no-op returns `prev`.
456
+ * Project an approval resolution onto the thread the ONE rule lives in
457
+ * `applyApprovalStatusToSegment` (shared with the accumulator's flat
458
+ * segment list); this is just its message-array projection via
459
+ * `mapSegments`.
353
460
  */
354
461
  export function projectApprovalResolutionToMessages(
355
462
  prev: UnifiedChatMessage[],
@@ -357,57 +464,7 @@ export function projectApprovalResolutionToMessages(
357
464
  status: ChatApprovalStatus,
358
465
  resolvedByName?: string | null,
359
466
  ): UnifiedChatMessage[] {
360
- let changed = false
361
- const next = prev.map((m) => {
362
- if (m.role !== 'assistant' || !m.segments) return m
363
- let msgChanged = false
364
- const segs = m.segments.map((s) => {
365
- if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {
366
- msgChanged = true
367
- return { ...s, status }
368
- }
369
- if (s.type === 'approval_batch') {
370
- const isAnchor = s.data.approvalRequestId === requestId
371
- // Wire-native chat batches: each ROW is its own proposal
372
- // (`toolExecutionRequestId` = proposalId) resolving through its
373
- // own per-proposal confirm — a resolution for a row ticks that
374
- // row's execution icon (check on approved, cross on rejected/
375
- // failed) without touching the batch status (flipped
376
- // optimistically at click time). The anchor row ALSO flips the
377
- // batch status (anchor id = first proposal id).
378
- const hasRow = s.data.toolCalls?.some((c) => c.toolExecutionRequestId === requestId)
379
- if (!isAnchor && !hasRow) return s
380
- const nextResolvedBy = resolvedByName ?? s.resolvedByName
381
- const nextExecutions = hasRow
382
- ? {
383
- ...(s.data.executions ?? {}),
384
- [requestId]: { status: 'done' as const, success: status === 'approved' },
385
- }
386
- : s.data.executions
387
- const nextStatus = isAnchor ? status : s.status
388
- if (
389
- s.status === nextStatus &&
390
- nextResolvedBy === s.resolvedByName &&
391
- nextExecutions === s.data.executions
392
- ) {
393
- return s
394
- }
395
- msgChanged = true
396
- return {
397
- ...s,
398
- status: nextStatus,
399
- resolvedByName: nextResolvedBy,
400
- data: {
401
- ...s.data,
402
- ...(nextExecutions ? { executions: nextExecutions } : {}),
403
- },
404
- }
405
- }
406
- return s
407
- })
408
- if (!msgChanged) return m
409
- changed = true
410
- return { ...m, segments: segs }
411
- })
412
- return changed ? next : prev
467
+ return mapSegments(prev, (s) =>
468
+ applyApprovalStatusToSegment(s, requestId, status, resolvedByName),
469
+ )
413
470
  }
@@ -23,6 +23,11 @@ import type {
23
23
  ChatApprovalStatus,
24
24
  ExecutingToolState,
25
25
  } from '../types'
26
+ import {
27
+ applyApprovalStatusToSegment,
28
+ nextBatchExecution,
29
+ withBatchExecution,
30
+ } from '../stream/message-mutations'
26
31
 
27
32
  export interface AccumulatorCallbacks {
28
33
  /** See `ApprovalResolutionHandler` (message.types) — the SSOT for the
@@ -247,25 +252,13 @@ export class MessageSegmentAccumulator {
247
252
  if (!hasCall) return seg
248
253
 
249
254
  const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
250
- // Never downgrade a done slot back to executing (redelivered EXECUTING
251
- // after its EXECUTED already landed) matched, but unchanged.
252
- if (toolData.type === 'EXECUTING_TOOL' && prev?.status === 'done') {
253
- matched = true
254
- return seg
255
- }
256
- const next: ApprovalBatchExecutionState =
257
- toolData.type === 'EXECUTED_TOOL'
258
- ? { status: 'done', result: toolData.result, success: toolData.success }
259
- : { status: 'executing', result: prev?.result, success: prev?.success }
260
-
255
+ // Shared rule (`nextBatchExecution` from message-mutations): the
256
+ // never-downgrade guard + EXECUTED/EXECUTING ternaryone
257
+ // implementation with the message-array projection.
258
+ const next = nextBatchExecution(prev, toolData)
261
259
  matched = true
262
- return {
263
- ...seg,
264
- data: {
265
- ...seg.data,
266
- executions: { ...(seg.data.executions ?? {}), [execId]: next },
267
- },
268
- }
260
+ if (next === null) return seg
261
+ return withBatchExecution(seg, execId, next)
269
262
  })
270
263
  return matched
271
264
  }
@@ -432,15 +425,14 @@ export class MessageSegmentAccumulator {
432
425
  * resolved card shows "by {name}"; omit it to leave any existing value untouched.
433
426
  */
434
427
  updateApprovalStatus(requestId: string, status: ChatApprovalStatus, resolvedByName?: string | null): MessageSegment[] {
435
- this.segments = this.segments.map(segment => {
436
- if (segment.type === 'approval_request' && segment.data.requestId === requestId) {
437
- return { ...segment, status }
438
- }
439
- if (segment.type === 'approval_batch' && segment.data.approvalRequestId === requestId) {
440
- return { ...segment, status, resolvedByName: resolvedByName ?? segment.resolvedByName }
441
- }
442
- return segment
443
- })
428
+ // ONE rule, two containers: `applyApprovalStatusToSegment` is the
429
+ // same predicate the message-array projection uses
430
+ // (`projectApprovalResolutionToMessages`) anchor status flip AND
431
+ // batch-row execution tick, so this flat-list path can never drift
432
+ // from the projection again (it previously missed the row case).
433
+ this.segments = this.segments.map((segment) =>
434
+ applyApprovalStatusToSegment(segment, requestId, status, resolvedByName),
435
+ )
444
436
  return this.getSegments()
445
437
  }
446
438