@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.
- package/dist/{chunk-AY3E5FKM.cjs → chunk-2MEBOTV4.cjs} +2263 -1698
- package/dist/chunk-2MEBOTV4.cjs.map +1 -0
- package/dist/{chunk-FEEPEOW2.js → chunk-J3ZCNPDM.js} +6128 -5563
- package/dist/chunk-J3ZCNPDM.js.map +1 -0
- package/dist/components/chat/approval-batch-message.d.ts +10 -0
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -0
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/chat-message-loader.d.ts +23 -0
- package/dist/components/chat/chat-message-loader.d.ts.map +1 -0
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-collapsible.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-delayed-flag.d.ts +25 -0
- package/dist/components/chat/hooks/use-delayed-flag.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.d.ts +3 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/thinking-display.d.ts.map +1 -1
- package/dist/components/chat/tool-call-blocks.d.ts +18 -0
- package/dist/components/chat/tool-call-blocks.d.ts.map +1 -0
- package/dist/components/chat/tool-execution-display.d.ts +1 -1
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/types/api.types.d.ts +16 -1
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +2 -2
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +54 -1
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +2 -0
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +14 -1
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +1 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +32 -4
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +2 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/chat/utils/tool-call-helpers.d.ts +38 -0
- package/dist/components/chat/utils/tool-call-helpers.d.ts.map +1 -0
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/index.cjs +14 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/ui/index.cjs +14 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +13 -1
- package/dist/components/ui/textarea.d.ts +11 -0
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -1
- package/package.json +3 -3
- package/src/components/chat/approval-batch-message.tsx +237 -0
- package/src/components/chat/chat-input.tsx +34 -106
- package/src/components/chat/chat-message-enhanced.tsx +11 -0
- package/src/components/chat/chat-message-list.tsx +22 -6
- package/src/components/chat/chat-message-loader.tsx +67 -0
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-collapsible.ts +10 -1
- package/src/components/chat/hooks/use-delayed-flag.ts +56 -0
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +109 -20
- package/src/components/chat/index.ts +3 -0
- package/src/components/chat/thinking-display.tsx +76 -12
- package/src/components/chat/tool-call-blocks.tsx +58 -0
- package/src/components/chat/tool-execution-display.tsx +60 -81
- package/src/components/chat/types/api.types.ts +22 -3
- package/src/components/chat/types/component.types.ts +2 -2
- package/src/components/chat/types/message.types.ts +58 -1
- package/src/components/chat/types/network.types.ts +2 -0
- package/src/components/chat/types/processing.types.ts +13 -2
- package/src/components/chat/utils/chunk-parser.ts +40 -5
- package/src/components/chat/utils/extract-incomplete-message-state.ts +19 -1
- package/src/components/chat/utils/index.ts +3 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +136 -12
- package/src/components/chat/utils/process-historical-messages.ts +88 -13
- package/src/components/chat/utils/tool-call-helpers.ts +105 -0
- package/src/components/ui/textarea.tsx +107 -25
- package/dist/chunk-AY3E5FKM.cjs.map +0 -1
- package/dist/chunk-FEEPEOW2.js.map +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react"
|
|
4
|
+
|
|
5
|
+
interface UseDelayedFlagOptions {
|
|
6
|
+
/** Wait this long before flipping the returned flag to `true`. Default 200ms. */
|
|
7
|
+
delay?: number
|
|
8
|
+
/**
|
|
9
|
+
* Once the flag has gone `true`, hold it for at least this long even if
|
|
10
|
+
* the source signal flips back to `false` sooner. Prevents a sub-frame
|
|
11
|
+
* flash when the operation finishes right after the loader appeared.
|
|
12
|
+
* Default 400ms.
|
|
13
|
+
*/
|
|
14
|
+
minDuration?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Debounce + min-hold gate for transient boolean flags (most useful for
|
|
19
|
+
* loading indicators).
|
|
20
|
+
*
|
|
21
|
+
* - source `true` → wait `delay` ms; if still true, flip output to `true`
|
|
22
|
+
* - source `false` → if output is `true`, hold for `minDuration - elapsed`
|
|
23
|
+
* ms before flipping back; otherwise flip immediately
|
|
24
|
+
*
|
|
25
|
+
* The net effect: fast operations never trigger the indicator, and slow
|
|
26
|
+
* ones always show it for long enough to avoid a flicker.
|
|
27
|
+
*/
|
|
28
|
+
export function useDelayedFlag(
|
|
29
|
+
source: boolean,
|
|
30
|
+
{ delay = 200, minDuration = 400 }: UseDelayedFlagOptions = {},
|
|
31
|
+
): boolean {
|
|
32
|
+
const [active, setActive] = useState(false)
|
|
33
|
+
const activatedAtRef = useRef<number | null>(null)
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (source) {
|
|
37
|
+
if (active) return
|
|
38
|
+
const timer = window.setTimeout(() => {
|
|
39
|
+
activatedAtRef.current = Date.now()
|
|
40
|
+
setActive(true)
|
|
41
|
+
}, delay)
|
|
42
|
+
return () => window.clearTimeout(timer)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!active) return
|
|
46
|
+
const elapsed = activatedAtRef.current ? Date.now() - activatedAtRef.current : minDuration
|
|
47
|
+
const remaining = Math.max(0, minDuration - elapsed)
|
|
48
|
+
const timer = window.setTimeout(() => {
|
|
49
|
+
activatedAtRef.current = null
|
|
50
|
+
setActive(false)
|
|
51
|
+
}, remaining)
|
|
52
|
+
return () => window.clearTimeout(timer)
|
|
53
|
+
}, [source, active, delay, minDuration])
|
|
54
|
+
|
|
55
|
+
return active
|
|
56
|
+
}
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
createMessageSegmentAccumulator,
|
|
13
13
|
} from '../utils/message-segment-accumulator'
|
|
14
14
|
import { MESSAGE_TYPE } from '../types'
|
|
15
|
-
import type { UseRealtimeChunkProcessorReturn, UseRealtimeChunkProcessorOptions, ChatApprovalStatus } from '../types'
|
|
15
|
+
import type { UseRealtimeChunkProcessorReturn, UseRealtimeChunkProcessorOptions, ChatApprovalStatus, PendingToolCallData } from '../types'
|
|
16
|
+
import { getCommandText } from '../utils/tool-call-helpers'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Hook for processing real-time NATS chunks into message segments
|
|
@@ -20,7 +21,17 @@ import type { UseRealtimeChunkProcessorReturn, UseRealtimeChunkProcessorOptions,
|
|
|
20
21
|
export function useRealtimeChunkProcessor(
|
|
21
22
|
options: UseRealtimeChunkProcessorOptions
|
|
22
23
|
): UseRealtimeChunkProcessorReturn {
|
|
23
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
callbacks,
|
|
26
|
+
displayApprovalTypes = ['CLIENT'],
|
|
27
|
+
approvalStatuses = {},
|
|
28
|
+
initialState,
|
|
29
|
+
enableThinking = false,
|
|
30
|
+
// Owned by the consumer (e.g. oss-tenant chat client / openframe-frontend
|
|
31
|
+
// tickets view). Default ON so consumers that haven't wired the flag yet
|
|
32
|
+
// get the new batch UI; pass `false` explicitly to fall back to legacy.
|
|
33
|
+
batchApprovalsEnabled = true,
|
|
34
|
+
} = options
|
|
24
35
|
|
|
25
36
|
const accumulatorRef = useRef<MessageSegmentAccumulator>(
|
|
26
37
|
createMessageSegmentAccumulator({
|
|
@@ -50,10 +61,10 @@ export function useRealtimeChunkProcessor(
|
|
|
50
61
|
|
|
51
62
|
const isInStreamRef = useRef(false)
|
|
52
63
|
|
|
53
|
-
// Track pending escalated approvals
|
|
54
|
-
const pendingEscalatedRef = useRef<
|
|
55
|
-
|
|
56
|
-
)
|
|
64
|
+
// Track pending escalated approvals (single or batch)
|
|
65
|
+
const pendingEscalatedRef = useRef<
|
|
66
|
+
Map<string, { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }>
|
|
67
|
+
>(new Map())
|
|
57
68
|
|
|
58
69
|
const processChunk = useCallback(
|
|
59
70
|
(chunk: unknown) => {
|
|
@@ -128,28 +139,106 @@ export function useRealtimeChunkProcessor(
|
|
|
128
139
|
break
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
case 'approval_batch': {
|
|
143
|
+
const { requestId, approvalType, toolCalls } = action
|
|
144
|
+
const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus
|
|
145
|
+
|
|
146
|
+
if (!displayApprovalTypes.includes(approvalType)) {
|
|
147
|
+
// Escalated: keep batch context locally for replay on result; surface a
|
|
148
|
+
// summary command via the legacy escalation callback.
|
|
149
|
+
const required = toolCalls.find((c) => c.requiresApproval) ?? toolCalls[0]
|
|
150
|
+
const summary = required ? getCommandText(required) : `Batch of ${toolCalls.length} tool calls`
|
|
151
|
+
pendingEscalatedRef.current.set(requestId, {
|
|
152
|
+
command: summary,
|
|
153
|
+
explanation: required?.toolExplanation,
|
|
154
|
+
approvalType,
|
|
155
|
+
toolCalls,
|
|
156
|
+
})
|
|
157
|
+
callbacks.onEscalatedApproval?.(requestId, {
|
|
158
|
+
command: summary,
|
|
159
|
+
explanation: required?.toolExplanation,
|
|
160
|
+
approvalType,
|
|
161
|
+
})
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (batchApprovalsEnabled) {
|
|
166
|
+
const segments = accumulator.addApprovalBatch(requestId, approvalType, toolCalls, status)
|
|
167
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Flag OFF — unfold batch into N legacy approval cards. They share
|
|
172
|
+
// `requestId`, so a click on any will approve the whole batch via a
|
|
173
|
+
// single backend call, and the resulting APPROVAL_RESULT chunk will
|
|
174
|
+
// flip status on every matching segment (see updateApprovalStatus).
|
|
175
|
+
let segments = accumulator.getSegments()
|
|
176
|
+
for (const call of toolCalls) {
|
|
177
|
+
if (!call.requiresApproval) continue
|
|
178
|
+
segments = accumulator.addApprovalRequest(
|
|
179
|
+
requestId,
|
|
180
|
+
getCommandText(call),
|
|
181
|
+
call.toolExplanation,
|
|
182
|
+
approvalType,
|
|
183
|
+
status,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
|
|
131
190
|
case 'approval_result': {
|
|
132
191
|
const { requestId, approved, approvalType } = action
|
|
133
192
|
const escalatedData = pendingEscalatedRef.current.get(requestId)
|
|
193
|
+
const status: ChatApprovalStatus = approved ? 'approved' : 'rejected'
|
|
194
|
+
|
|
134
195
|
if (escalatedData) {
|
|
135
196
|
pendingEscalatedRef.current.delete(requestId)
|
|
136
|
-
callbacks.onEscalatedApprovalResult?.(requestId, approved,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
197
|
+
callbacks.onEscalatedApprovalResult?.(requestId, approved, {
|
|
198
|
+
command: escalatedData.command,
|
|
199
|
+
explanation: escalatedData.explanation,
|
|
200
|
+
approvalType: escalatedData.approvalType,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (escalatedData.toolCalls && escalatedData.toolCalls.length > 0) {
|
|
204
|
+
if (batchApprovalsEnabled) {
|
|
205
|
+
const segments = accumulator.addApprovalBatch(
|
|
206
|
+
requestId,
|
|
207
|
+
escalatedData.approvalType,
|
|
208
|
+
escalatedData.toolCalls,
|
|
209
|
+
status,
|
|
210
|
+
)
|
|
211
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
212
|
+
} else {
|
|
213
|
+
let segments = accumulator.getSegments()
|
|
214
|
+
for (const call of escalatedData.toolCalls) {
|
|
215
|
+
if (!call.requiresApproval) continue
|
|
216
|
+
segments = accumulator.addApprovalRequest(
|
|
217
|
+
requestId,
|
|
218
|
+
getCommandText(call),
|
|
219
|
+
call.toolExplanation,
|
|
220
|
+
escalatedData.approvalType,
|
|
221
|
+
status,
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
const segments = accumulator.addApprovalRequest(
|
|
228
|
+
requestId,
|
|
229
|
+
escalatedData.command,
|
|
230
|
+
escalatedData.explanation,
|
|
231
|
+
escalatedData.approvalType,
|
|
232
|
+
status,
|
|
233
|
+
)
|
|
234
|
+
callbacks.onSegmentsUpdate?.(segments)
|
|
235
|
+
}
|
|
146
236
|
} else {
|
|
147
|
-
const segments = accumulator.updateApprovalStatus(
|
|
148
|
-
requestId,
|
|
149
|
-
approved ? 'approved' : 'rejected'
|
|
150
|
-
)
|
|
237
|
+
const segments = accumulator.updateApprovalStatus(requestId, status)
|
|
151
238
|
callbacks.onSegmentsUpdate?.(segments)
|
|
152
239
|
}
|
|
240
|
+
// approvalType from the result is informational; not consumed downstream yet.
|
|
241
|
+
void approvalType
|
|
153
242
|
break
|
|
154
243
|
}
|
|
155
244
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Chat components exports
|
|
4
4
|
export * from './approval-request-message'
|
|
5
|
+
export * from './approval-batch-message'
|
|
5
6
|
export * from './context-compaction-display'
|
|
6
7
|
export * from './expand-chevron'
|
|
7
8
|
export * from './thinking-display'
|
|
@@ -11,11 +12,13 @@ export * from './chat-input'
|
|
|
11
12
|
export * from './slash-command-suggestions'
|
|
12
13
|
export * from './chat-message-enhanced'
|
|
13
14
|
export * from './chat-message-list'
|
|
15
|
+
export * from './chat-message-loader'
|
|
14
16
|
export * from './chat-quick-action'
|
|
15
17
|
export * from './chat-ticket-item'
|
|
16
18
|
export * from './chat-ticket-list'
|
|
17
19
|
export * from './chat-typing-indicator'
|
|
18
20
|
export * from './tool-execution-display'
|
|
21
|
+
export * from './tool-call-blocks'
|
|
19
22
|
export * from './model-display'
|
|
20
23
|
export * from './chat-sidebar'
|
|
21
24
|
export type { ChatRef } from './chat-ref.types'
|
|
@@ -4,21 +4,75 @@ import { forwardRef, useState } from "react"
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../utils/cn"
|
|
6
6
|
import { PulseDots } from "../ui/pulse-dots"
|
|
7
|
+
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
7
8
|
import { ExpandChevron } from "./expand-chevron"
|
|
8
9
|
import { useCollapsible } from "./hooks/use-collapsible"
|
|
9
10
|
import type { ThinkingDisplayProps } from "./types"
|
|
10
11
|
|
|
12
|
+
// Tame SimpleMarkdownRenderer for the muted "thought" aesthetic:
|
|
13
|
+
// - all text in secondary tone with small size
|
|
14
|
+
// - bold pops back to primary so emphasis still reads
|
|
15
|
+
// - tighter paragraph/list/heading spacing (renderer ships with mt-8 mb-4)
|
|
16
|
+
// - flatter horizontal rules
|
|
17
|
+
// - inline-code visually quieter inside thoughts
|
|
18
|
+
// - fenced code blocks (`.code-block-container`) lose the renderer's `my-6`
|
|
19
|
+
// and inner `p-4` so they sit flush with surrounding paragraphs
|
|
20
|
+
const thoughtMdClasses = [
|
|
21
|
+
// Body & lists — muted grey, never white.
|
|
22
|
+
"[&_p]:!text-sm [&_p]:!text-ods-text-secondary [&_p]:!leading-snug [&_p]:!my-2 [&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0",
|
|
23
|
+
"[&_li]:!text-sm [&_li]:!text-ods-text-secondary [&_li]:!leading-snug",
|
|
24
|
+
"[&_ul]:!my-2 [&_ol]:!my-2 [&_ul]:!pl-5 [&_ol]:!pl-5",
|
|
25
|
+
// Inline emphasis stays grey too. Bold pops via weight (no color shift),
|
|
26
|
+
// italic via slant — neither is allowed to brighten into white.
|
|
27
|
+
"[&_strong]:!text-ods-text-secondary [&_strong]:!font-semibold",
|
|
28
|
+
"[&_em]:!text-ods-text-secondary",
|
|
29
|
+
// Headings — SimpleMarkdownRenderer paints them `text-ods-text-primary`
|
|
30
|
+
// by default. Override every level back to secondary grey so headings
|
|
31
|
+
// don't suddenly turn white inside a thought block.
|
|
32
|
+
"[&_h1]:!text-sm [&_h1]:!text-ods-text-secondary [&_h1]:!font-semibold [&_h1]:!mt-3 [&_h1]:!mb-1 [&_h1]:!pb-0 [&_h1]:!border-0",
|
|
33
|
+
"[&_h2]:!text-sm [&_h2]:!text-ods-text-secondary [&_h2]:!font-semibold [&_h2]:!mt-3 [&_h2]:!mb-1 [&_h2]:!pb-0 [&_h2]:!border-0",
|
|
34
|
+
"[&_h3]:!text-sm [&_h3]:!text-ods-text-secondary [&_h3]:!font-semibold [&_h3]:!mt-3 [&_h3]:!mb-1",
|
|
35
|
+
"[&_h4]:!text-sm [&_h4]:!text-ods-text-secondary [&_h4]:!font-semibold [&_h4]:!mt-2 [&_h4]:!mb-1",
|
|
36
|
+
"[&_h5]:!text-sm [&_h5]:!text-ods-text-secondary [&_h5]:!font-semibold [&_h5]:!mt-2 [&_h5]:!mb-1",
|
|
37
|
+
"[&_h6]:!text-sm [&_h6]:!text-ods-text-secondary [&_h6]:!font-semibold [&_h6]:!mt-2 [&_h6]:!mb-1",
|
|
38
|
+
// Rules
|
|
39
|
+
"[&_hr]:!my-3 [&_hr]:!border-ods-border [&_hr]:!opacity-40",
|
|
40
|
+
// Inline code — Cursor-style chip: no border, background a notch
|
|
41
|
+
// LIGHTER than the surrounding thought card (`bg-ods-border` is the
|
|
42
|
+
// brightest neutral grey in the palette and sits above `bg-ods-card`),
|
|
43
|
+
// small rounded corners, tight padding. Reads as a clearly raised token
|
|
44
|
+
// without the loud bordered chip of the default renderer.
|
|
45
|
+
"[&_code]:!text-[12px] [&_code]:!text-ods-text-secondary",
|
|
46
|
+
"[&_code]:!border-0 [&_code]:!bg-ods-border [&_code]:!rounded",
|
|
47
|
+
"[&_code]:!px-1.5 [&_code]:!py-0.5",
|
|
48
|
+
"[&_.code-block-container]:!my-2",
|
|
49
|
+
"[&_.code-block-container_.code-header]:!py-1 [&_.code-block-container_.code-header]:!px-3",
|
|
50
|
+
"[&_.code-block-container_>div:last-child]:!p-3",
|
|
51
|
+
].join(" ")
|
|
52
|
+
|
|
11
53
|
const ThinkingDisplay = forwardRef<HTMLDivElement, ThinkingDisplayProps>(
|
|
12
54
|
({ className, text, isStreaming = false, ...props }, ref) => {
|
|
13
55
|
const [expanded, setExpanded] = useState(false)
|
|
56
|
+
// 26px ≈ one line of `text-sm leading-snug` (~19px) + space for the
|
|
57
|
+
// bordered/padded `<code>` badges that markdown renders inline. With
|
|
58
|
+
// exact 1lh the bottom of those badges gets clipped.
|
|
14
59
|
const { innerRef, isOverflowing, containerStyle } = useCollapsible({
|
|
15
60
|
expanded,
|
|
16
|
-
collapsedHeight:
|
|
61
|
+
collapsedHeight: 19,
|
|
17
62
|
disableTransition: isStreaming,
|
|
18
63
|
})
|
|
19
64
|
const label = isStreaming ? "Thinking" : "Thought"
|
|
20
65
|
const canToggle = isOverflowing || expanded
|
|
21
66
|
|
|
67
|
+
const toggle = () => canToggle && setExpanded(!expanded)
|
|
68
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
69
|
+
if (!canToggle) return
|
|
70
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
71
|
+
e.preventDefault()
|
|
72
|
+
setExpanded(!expanded)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
22
76
|
return (
|
|
23
77
|
<div
|
|
24
78
|
ref={ref}
|
|
@@ -28,29 +82,39 @@ const ThinkingDisplay = forwardRef<HTMLDivElement, ThinkingDisplayProps>(
|
|
|
28
82
|
)}
|
|
29
83
|
{...props}
|
|
30
84
|
>
|
|
31
|
-
<
|
|
32
|
-
|
|
85
|
+
<div
|
|
86
|
+
role="button"
|
|
87
|
+
tabIndex={canToggle ? 0 : -1}
|
|
88
|
+
aria-expanded={expanded}
|
|
89
|
+
onClick={toggle}
|
|
90
|
+
onKeyDown={handleKeyDown}
|
|
33
91
|
className={cn(
|
|
34
92
|
"w-full flex items-start gap-3 text-left",
|
|
35
93
|
canToggle ? "cursor-pointer" : "cursor-default"
|
|
36
94
|
)}
|
|
37
|
-
onClick={() => canToggle && setExpanded(!expanded)}
|
|
38
95
|
>
|
|
39
|
-
<div className="flex items-center gap-1 shrink-0
|
|
40
|
-
<span className="text-sm font-medium text-ods-text-secondary">
|
|
41
|
-
{label}
|
|
96
|
+
<div className="flex items-center gap-1 shrink-0 h-[19px]">
|
|
97
|
+
<span className="text-sm font-medium leading-snug text-ods-text-secondary">
|
|
98
|
+
{label}:
|
|
42
99
|
</span>
|
|
43
100
|
{isStreaming && <PulseDots size="sm" />}
|
|
44
101
|
</div>
|
|
45
102
|
|
|
46
|
-
<div className="flex-1 min-w-0
|
|
47
|
-
<div
|
|
48
|
-
{
|
|
103
|
+
<div className="flex-1 min-w-0" style={containerStyle}>
|
|
104
|
+
<div
|
|
105
|
+
ref={innerRef}
|
|
106
|
+
className={cn("text-sm leading-snug text-ods-text-secondary", thoughtMdClasses)}
|
|
107
|
+
>
|
|
108
|
+
<SimpleMarkdownRenderer content={text} textSize="compact" />
|
|
49
109
|
</div>
|
|
50
110
|
</div>
|
|
51
111
|
|
|
52
|
-
{canToggle &&
|
|
53
|
-
|
|
112
|
+
{canToggle && (
|
|
113
|
+
<div className="h-[19px] flex items-center shrink-0">
|
|
114
|
+
<ExpandChevron expanded={expanded} />
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
54
118
|
</div>
|
|
55
119
|
)
|
|
56
120
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../utils/cn"
|
|
4
|
+
import { formatToolArgValue, formatToolResult } from "./utils/tool-call-helpers"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Single arg row: inline `key: value` or labeled `<pre>` block.
|
|
8
|
+
* Shared between ApprovalBatchMessage and ToolExecutionDisplay so both
|
|
9
|
+
* surfaces render long script bodies / JSON the same way.
|
|
10
|
+
*/
|
|
11
|
+
export function ArgRow({ argKey, value }: { argKey: string; value: unknown }) {
|
|
12
|
+
const formatted = formatToolArgValue(value)
|
|
13
|
+
|
|
14
|
+
if (formatted.kind === "inline") {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex gap-1 items-start w-full">
|
|
17
|
+
<span className="shrink-0 text-ods-text-secondary">{argKey}:</span>
|
|
18
|
+
<span className="flex-1 min-w-0 text-ods-text-primary break-all">{formatted.text}</span>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col gap-1 items-start w-full">
|
|
25
|
+
<span className="text-ods-text-secondary">{argKey}:</span>
|
|
26
|
+
<pre className="bg-ods-bg border border-ods-border rounded-md p-3 w-full max-h-64 overflow-auto text-xs leading-5 text-ods-text-primary font-mono whitespace-pre">
|
|
27
|
+
<code>{formatted.text}</code>
|
|
28
|
+
</pre>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result block for a tool execution. Renders inline for short single-line
|
|
35
|
+
* output and as a scrollable `<pre>` for code / JSON / multiline output.
|
|
36
|
+
*/
|
|
37
|
+
export function ResultBlock({ result, className }: { result: string | undefined | null; className?: string }) {
|
|
38
|
+
const formatted = formatToolResult(result)
|
|
39
|
+
if (!formatted.text) return null
|
|
40
|
+
|
|
41
|
+
if (formatted.kind === "inline") {
|
|
42
|
+
return (
|
|
43
|
+
<div className={cn("flex flex-col gap-1 items-start w-full", className)}>
|
|
44
|
+
<span className="text-ods-text-secondary">Result:</span>
|
|
45
|
+
<span className="text-ods-text-primary break-all">{formatted.text}</span>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={cn("flex flex-col gap-1 items-start w-full", className)}>
|
|
52
|
+
<span className="text-ods-text-secondary">Result:</span>
|
|
53
|
+
<pre className="bg-ods-bg border border-ods-border rounded-md p-3 w-full max-h-64 overflow-auto text-xs leading-5 text-ods-text-primary font-mono whitespace-pre">
|
|
54
|
+
<code>{formatted.text}</code>
|
|
55
|
+
</pre>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -9,119 +9,98 @@ import { ToolIcon } from "../tool-icon"
|
|
|
9
9
|
import { PulseDots } from "../ui/pulse-dots"
|
|
10
10
|
import { ExpandChevron } from "./expand-chevron"
|
|
11
11
|
import { useCollapsible } from "./hooks/use-collapsible"
|
|
12
|
+
import { ArgRow, ResultBlock } from "./tool-call-blocks"
|
|
13
|
+
import { COMMAND_BODY_ARG_KEYS } from "./utils/tool-call-helpers"
|
|
12
14
|
import type { ToolExecutionDisplayProps } from "./types"
|
|
13
15
|
|
|
14
|
-
const
|
|
16
|
+
const COMMAND_BODY_KEYS = new Set<string>(COMMAND_BODY_ARG_KEYS)
|
|
17
|
+
|
|
18
|
+
const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProps>(
|
|
15
19
|
({ className, message, ...props }, ref) => {
|
|
16
20
|
const [expanded, setExpanded] = useState(false)
|
|
17
21
|
const { innerRef, containerStyle } = useCollapsible({ expanded })
|
|
18
22
|
|
|
19
23
|
const isExecuting = message.type === "EXECUTING_TOOL"
|
|
20
24
|
const isExecuted = message.type === "EXECUTED_TOOL"
|
|
21
|
-
const integratedToolType = message.integratedToolType ||
|
|
25
|
+
const integratedToolType = (message.integratedToolType as ToolType) || ("OPENFRAME" as ToolType)
|
|
22
26
|
|
|
23
27
|
const previewText = useMemo(() => {
|
|
24
28
|
const command = message.parameters?.command ?? message.parameters?.query
|
|
25
29
|
if (command) return String(command)
|
|
26
|
-
return message.toolFunction ??
|
|
30
|
+
return message.toolFunction ?? ""
|
|
27
31
|
}, [message.toolFunction, message.parameters])
|
|
28
32
|
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
33
|
+
const argEntries = useMemo<Array<[string, unknown]>>(() => {
|
|
34
|
+
if (!message.parameters || typeof message.parameters !== "object") return []
|
|
35
|
+
return Object.entries(message.parameters).filter(
|
|
36
|
+
([k, v]) => !COMMAND_BODY_KEYS.has(k) && v !== null && v !== undefined && v !== "",
|
|
37
|
+
)
|
|
38
|
+
}, [message.parameters])
|
|
35
39
|
|
|
36
|
-
const
|
|
37
|
-
const
|
|
40
|
+
const hasResult = isExecuted && typeof message.result === "string" && message.result.length > 0
|
|
41
|
+
const hasBody = argEntries.length > 0 || hasResult || isExecuting
|
|
38
42
|
|
|
39
43
|
const renderStatusIcon = () => {
|
|
40
|
-
if (isExecuting)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (isExecuted && message.success === true) {
|
|
44
|
-
return <CheckCircleIcon className="w-4 h-4 text-ods-success" />
|
|
45
|
-
}
|
|
46
|
-
if (isExecuted && message.success === false) {
|
|
47
|
-
return <XmarkCircleIcon className="w-4 h-4 text-ods-error" />
|
|
48
|
-
}
|
|
44
|
+
if (isExecuting) return <PulseDots size="sm" />
|
|
45
|
+
if (isExecuted && message.success === true) return <CheckCircleIcon className="w-4 h-4 text-ods-success" />
|
|
46
|
+
if (isExecuted && message.success === false) return <XmarkCircleIcon className="w-4 h-4 text-ods-error" />
|
|
49
47
|
return null
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
return (
|
|
53
|
-
<
|
|
51
|
+
<div
|
|
54
52
|
ref={ref}
|
|
55
|
-
type="button"
|
|
56
53
|
className={cn(
|
|
57
|
-
"bg-ods-card border border-ods-border rounded-[6px]
|
|
58
|
-
className
|
|
54
|
+
"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full flex flex-col",
|
|
55
|
+
className,
|
|
59
56
|
)}
|
|
60
|
-
onClick={() => setExpanded(!expanded)}
|
|
61
57
|
{...props}
|
|
62
58
|
>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<div className="flex
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{expanded ? message.toolFunction : previewText}
|
|
78
|
-
</div>
|
|
79
|
-
{hasStatus && (
|
|
80
|
-
<div className="flex items-center justify-center shrink-0 w-5 h-5">
|
|
81
|
-
{renderStatusIcon()}
|
|
82
|
-
</div>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="flex gap-[var(--spacing-system-xs)] items-start p-[var(--spacing-system-s)] cursor-pointer text-left w-full"
|
|
62
|
+
onClick={() => setExpanded((prev) => !prev)}
|
|
63
|
+
>
|
|
64
|
+
<div className="flex items-center justify-center shrink-0 w-5 h-5">
|
|
65
|
+
<ToolIcon toolType={integratedToolType} size={16} />
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
"flex-1 min-w-0 text-sm font-medium leading-5",
|
|
70
|
+
expanded
|
|
71
|
+
? "text-ods-text-primary whitespace-pre-wrap break-all"
|
|
72
|
+
: "text-ods-text-secondary line-clamp-2 max-h-10 break-all",
|
|
83
73
|
)}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</div>
|
|
74
|
+
>
|
|
75
|
+
{expanded ? message.toolFunction || previewText : previewText}
|
|
87
76
|
</div>
|
|
77
|
+
<div className="flex items-center justify-center shrink-0 w-5 h-5">{renderStatusIcon()}</div>
|
|
78
|
+
<div className="flex items-center justify-center shrink-0 w-5 h-5">
|
|
79
|
+
<ExpandChevron expanded={expanded} />
|
|
80
|
+
</div>
|
|
81
|
+
</button>
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{(isExecuting || (isExecuted && message.result)) && (
|
|
107
|
-
<div className="flex flex-col items-start w-full">
|
|
108
|
-
<p className="text-sm font-medium leading-5 text-ods-text-secondary">
|
|
109
|
-
Result:
|
|
110
|
-
</p>
|
|
111
|
-
{isExecuted && message.result && (
|
|
112
|
-
<div className="text-sm font-medium leading-5 text-ods-text-primary whitespace-pre-wrap break-all">
|
|
113
|
-
{message.result}
|
|
114
|
-
</div>
|
|
115
|
-
)}
|
|
116
|
-
{isExecuting && <PulseDots size="sm" />}
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
83
|
+
<div className="w-full" style={containerStyle}>
|
|
84
|
+
<div ref={innerRef}>
|
|
85
|
+
{hasBody && (
|
|
86
|
+
<div className="flex flex-col gap-2 items-start w-full text-sm font-medium leading-5 p-3 bg-ods-card">
|
|
87
|
+
{argEntries.map(([key, value]) => (
|
|
88
|
+
<ArgRow key={key} argKey={key} value={value} />
|
|
89
|
+
))}
|
|
90
|
+
{hasResult && <ResultBlock result={message.result} />}
|
|
91
|
+
{isExecuting && (
|
|
92
|
+
<div className="flex flex-col gap-1 items-start w-full">
|
|
93
|
+
<span className="text-ods-text-secondary">Result:</span>
|
|
94
|
+
<PulseDots size="sm" />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
120
99
|
</div>
|
|
121
100
|
</div>
|
|
122
|
-
</
|
|
101
|
+
</div>
|
|
123
102
|
)
|
|
124
|
-
}
|
|
103
|
+
},
|
|
125
104
|
)
|
|
126
105
|
|
|
127
106
|
ToolExecutionDisplay.displayName = "ToolExecutionDisplay"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ChunkData, NatsMessageType, FetchChunksFunction } from './network.types'
|
|
7
7
|
import type { ChatType, ChatApprovalStatus } from './chat.types'
|
|
8
|
-
import type { MessageSegment, TokenUsageData } from './message.types'
|
|
8
|
+
import type { MessageSegment, PendingToolCallData, TokenUsageData } from './message.types'
|
|
9
9
|
|
|
10
10
|
// ========== Hook Options ==========
|
|
11
11
|
|
|
@@ -152,7 +152,10 @@ export interface UseRealtimeChunkProcessorOptions {
|
|
|
152
152
|
/** Executing tools waiting for completion */
|
|
153
153
|
executingTools?: Map<string, { integratedToolType: string; toolFunction: string; parameters?: Record<string, any> }>
|
|
154
154
|
/** Escalated approvals */
|
|
155
|
-
escalatedApprovals?: Map<
|
|
155
|
+
escalatedApprovals?: Map<
|
|
156
|
+
string,
|
|
157
|
+
{ command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
|
|
158
|
+
>
|
|
156
159
|
}
|
|
157
160
|
/**
|
|
158
161
|
* When true, THINKING chunks are processed into thinking segments. When false
|
|
@@ -160,6 +163,19 @@ export interface UseRealtimeChunkProcessorOptions {
|
|
|
160
163
|
* accumulator or store.
|
|
161
164
|
*/
|
|
162
165
|
enableThinking?: boolean
|
|
166
|
+
/**
|
|
167
|
+
* Consumer-owned (e.g. set in `openframe-oss-tenant` chat client via the
|
|
168
|
+
* `'batch-approvals'` feature flag and forwarded here). The lib does NOT
|
|
169
|
+
* default this to a batch-on behavior — when omitted it falls back to the
|
|
170
|
+
* legacy single-card rendering.
|
|
171
|
+
*
|
|
172
|
+
* When true: `APPROVAL_REQUEST` chunks containing `toolCalls[]` are rendered
|
|
173
|
+
* as a single batch card. When false / omitted: the batch is split into N
|
|
174
|
+
* legacy approval cards (one per tool that requires approval), all sharing
|
|
175
|
+
* the same `approvalRequestId`. Tools with `requiresApproval=false` are
|
|
176
|
+
* dropped from the UI in the unfolded mode.
|
|
177
|
+
*/
|
|
178
|
+
batchApprovalsEnabled?: boolean
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
export interface UseRealtimeChunkProcessorReturn {
|
|
@@ -172,7 +188,10 @@ export interface UseRealtimeChunkProcessorReturn {
|
|
|
172
188
|
/** Update approval status for a request */
|
|
173
189
|
updateApprovalStatus: (requestId: string, status: ChatApprovalStatus) => MessageSegment[]
|
|
174
190
|
/** Get pending approval requests */
|
|
175
|
-
getPendingApprovals: () => Map<
|
|
191
|
+
getPendingApprovals: () => Map<
|
|
192
|
+
string,
|
|
193
|
+
{ command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
|
|
194
|
+
>
|
|
176
195
|
}
|
|
177
196
|
|
|
178
197
|
// ========== API Request Types ==========
|