@flamingo-stack/openframe-frontend-core 0.0.172 → 0.0.173

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 (87) hide show
  1. package/dist/{chunk-AY3E5FKM.cjs → chunk-2MEBOTV4.cjs} +2263 -1698
  2. package/dist/chunk-2MEBOTV4.cjs.map +1 -0
  3. package/dist/{chunk-FEEPEOW2.js → chunk-J3ZCNPDM.js} +6128 -5563
  4. package/dist/chunk-J3ZCNPDM.js.map +1 -0
  5. package/dist/components/chat/approval-batch-message.d.ts +10 -0
  6. package/dist/components/chat/approval-batch-message.d.ts.map +1 -0
  7. package/dist/components/chat/chat-input.d.ts.map +1 -1
  8. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  9. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  10. package/dist/components/chat/chat-message-loader.d.ts +23 -0
  11. package/dist/components/chat/chat-message-loader.d.ts.map +1 -0
  12. package/dist/components/chat/hooks/index.d.ts +1 -0
  13. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  14. package/dist/components/chat/hooks/use-collapsible.d.ts.map +1 -1
  15. package/dist/components/chat/hooks/use-delayed-flag.d.ts +25 -0
  16. package/dist/components/chat/hooks/use-delayed-flag.d.ts.map +1 -0
  17. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  18. package/dist/components/chat/index.d.ts +3 -0
  19. package/dist/components/chat/index.d.ts.map +1 -1
  20. package/dist/components/chat/thinking-display.d.ts.map +1 -1
  21. package/dist/components/chat/tool-call-blocks.d.ts +18 -0
  22. package/dist/components/chat/tool-call-blocks.d.ts.map +1 -0
  23. package/dist/components/chat/tool-execution-display.d.ts +1 -1
  24. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  25. package/dist/components/chat/types/api.types.d.ts +16 -1
  26. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  27. package/dist/components/chat/types/component.types.d.ts +2 -2
  28. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  29. package/dist/components/chat/types/message.types.d.ts +54 -1
  30. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  31. package/dist/components/chat/types/network.types.d.ts +2 -0
  32. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  33. package/dist/components/chat/types/processing.types.d.ts +14 -1
  34. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  35. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
  36. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  37. package/dist/components/chat/utils/index.d.ts +1 -0
  38. package/dist/components/chat/utils/index.d.ts.map +1 -1
  39. package/dist/components/chat/utils/message-segment-accumulator.d.ts +32 -4
  40. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  41. package/dist/components/chat/utils/process-historical-messages.d.ts +2 -1
  42. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  43. package/dist/components/chat/utils/tool-call-helpers.d.ts +38 -0
  44. package/dist/components/chat/utils/tool-call-helpers.d.ts.map +1 -0
  45. package/dist/components/features/index.cjs +2 -2
  46. package/dist/components/features/index.js +1 -1
  47. package/dist/components/index.cjs +14 -2
  48. package/dist/components/index.cjs.map +1 -1
  49. package/dist/components/index.js +13 -1
  50. package/dist/components/navigation/index.cjs +2 -2
  51. package/dist/components/navigation/index.js +1 -1
  52. package/dist/components/ui/index.cjs +14 -2
  53. package/dist/components/ui/index.cjs.map +1 -1
  54. package/dist/components/ui/index.js +13 -1
  55. package/dist/components/ui/textarea.d.ts +11 -0
  56. package/dist/components/ui/textarea.d.ts.map +1 -1
  57. package/dist/index.cjs +14 -2
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.js +13 -1
  60. package/package.json +3 -3
  61. package/src/components/chat/approval-batch-message.tsx +237 -0
  62. package/src/components/chat/chat-input.tsx +34 -106
  63. package/src/components/chat/chat-message-enhanced.tsx +11 -0
  64. package/src/components/chat/chat-message-list.tsx +22 -6
  65. package/src/components/chat/chat-message-loader.tsx +67 -0
  66. package/src/components/chat/hooks/index.ts +1 -0
  67. package/src/components/chat/hooks/use-collapsible.ts +10 -1
  68. package/src/components/chat/hooks/use-delayed-flag.ts +56 -0
  69. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +109 -20
  70. package/src/components/chat/index.ts +3 -0
  71. package/src/components/chat/thinking-display.tsx +76 -12
  72. package/src/components/chat/tool-call-blocks.tsx +58 -0
  73. package/src/components/chat/tool-execution-display.tsx +60 -81
  74. package/src/components/chat/types/api.types.ts +22 -3
  75. package/src/components/chat/types/component.types.ts +2 -2
  76. package/src/components/chat/types/message.types.ts +58 -1
  77. package/src/components/chat/types/network.types.ts +2 -0
  78. package/src/components/chat/types/processing.types.ts +13 -2
  79. package/src/components/chat/utils/chunk-parser.ts +40 -5
  80. package/src/components/chat/utils/extract-incomplete-message-state.ts +19 -1
  81. package/src/components/chat/utils/index.ts +3 -0
  82. package/src/components/chat/utils/message-segment-accumulator.ts +136 -12
  83. package/src/components/chat/utils/process-historical-messages.ts +88 -13
  84. package/src/components/chat/utils/tool-call-helpers.ts +105 -0
  85. package/src/components/ui/textarea.tsx +107 -25
  86. package/dist/chunk-AY3E5FKM.cjs.map +0 -1
  87. package/dist/chunk-FEEPEOW2.js.map +0 -1
@@ -2,7 +2,7 @@
2
2
  * Component prop types
3
3
  */
4
4
 
5
- import type { ButtonHTMLAttributes, ComponentType, HTMLAttributes, TextareaHTMLAttributes } from 'react'
5
+ import type { ComponentType, HTMLAttributes, TextareaHTMLAttributes } from 'react'
6
6
  import type { AssistantType, AuthorType, ChatApprovalStatus, ConnectionStatus } from './chat.types'
7
7
  import type { ApprovalRequestData, Message, MessageSegment, ToolExecutionData } from './message.types'
8
8
  import type { ChatRef } from '../chat-ref.types'
@@ -242,7 +242,7 @@ export interface ChatTypingIndicatorProps extends HTMLAttributes<HTMLDivElement>
242
242
 
243
243
  // ========== Tool Execution Display Props ==========
244
244
 
245
- export interface ToolExecutionDisplayProps extends ButtonHTMLAttributes<HTMLButtonElement> {
245
+ export interface ToolExecutionDisplayProps extends HTMLAttributes<HTMLDivElement> {
246
246
  message: ToolExecutionData
247
247
  }
248
248
 
@@ -38,6 +38,12 @@ export interface ToolExecutionData {
38
38
  parameters?: Record<string, any>
39
39
  result?: string
40
40
  success?: boolean
41
+ /**
42
+ * Backend-issued id (matches `PendingToolCallData.toolExecutionRequestId`).
43
+ * When present, lets the accumulator merge this execution event into the
44
+ * matching approval batch row instead of emitting a standalone segment.
45
+ */
46
+ toolExecutionRequestId?: string
41
47
  }
42
48
 
43
49
  // ========== Approval Request Types ==========
@@ -57,6 +63,45 @@ export interface ApprovalResultData {
57
63
  approvalType?: string
58
64
  }
59
65
 
66
+ /**
67
+ * Single tool call inside a batch approval request.
68
+ * Mirrors backend PendingToolCallDto.
69
+ */
70
+ export interface PendingToolCallData {
71
+ toolExecutionRequestId: string
72
+ toolName: string
73
+ toolTitle?: string
74
+ toolExplanation?: string
75
+ toolType?: string
76
+ requiresApproval: boolean
77
+ approvalType?: string | null
78
+ toolCallArguments?: Record<string, any> | null
79
+ }
80
+
81
+ /**
82
+ * Per-tool execution state inside an approval batch.
83
+ * Populated by EXECUTING_TOOL / EXECUTED_TOOL chunks that carry a
84
+ * `toolExecutionRequestId` matching one of the batch's tool calls.
85
+ */
86
+ export interface ApprovalBatchExecutionState {
87
+ status: 'executing' | 'done'
88
+ result?: string
89
+ success?: boolean
90
+ }
91
+
92
+ export interface ApprovalBatchData {
93
+ approvalRequestId: string
94
+ /** Highest approval type required across the batch (e.g. ADMIN beats CLIENT). */
95
+ approvalType: string
96
+ toolCalls: PendingToolCallData[]
97
+ /**
98
+ * Keyed by `PendingToolCallData.toolExecutionRequestId`. Absent before
99
+ * approval; rows without an entry render as "queued" (loader) once the
100
+ * batch itself is approved.
101
+ */
102
+ executions?: Record<string, ApprovalBatchExecutionState>
103
+ }
104
+
60
105
  // ========== Message Segment Types ==========
61
106
 
62
107
  export type TextSegment = {
@@ -82,6 +127,14 @@ export type ApprovalRequestSegment = {
82
127
  onReject?: (requestId?: string) => void | Promise<void>
83
128
  }
84
129
 
130
+ export type ApprovalBatchSegment = {
131
+ type: 'approval_batch'
132
+ data: ApprovalBatchData
133
+ status?: ChatApprovalStatus
134
+ onApprove?: (requestId?: string) => void | Promise<void>
135
+ onReject?: (requestId?: string) => void | Promise<void>
136
+ }
137
+
85
138
  export type ErrorSegment = {
86
139
  type: 'error'
87
140
  title: string
@@ -94,7 +147,7 @@ export type ContextCompactionSegment = {
94
147
  summary?: string
95
148
  }
96
149
 
97
- export type MessageSegment = TextSegment | ThinkingSegment | ToolExecutionSegment | ApprovalRequestSegment | ErrorSegment | ContextCompactionSegment
150
+ export type MessageSegment = TextSegment | ThinkingSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
98
151
 
99
152
  export type MessageContent = string | MessageSegment[]
100
153
 
@@ -119,6 +172,7 @@ export interface ExecutingToolMessageData extends MessageDataBase {
119
172
  integratedToolType?: string
120
173
  toolFunction?: string
121
174
  parameters?: Record<string, any>
175
+ toolExecutionRequestId?: string
122
176
  }
123
177
 
124
178
  export interface ExecutedToolMessageData extends MessageDataBase {
@@ -128,6 +182,7 @@ export interface ExecutedToolMessageData extends MessageDataBase {
128
182
  parameters?: Record<string, any>
129
183
  result?: string
130
184
  success?: boolean
185
+ toolExecutionRequestId?: string
131
186
  }
132
187
 
133
188
  export interface ApprovalRequestMessageData extends MessageDataBase {
@@ -136,6 +191,8 @@ export interface ApprovalRequestMessageData extends MessageDataBase {
136
191
  approvalType?: string
137
192
  command?: string
138
193
  explanation?: string
194
+ /** Present when the approval is a batch of tool calls (new format). */
195
+ toolCalls?: PendingToolCallData[]
139
196
  }
140
197
 
141
198
  export interface ApprovalResultMessageData extends MessageDataBase {
@@ -41,6 +41,7 @@ export interface ChunkData {
41
41
  parameters?: Record<string, any>
42
42
  result?: string
43
43
  success?: boolean
44
+ toolExecutionRequestId?: string
44
45
  error?: string
45
46
  details?: string
46
47
  approvalRequestId?: string
@@ -49,6 +50,7 @@ export interface ChunkData {
49
50
  command?: string
50
51
  explanation?: string
51
52
  approved?: boolean
53
+ toolCalls?: any[]
52
54
  modelName?: string
53
55
  providerName?: string
54
56
  provider?: string
@@ -3,7 +3,7 @@
3
3
  * Contains types for message parsing, accumulation, and processing
4
4
  */
5
5
 
6
- import type { MessageSegment, ProcessedMessage, ToolExecutionSegment, TokenUsageData } from './message.types'
6
+ import type { MessageSegment, ProcessedMessage, ToolExecutionSegment, TokenUsageData, PendingToolCallData } from './message.types'
7
7
  import type { ChatApprovalStatus, AssistantType } from './chat.types'
8
8
  import type { ChunkData, NatsMessageType } from './network.types'
9
9
 
@@ -18,6 +18,7 @@ export type ParsedChunkAction =
18
18
  | { action: 'thinking'; text: string }
19
19
  | { action: 'tool_execution'; segment: ToolExecutionSegment }
20
20
  | { action: 'approval_request'; requestId: string; command: string; explanation?: string; approvalType: string }
21
+ | { action: 'approval_batch'; requestId: string; approvalType: string; toolCalls: PendingToolCallData[] }
21
22
  | { action: 'approval_result'; requestId: string; approved: boolean; approvalType: string }
22
23
  | { action: 'message_request'; text: string; ownerType?: string; displayName?: string }
23
24
  | { action: 'token_usage'; data: TokenUsageData }
@@ -43,7 +44,10 @@ export interface AccumulatorState {
43
44
  toolFunction: string
44
45
  parameters?: Record<string, any>
45
46
  }>
46
- escalatedApprovals?: Map<string, { command: string; explanation?: string; approvalType: string }>
47
+ escalatedApprovals?: Map<
48
+ string,
49
+ { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
50
+ >
47
51
  }
48
52
 
49
53
  // ========== Message Processing Options ==========
@@ -65,6 +69,13 @@ export interface MessageProcessingOptions {
65
69
  approvalStatuses?: Record<string, ChatApprovalStatus>
66
70
  /** Approval types to display directly (others get escalated) - defaults to all types */
67
71
  displayApprovalTypes?: string[]
72
+ /**
73
+ * Consumer-owned. Forwarded by the host app (e.g. oss-tenant chat client
74
+ * reads its `'batch-approvals'` feature flag and passes it down). The lib
75
+ * defaults to legacy single-card rendering when this is omitted — it will
76
+ * not enable the batch UI on its own.
77
+ */
78
+ batchApprovalsEnabled?: boolean
68
79
  }
69
80
 
70
81
  // ========== Chunk Processing Types ==========
@@ -2,7 +2,26 @@
2
2
  * Utilities for parsing NATS chunks into structured actions
3
3
  */
4
4
 
5
- import { MESSAGE_TYPE, type ChunkData, type ParsedChunkAction, type ToolExecutionSegment } from '../types'
5
+ import { MESSAGE_TYPE, type ChunkData, type ParsedChunkAction, type ToolExecutionSegment, type PendingToolCallData } from '../types'
6
+
7
+ function normalizeToolCalls(raw: unknown): PendingToolCallData[] {
8
+ if (!Array.isArray(raw)) return []
9
+ return raw
10
+ .filter((item): item is Record<string, any> => !!item && typeof item === 'object')
11
+ .map((item) => ({
12
+ toolExecutionRequestId: String(item.toolExecutionRequestId ?? ''),
13
+ toolName: String(item.toolName ?? ''),
14
+ toolTitle: typeof item.toolTitle === 'string' ? item.toolTitle : undefined,
15
+ toolExplanation: typeof item.toolExplanation === 'string' ? item.toolExplanation : undefined,
16
+ toolType: typeof item.toolType === 'string' ? item.toolType : undefined,
17
+ requiresApproval: item.requiresApproval === true,
18
+ approvalType: typeof item.approvalType === 'string' ? item.approvalType : null,
19
+ toolCallArguments:
20
+ item.toolCallArguments && typeof item.toolCallArguments === 'object'
21
+ ? (item.toolCallArguments as Record<string, any>)
22
+ : null,
23
+ }))
24
+ }
6
25
 
7
26
  /**
8
27
  * Parse a raw NATS chunk into a structured action
@@ -56,10 +75,11 @@ export function parseChunkToAction(chunk: unknown): ParsedChunkAction | null {
56
75
  integratedToolType: data.integratedToolType || '',
57
76
  toolFunction: data.toolFunction || '',
58
77
  parameters: data.parameters,
78
+ toolExecutionRequestId: typeof data.toolExecutionRequestId === 'string' ? data.toolExecutionRequestId : undefined,
59
79
  }
60
80
  }
61
81
  }
62
-
82
+
63
83
  case MESSAGE_TYPE.EXECUTED_TOOL:
64
84
  return {
65
85
  action: 'tool_execution',
@@ -72,18 +92,33 @@ export function parseChunkToAction(chunk: unknown): ParsedChunkAction | null {
72
92
  parameters: data.parameters,
73
93
  result: data.result,
74
94
  success: data.success,
95
+ toolExecutionRequestId: typeof data.toolExecutionRequestId === 'string' ? data.toolExecutionRequestId : undefined,
75
96
  }
76
97
  }
77
98
  }
78
99
 
79
- case MESSAGE_TYPE.APPROVAL_REQUEST:
100
+ case MESSAGE_TYPE.APPROVAL_REQUEST: {
101
+ const requestId = data.approvalRequestId || data.approval_request_id || ''
102
+ const approvalType = data.approvalType || 'USER'
103
+ const toolCalls = normalizeToolCalls(data.toolCalls)
104
+
105
+ if (toolCalls.length > 0) {
106
+ return {
107
+ action: 'approval_batch',
108
+ requestId,
109
+ approvalType,
110
+ toolCalls,
111
+ }
112
+ }
113
+
80
114
  return {
81
115
  action: 'approval_request',
82
- requestId: data.approvalRequestId || data.approval_request_id || '',
116
+ requestId,
83
117
  command: data.command || '',
84
118
  explanation: data.explanation,
85
- approvalType: data.approvalType || 'USER',
119
+ approvalType,
86
120
  }
121
+ }
87
122
 
88
123
  case MESSAGE_TYPE.APPROVAL_RESULT:
89
124
  return {
@@ -33,7 +33,9 @@ export function extractIncompleteMessageState(
33
33
  switch (segment.type) {
34
34
  case 'tool_execution':
35
35
  if (segment.data.type === 'EXECUTING_TOOL') {
36
- const toolKey = `${segment.data.integratedToolType}-${segment.data.toolFunction}`
36
+ const toolKey =
37
+ segment.data.toolExecutionRequestId ||
38
+ `${segment.data.integratedToolType}-${segment.data.toolFunction}`
37
39
  executingTools.set(toolKey, {
38
40
  integratedToolType: segment.data.integratedToolType,
39
41
  toolFunction: segment.data.toolFunction,
@@ -54,6 +56,22 @@ export function extractIncompleteMessageState(
54
56
  }
55
57
  break
56
58
 
59
+ case 'approval_batch': {
60
+ // Treat a batch as in-progress until every tool call has a
61
+ // `done` execution OR the batch was rejected. Otherwise the realtime
62
+ // accumulator won't hold the segment and post-approval EXECUTED_TOOL
63
+ // chunks won't be able to merge into it via `applyExecutionToBatch`.
64
+ const allDone =
65
+ !!segment.data.executions &&
66
+ segment.data.toolCalls.every(
67
+ (c) => segment.data.executions?.[c.toolExecutionRequestId]?.status === 'done',
68
+ )
69
+ if (segment.status !== 'rejected' && !allDone) {
70
+ hasIncompleteState = true
71
+ }
72
+ break
73
+ }
74
+
57
75
  case 'context_compaction':
58
76
  if (segment.status === 'started') {
59
77
  hasIncompleteState = true
@@ -30,3 +30,6 @@ export {
30
30
  export {
31
31
  extractIncompleteMessageState,
32
32
  } from './extract-incomplete-message-state'
33
+
34
+ // Tool call helpers
35
+ export { getCommandText } from './tool-call-helpers'
@@ -12,9 +12,12 @@ import type {
12
12
  MessageSegment,
13
13
  ToolExecutionSegment,
14
14
  ApprovalRequestSegment,
15
+ ApprovalBatchSegment,
16
+ ApprovalBatchExecutionState,
15
17
  ContextCompactionSegment,
16
18
  ErrorSegment,
17
19
  PendingApproval,
20
+ PendingToolCallData,
18
21
  AccumulatorState,
19
22
  ChatApprovalStatus,
20
23
  } from '../types'
@@ -136,13 +139,27 @@ export class MessageSegmentAccumulator {
136
139
  }
137
140
 
138
141
  /**
139
- * Add a tool execution segment
140
- * If adding EXECUTED_TOOL, replace the matching EXECUTING_TOOL
142
+ * Add a tool execution segment.
143
+ *
144
+ * Routing:
145
+ * 1) If `toolExecutionRequestId` matches a tool call inside an existing
146
+ * `approval_batch` segment, merge the state into that batch's
147
+ * `executions` map (no standalone segment is pushed).
148
+ * 2) Otherwise: pair EXECUTING ↔ EXECUTED by `toolExecutionRequestId`.
149
+ * If no id is present (older backends), fall back to
150
+ * `(integratedToolType, toolFunction)` so repeat calls of the same
151
+ * function don't all bucket under one key.
141
152
  */
142
153
  addToolExecution(segment: ToolExecutionSegment): MessageSegment[] {
143
154
  const toolData = segment.data
144
- const toolKey = `${toolData.integratedToolType}-${toolData.toolFunction}`
145
-
155
+ const execId = toolData.toolExecutionRequestId
156
+
157
+ if (execId && this.applyExecutionToBatch(execId, toolData)) {
158
+ return this.getSegments()
159
+ }
160
+
161
+ const toolKey = execId || `${toolData.integratedToolType}-${toolData.toolFunction}`
162
+
146
163
  if (toolData.type === 'EXECUTING_TOOL') {
147
164
  this.executingTools.set(toolKey, {
148
165
  integratedToolType: toolData.integratedToolType,
@@ -155,10 +172,12 @@ export class MessageSegmentAccumulator {
155
172
  (s): s is ToolExecutionSegment =>
156
173
  s.type === 'tool_execution' &&
157
174
  s.data.type === 'EXECUTING_TOOL' &&
158
- s.data.integratedToolType === toolData.integratedToolType &&
159
- s.data.toolFunction === toolData.toolFunction
175
+ (execId
176
+ ? s.data.toolExecutionRequestId === execId
177
+ : s.data.integratedToolType === toolData.integratedToolType &&
178
+ s.data.toolFunction === toolData.toolFunction),
160
179
  )
161
-
180
+
162
181
  const executingTool = this.executingTools.get(toolKey)
163
182
  const mergedSegment: ToolExecutionSegment = {
164
183
  type: 'tool_execution',
@@ -167,19 +186,50 @@ export class MessageSegmentAccumulator {
167
186
  parameters: toolData.parameters || executingTool?.parameters,
168
187
  }
169
188
  }
170
-
189
+
171
190
  if (existingIndex !== -1) {
172
191
  this.segments[existingIndex] = mergedSegment
173
192
  } else {
174
193
  this.segments.push(mergedSegment)
175
194
  }
176
-
195
+
177
196
  this.executingTools.delete(toolKey)
178
197
  }
179
-
198
+
180
199
  return this.getSegments()
181
200
  }
182
201
 
202
+ /**
203
+ * Try to merge a tool execution event into an existing approval_batch
204
+ * segment whose `toolCalls` contains the same `toolExecutionRequestId`.
205
+ * Returns true when a batch was updated, false when no batch matches.
206
+ */
207
+ private applyExecutionToBatch(execId: string, toolData: ToolExecutionSegment['data']): boolean {
208
+ let matched = false
209
+ this.segments = this.segments.map((seg) => {
210
+ if (matched) return seg
211
+ if (seg.type !== 'approval_batch') return seg
212
+ const hasCall = seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)
213
+ if (!hasCall) return seg
214
+
215
+ const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
216
+ const next: ApprovalBatchExecutionState =
217
+ toolData.type === 'EXECUTED_TOOL'
218
+ ? { status: 'done', result: toolData.result, success: toolData.success }
219
+ : { status: 'executing', result: prev?.result, success: prev?.success }
220
+
221
+ matched = true
222
+ return {
223
+ ...seg,
224
+ data: {
225
+ ...seg.data,
226
+ executions: { ...(seg.data.executions ?? {}), [execId]: next },
227
+ },
228
+ }
229
+ })
230
+ return matched
231
+ }
232
+
183
233
  /**
184
234
  * Track a pending approval request
185
235
  */
@@ -209,7 +259,67 @@ export class MessageSegmentAccumulator {
209
259
  onApprove: this.callbacks.onApprove,
210
260
  onReject: this.callbacks.onReject,
211
261
  }
212
-
262
+
263
+ this.segments.push(segment)
264
+ return this.getSegments()
265
+ }
266
+
267
+ /**
268
+ * Add a batch approval segment containing multiple tool calls. Upserts by
269
+ * `approvalRequestId`: when a batch with the same id is already in the
270
+ * accumulator, the existing segment is updated in place rather than a
271
+ * second segment being pushed. This matters for the consumer-store replay
272
+ * path, which feeds `[existing..., new...]` into `replaySegments` and would
273
+ * otherwise produce two batch segments for the same approval after a
274
+ * status flip or per-tool execution merge.
275
+ *
276
+ * `approvalType` is the highest-privilege type required across the batch.
277
+ * `executions` is forwarded as-is. On upsert, a new `executions` object
278
+ * overrides the existing one (so the latest replay wins).
279
+ */
280
+ addApprovalBatch(
281
+ approvalRequestId: string,
282
+ approvalType: string,
283
+ toolCalls: PendingToolCallData[],
284
+ status: ChatApprovalStatus = 'pending',
285
+ executions?: Record<string, ApprovalBatchExecutionState>,
286
+ ): MessageSegment[] {
287
+ const existingIndex = this.segments.findIndex(
288
+ (s): s is ApprovalBatchSegment =>
289
+ s.type === 'approval_batch' && s.data.approvalRequestId === approvalRequestId,
290
+ )
291
+
292
+ if (existingIndex !== -1) {
293
+ const existing = this.segments[existingIndex] as ApprovalBatchSegment
294
+ const mergedExecutions = executions ?? existing.data.executions
295
+ this.segments[existingIndex] = {
296
+ ...existing,
297
+ data: {
298
+ approvalRequestId,
299
+ approvalType,
300
+ toolCalls,
301
+ ...(mergedExecutions ? { executions: mergedExecutions } : {}),
302
+ },
303
+ status,
304
+ onApprove: this.callbacks.onApprove,
305
+ onReject: this.callbacks.onReject,
306
+ }
307
+ return this.getSegments()
308
+ }
309
+
310
+ const segment: ApprovalBatchSegment = {
311
+ type: 'approval_batch',
312
+ data: {
313
+ approvalRequestId,
314
+ approvalType,
315
+ toolCalls,
316
+ ...(executions ? { executions } : {}),
317
+ },
318
+ status,
319
+ onApprove: this.callbacks.onApprove,
320
+ onReject: this.callbacks.onReject,
321
+ }
322
+
213
323
  this.segments.push(segment)
214
324
  return this.getSegments()
215
325
  }
@@ -249,13 +359,16 @@ export class MessageSegmentAccumulator {
249
359
  }
250
360
 
251
361
  /**
252
- * Update status of an existing approval segment
362
+ * Update status of an existing approval segment (single or batch)
253
363
  */
254
364
  updateApprovalStatus(requestId: string, status: ChatApprovalStatus): MessageSegment[] {
255
365
  this.segments = this.segments.map(segment => {
256
366
  if (segment.type === 'approval_request' && segment.data.requestId === requestId) {
257
367
  return { ...segment, status }
258
368
  }
369
+ if (segment.type === 'approval_batch' && segment.data.approvalRequestId === requestId) {
370
+ return { ...segment, status }
371
+ }
259
372
  return segment
260
373
  })
261
374
  return this.getSegments()
@@ -366,6 +479,17 @@ export class MessageSegmentAccumulator {
366
479
  )
367
480
  break
368
481
  }
482
+ case 'approval_batch': {
483
+ const { data, status } = segment
484
+ this.addApprovalBatch(
485
+ data.approvalRequestId,
486
+ data.approvalType,
487
+ data.toolCalls,
488
+ status,
489
+ data.executions,
490
+ )
491
+ break
492
+ }
369
493
  case 'error':
370
494
  this.addError(segment.title, segment.details)
371
495
  break