@flamingo-stack/openframe-frontend-core 0.0.518 → 0.0.519
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/chat-protocol/events.d.ts +15 -1
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +25 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +25 -1
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/nats-decoder.d.ts +5 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/{chunk-262OTU4W.cjs → chunk-5PMRHRMM.cjs} +14 -14
- package/dist/{chunk-262OTU4W.cjs.map → chunk-5PMRHRMM.cjs.map} +1 -1
- package/dist/{chunk-SYQ4KUXW.cjs → chunk-6FLRFCK4.cjs} +1104 -879
- package/dist/chunk-6FLRFCK4.cjs.map +1 -0
- package/dist/{chunk-V76TAQ3O.js → chunk-6WQN7NLD.js} +4 -4
- package/dist/{chunk-FAOL7EI2.cjs → chunk-BFCBR277.cjs} +11 -11
- package/dist/{chunk-FAOL7EI2.cjs.map → chunk-BFCBR277.cjs.map} +1 -1
- package/dist/{chunk-V3K5H4I2.js → chunk-BUZOZOQ2.js} +2 -2
- package/dist/{chunk-U6NODSTY.cjs → chunk-CCK2MFIQ.cjs} +26 -26
- package/dist/{chunk-U6NODSTY.cjs.map → chunk-CCK2MFIQ.cjs.map} +1 -1
- package/dist/{chunk-J2JLAW2J.js → chunk-CRJRVUWB.js} +2 -2
- package/dist/{chunk-XDADNUC5.cjs → chunk-FWCENA3G.cjs} +31 -31
- package/dist/{chunk-XDADNUC5.cjs.map → chunk-FWCENA3G.cjs.map} +1 -1
- package/dist/{chunk-J3DRYWFP.cjs → chunk-GLFQSFYV.cjs} +61 -61
- package/dist/{chunk-J3DRYWFP.cjs.map → chunk-GLFQSFYV.cjs.map} +1 -1
- package/dist/{chunk-CV624KM6.js → chunk-HVXGSQ4V.js} +6 -6
- package/dist/{chunk-2ZHVOTSA.js → chunk-HWA3NZPK.js} +2 -2
- package/dist/{chunk-QOUKB6OJ.js → chunk-JMGVYERE.js} +2 -2
- package/dist/{chunk-WBEF37M5.cjs → chunk-KCDKQXE5.cjs} +7 -7
- package/dist/{chunk-WBEF37M5.cjs.map → chunk-KCDKQXE5.cjs.map} +1 -1
- package/dist/{chunk-QPCPYCU2.js → chunk-MEWHQVOU.js} +2 -2
- package/dist/{chunk-IPHH3GKC.cjs → chunk-OBJSXQ67.cjs} +3 -3
- package/dist/{chunk-IPHH3GKC.cjs.map → chunk-OBJSXQ67.cjs.map} +1 -1
- package/dist/{chunk-VCA3HNJ2.js → chunk-OQHHJDUB.js} +4 -4
- package/dist/{chunk-2EU3TWCW.cjs → chunk-QEGGWVFV.cjs} +26 -26
- package/dist/{chunk-2EU3TWCW.cjs.map → chunk-QEGGWVFV.cjs.map} +1 -1
- package/dist/{chunk-2AHSX63M.cjs → chunk-QGLEI6T2.cjs} +5 -5
- package/dist/{chunk-2AHSX63M.cjs.map → chunk-QGLEI6T2.cjs.map} +1 -1
- package/dist/{chunk-PJ4WHNJH.cjs → chunk-QQZWLTYU.cjs} +4 -4
- package/dist/{chunk-PJ4WHNJH.cjs.map → chunk-QQZWLTYU.cjs.map} +1 -1
- package/dist/{chunk-BBI7GD2Q.js → chunk-S4OAWVKF.js} +2 -2
- package/dist/{chunk-MY7663CW.js → chunk-SAPKMN4W.js} +2 -2
- package/dist/{chunk-FISSR67K.js → chunk-UBT2KBK7.js} +5 -5
- package/dist/{chunk-S3C4RT4O.cjs → chunk-V3MAG3G5.cjs} +87 -87
- package/dist/{chunk-S3C4RT4O.cjs.map → chunk-V3MAG3G5.cjs.map} +1 -1
- package/dist/{chunk-YHAYMOXR.js → chunk-VH7BOHRG.js} +2 -2
- package/dist/{chunk-QNJCZGHH.cjs → chunk-WEPJKF65.cjs} +37 -37
- package/dist/{chunk-QNJCZGHH.cjs.map → chunk-WEPJKF65.cjs.map} +1 -1
- package/dist/{chunk-6EAACE7G.js → chunk-YACGPO7I.js} +2 -2
- package/dist/{chunk-7SPVAAWE.js → chunk-ZA5PFNHR.js} +5975 -5750
- package/dist/chunk-ZA5PFNHR.js.map +1 -0
- package/dist/{chunk-LY7IT5QX.cjs → chunk-ZGGI4XMF.cjs} +9 -9
- package/dist/{chunk-LY7IT5QX.cjs.map → chunk-ZGGI4XMF.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/ask-display.d.ts +24 -0
- package/dist/components/chat/ask-display.d.ts.map +1 -0
- 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/index.cjs +4 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +3 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +25 -1
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +32 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +8 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +134 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +30 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +23 -0
- package/src/chat-protocol/events.ts +20 -0
- package/src/chat-protocol/nats-decoder.ts +32 -0
- package/src/components/chat/__tests__/ask-display.test.tsx +101 -0
- package/src/components/chat/__tests__/chat-message-list-ask.test.tsx +100 -0
- package/src/components/chat/ask-display.tsx +151 -0
- package/src/components/chat/chat-message-enhanced.tsx +39 -2
- package/src/components/chat/chat-message-list.tsx +18 -1
- package/src/components/chat/embeddable-chat.tsx +22 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +72 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +27 -0
- package/src/components/chat/types/component.types.ts +28 -1
- package/src/components/chat/types/message.types.ts +35 -1
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +74 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +15 -0
- package/src/components/chat/utils/process-historical-messages.ts +26 -0
- package/dist/chunk-7SPVAAWE.js.map +0 -1
- package/dist/chunk-SYQ4KUXW.cjs.map +0 -1
- /package/dist/{chunk-V76TAQ3O.js.map → chunk-6WQN7NLD.js.map} +0 -0
- /package/dist/{chunk-V3K5H4I2.js.map → chunk-BUZOZOQ2.js.map} +0 -0
- /package/dist/{chunk-J2JLAW2J.js.map → chunk-CRJRVUWB.js.map} +0 -0
- /package/dist/{chunk-CV624KM6.js.map → chunk-HVXGSQ4V.js.map} +0 -0
- /package/dist/{chunk-2ZHVOTSA.js.map → chunk-HWA3NZPK.js.map} +0 -0
- /package/dist/{chunk-QOUKB6OJ.js.map → chunk-JMGVYERE.js.map} +0 -0
- /package/dist/{chunk-QPCPYCU2.js.map → chunk-MEWHQVOU.js.map} +0 -0
- /package/dist/{chunk-VCA3HNJ2.js.map → chunk-OQHHJDUB.js.map} +0 -0
- /package/dist/{chunk-BBI7GD2Q.js.map → chunk-S4OAWVKF.js.map} +0 -0
- /package/dist/{chunk-MY7663CW.js.map → chunk-SAPKMN4W.js.map} +0 -0
- /package/dist/{chunk-FISSR67K.js.map → chunk-UBT2KBK7.js.map} +0 -0
- /package/dist/{chunk-YHAYMOXR.js.map → chunk-VH7BOHRG.js.map} +0 -0
- /package/dist/{chunk-6EAACE7G.js.map → chunk-YACGPO7I.js.map} +0 -0
|
@@ -1584,6 +1584,24 @@ function EmbeddableChatInner({
|
|
|
1584
1584
|
[sendMessage, readyAttachments, viewUrlPrefix, clearAttachments, contextItems],
|
|
1585
1585
|
)
|
|
1586
1586
|
|
|
1587
|
+
/**
|
|
1588
|
+
* Picking an option on a clarification card — deliberately NOT `handleSend`.
|
|
1589
|
+
*
|
|
1590
|
+
* `handleSend` belongs to the composer: it appends the staged attachments'
|
|
1591
|
+
* markdown to the text, ships the staged context items, and then clears both.
|
|
1592
|
+
* Routing an ask click through it would (a) send `label + attachment markdown`
|
|
1593
|
+
* where the backend's classifier expects the label VERBATIM to resolve which
|
|
1594
|
+
* reading was picked, and (b) consume a draft's attachments/context into a
|
|
1595
|
+
* one-word answer the user never attached them to. So the label goes out on
|
|
1596
|
+
* its own and whatever is staged in the composer stays staged.
|
|
1597
|
+
*/
|
|
1598
|
+
const handleAskSelect = useCallback(
|
|
1599
|
+
(label: string) => {
|
|
1600
|
+
sendMessage(label)
|
|
1601
|
+
},
|
|
1602
|
+
[sendMessage],
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1587
1605
|
// Admin "try-asking chips" → GUIDE-mode quick-action chips only (Mingo mode
|
|
1588
1606
|
// deliberately doesn't surface them). Clicking one SENDS the query immediately
|
|
1589
1607
|
// via the GuideWelcome `onQuickAction` → `handleSend` at the render site (no
|
|
@@ -2351,6 +2369,10 @@ function EmbeddableChatInner({
|
|
|
2351
2369
|
resolveContextIcon={resolveContextIcon}
|
|
2352
2370
|
renderContextItem={renderContextItem}
|
|
2353
2371
|
renderMention={renderMention}
|
|
2372
|
+
// Gated on `chatLoading` for the same reason the composer
|
|
2373
|
+
// is: no second send while a turn is in flight. Passive
|
|
2374
|
+
// demo hosts (previewMode) stay read-only.
|
|
2375
|
+
onAskSelect={chatLoading || previewMode ? undefined : handleAskSelect}
|
|
2354
2376
|
NavLinkAnchor={NavLinkAnchorViaRuntime}
|
|
2355
2377
|
// Real Mingo drawer: hide the message-list scrollbar
|
|
2356
2378
|
// (scroll stays functional). Scoped here via `className`
|
|
@@ -7,6 +7,7 @@ export * from './context-compaction-display'
|
|
|
7
7
|
export * from './expand-chevron'
|
|
8
8
|
export * from './thinking-display'
|
|
9
9
|
export * from './guide-display'
|
|
10
|
+
export * from './ask-display'
|
|
10
11
|
export * from './error-message-display'
|
|
11
12
|
export * from './chat-container'
|
|
12
13
|
export * from './chat-input'
|
|
@@ -1328,3 +1328,75 @@ describe('agentBusy on initializeWithState', () => {
|
|
|
1328
1328
|
expect(r.state.streamingPhase).toBe('idle')
|
|
1329
1329
|
})
|
|
1330
1330
|
})
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* `ask` — the guide-routing clarification card. Not a delta: one chunk carries
|
|
1334
|
+
* the whole card, plus the intro sentence that must render as ordinary answer
|
|
1335
|
+
* text IN FRONT of it. The post-MESSAGE_END path is the interesting one — the
|
|
1336
|
+
* card routinely lands in a continuation, where the delta is appended to the
|
|
1337
|
+
* finished bubble rather than replacing it.
|
|
1338
|
+
*/
|
|
1339
|
+
describe('createChatStreamReducer — ask cards', () => {
|
|
1340
|
+
const ask = (question: string, seq?: number, text?: string): ChatStreamEvent => ({
|
|
1341
|
+
type: 'ask',
|
|
1342
|
+
...(text ? { text } : {}),
|
|
1343
|
+
question,
|
|
1344
|
+
options: [
|
|
1345
|
+
{ label: 'Find documentation', description: 'How the feature works' },
|
|
1346
|
+
{ label: 'Work with workspace data' },
|
|
1347
|
+
],
|
|
1348
|
+
...(seq != null ? { seq } : {}),
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
it('renders the intro as text ahead of the card, in one bubble', () => {
|
|
1352
|
+
const r = createChatStreamReducer({ transport: 'nats' })
|
|
1353
|
+
r.apply({ type: 'turn-start', seq: 1 })
|
|
1354
|
+
r.apply(ask('What do you want to work on?', 2, 'Docs, or your own workspace?'))
|
|
1355
|
+
|
|
1356
|
+
const last = r.state.messages[r.state.messages.length - 1]
|
|
1357
|
+
expect(last.segments).toEqual([
|
|
1358
|
+
{ type: 'text', text: 'Docs, or your own workspace?' },
|
|
1359
|
+
{
|
|
1360
|
+
type: 'ask',
|
|
1361
|
+
question: 'What do you want to work on?',
|
|
1362
|
+
options: [
|
|
1363
|
+
{ label: 'Find documentation', description: 'How the feature works' },
|
|
1364
|
+
{ label: 'Work with workspace data' },
|
|
1365
|
+
],
|
|
1366
|
+
},
|
|
1367
|
+
])
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
it('appends into the finished bubble after MESSAGE_END, keeping the reply', () => {
|
|
1371
|
+
const r = createChatStreamReducer({ transport: 'nats' })
|
|
1372
|
+
r.apply({ type: 'turn-start', seq: 1 })
|
|
1373
|
+
r.apply({ type: 'text-delta', text: 'Working on it. ', seq: 2 })
|
|
1374
|
+
r.apply({ type: 'turn-end', seq: 3 })
|
|
1375
|
+
r.apply(ask('Which one?', 4, 'One thing first: '))
|
|
1376
|
+
|
|
1377
|
+
const last = r.state.messages[r.state.messages.length - 1]
|
|
1378
|
+
// The intro COALESCES onto the completed reply (same rule as a post-END
|
|
1379
|
+
// `text-delta`) — a slice off the accumulator used to drop it here.
|
|
1380
|
+
expect(last.segments).toEqual([
|
|
1381
|
+
{ type: 'text', text: 'Working on it. One thing first: ' },
|
|
1382
|
+
{
|
|
1383
|
+
type: 'ask',
|
|
1384
|
+
question: 'Which one?',
|
|
1385
|
+
options: [
|
|
1386
|
+
{ label: 'Find documentation', description: 'How the feature works' },
|
|
1387
|
+
{ label: 'Work with workspace data' },
|
|
1388
|
+
],
|
|
1389
|
+
},
|
|
1390
|
+
])
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
it('keeps two cards in one turn as separate segments (the card pages them)', () => {
|
|
1394
|
+
const r = createChatStreamReducer({ transport: 'nats' })
|
|
1395
|
+
r.apply({ type: 'turn-start', seq: 1 })
|
|
1396
|
+
r.apply(ask('First?', 2))
|
|
1397
|
+
r.apply(ask('Second?', 3))
|
|
1398
|
+
|
|
1399
|
+
const last = r.state.messages[r.state.messages.length - 1]
|
|
1400
|
+
expect(last.segments?.map((s) => s.type)).toEqual(['ask', 'ask'])
|
|
1401
|
+
})
|
|
1402
|
+
})
|
|
@@ -1041,6 +1041,33 @@ export function createChatStreamReducer(
|
|
|
1041
1041
|
break
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
1044
|
+
// A clarification card arrives whole, not as deltas. The intro sentence
|
|
1045
|
+
// riding the same chunk goes in FIRST as ordinary answer text so it runs
|
|
1046
|
+
// through the markdown body pipeline (and coalesces with any preamble the
|
|
1047
|
+
// turn already streamed); the card follows as its own segment. Both land
|
|
1048
|
+
// in one emit, so the bubble never shows the card ahead of its lead-in.
|
|
1049
|
+
case 'ask': {
|
|
1050
|
+
if (event.text) accumulator.appendText(event.text)
|
|
1051
|
+
const segments = accumulator.addAsk(event.question, event.options)
|
|
1052
|
+
if (isInStream || !hasEverStreamed) {
|
|
1053
|
+
emitSegments(segments)
|
|
1054
|
+
applySegmentsToState(segments, withSeqMeta(undefined))
|
|
1055
|
+
break
|
|
1056
|
+
}
|
|
1057
|
+
// Post-MESSAGE_END: the delta is spelled out rather than sliced off the
|
|
1058
|
+
// accumulator. `appendText` COALESCES into a trailing text segment, so a
|
|
1059
|
+
// slice would silently drop the intro whenever the continuation already
|
|
1060
|
+
// had text in flight; `appendToTrailingAssistant` re-coalesces it here
|
|
1061
|
+
// instead, exactly like a `text-delta`.
|
|
1062
|
+
const delta: MessageSegment[] = [
|
|
1063
|
+
...(event.text ? [{ type: 'text' as const, text: event.text }] : []),
|
|
1064
|
+
{ type: 'ask' as const, question: event.question, options: event.options },
|
|
1065
|
+
]
|
|
1066
|
+
emitSegments(delta, { append: true })
|
|
1067
|
+
applySegmentsToState(delta, withSeqMeta({ append: true }))
|
|
1068
|
+
break
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1044
1071
|
case 'tool-execution': {
|
|
1045
1072
|
const segment: ToolExecutionSegment = { type: 'tool_execution', data: event.data }
|
|
1046
1073
|
// A starting tool run means the agent's turn is in progress even
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ComponentType, HTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react'
|
|
6
6
|
import type { ApprovalBlockVariant, AssistantType, AuthorType, ChatApprovalStatus, ConnectionStatus } from './chat.types'
|
|
7
|
-
import type { ApprovalRequestData, Message, MessageSegment, ToolExecutionData,
|
|
7
|
+
import type { ApprovalRequestData, AskSegment, Message, MessageSegment, ToolExecutionData,
|
|
8
8
|
ApprovalResolutionHandler,
|
|
9
9
|
} from './message.types'
|
|
10
10
|
import type { ChatRef } from '../chat-ref.types'
|
|
@@ -212,6 +212,15 @@ export interface ChatMessageEnhancedProps extends Omit<HTMLAttributes<HTMLDivEle
|
|
|
212
212
|
* simply default to undefined on the receiver.
|
|
213
213
|
*/
|
|
214
214
|
NavLinkAnchor?: NavLinkAnchorComponent
|
|
215
|
+
/**
|
|
216
|
+
* Picks an option on an `ask` (clarification) card in this message. Receives
|
|
217
|
+
* the option's label VERBATIM — the host sends it as the user's next message
|
|
218
|
+
* and the backend resolves the reply against the labels it offered.
|
|
219
|
+
*
|
|
220
|
+
* Omit to render ask cards read-only. Keep the identity stable
|
|
221
|
+
* (`useCallback` / module const): the message memo compares it by reference.
|
|
222
|
+
*/
|
|
223
|
+
onAskSelect?: (label: string) => void
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
// ========== Chat Message List Props ==========
|
|
@@ -285,6 +294,10 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
285
294
|
* message's ChatMessageEnhanced. Owns the unified click rule
|
|
286
295
|
* (same-origin soft nav, cross-origin new tab). */
|
|
287
296
|
NavLinkAnchor?: NavLinkAnchorComponent
|
|
297
|
+
/** Ask-card option pick. Forwarded verbatim to every message's
|
|
298
|
+
* ChatMessageEnhanced; the host sends the received label as the user's next
|
|
299
|
+
* message. Omit to render ask cards read-only. */
|
|
300
|
+
onAskSelect?: (label: string) => void
|
|
288
301
|
}
|
|
289
302
|
|
|
290
303
|
export interface ChatMessageListRef {
|
|
@@ -567,6 +580,20 @@ export interface GuideDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
567
580
|
children?: ReactNode
|
|
568
581
|
}
|
|
569
582
|
|
|
583
|
+
// ========== Ask Display Props ==========
|
|
584
|
+
|
|
585
|
+
export interface AskDisplayProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'> {
|
|
586
|
+
/** The clarification cards to show. One entry per `ask` segment; a run of
|
|
587
|
+
* consecutive segments is passed as ONE card with a pager, so a multi-question
|
|
588
|
+
* turn reads as a single block instead of a stack of near-identical cards. */
|
|
589
|
+
cards: AskSegment[]
|
|
590
|
+
/** Picks an option. Receives the option's `label` VERBATIM — that string is
|
|
591
|
+
* sent as the user's next message and the backend's classifier resolves it
|
|
592
|
+
* against the labels it offered, so it must not be reworded. Omit to render
|
|
593
|
+
* the card read-only (replayed history, observer surfaces). */
|
|
594
|
+
onSelect?: (label: string) => void
|
|
595
|
+
}
|
|
596
|
+
|
|
570
597
|
// ========== Model Display Props ==========
|
|
571
598
|
|
|
572
599
|
export interface ModelDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
@@ -11,6 +11,7 @@ export const MESSAGE_TYPE = {
|
|
|
11
11
|
TEXT: 'TEXT',
|
|
12
12
|
THINKING: 'THINKING',
|
|
13
13
|
GUIDE: 'GUIDE',
|
|
14
|
+
ASK: 'ASK',
|
|
14
15
|
EXECUTING_TOOL: 'EXECUTING_TOOL',
|
|
15
16
|
EXECUTED_TOOL: 'EXECUTED_TOOL',
|
|
16
17
|
APPROVAL_REQUEST: 'APPROVAL_REQUEST',
|
|
@@ -190,6 +191,28 @@ export type GuideSegment = {
|
|
|
190
191
|
text: string
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
/** One reading the assistant offers in an `ask` card. `label` is BOTH the row's
|
|
195
|
+
* headline and the exact text sent back when the row is picked — the backend's
|
|
196
|
+
* guide classifier resolves the user's next message against the labels it
|
|
197
|
+
* offered, so the reply must be the label verbatim. `description` is a short
|
|
198
|
+
* clarifying line rendered under it. */
|
|
199
|
+
export type AskOptionData = {
|
|
200
|
+
label: string
|
|
201
|
+
description?: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Clarification card — the assistant asking WHICH reading of an ambiguous
|
|
205
|
+
* question it should answer, rendered as a heading plus a list of clickable
|
|
206
|
+
* options instead of prose bullets. NATS-only (the `ASK` chunk); the intro
|
|
207
|
+
* sentence riding the same chunk becomes an ordinary `text` segment in front
|
|
208
|
+
* of the card, so it goes through the normal markdown body pipeline. Unlike
|
|
209
|
+
* the three delta streams an ask arrives whole — it is never coalesced. */
|
|
210
|
+
export type AskSegment = {
|
|
211
|
+
type: 'ask'
|
|
212
|
+
question: string
|
|
213
|
+
options: AskOptionData[]
|
|
214
|
+
}
|
|
215
|
+
|
|
193
216
|
export type ToolExecutionSegment = {
|
|
194
217
|
type: 'tool_execution'
|
|
195
218
|
data: ToolExecutionData
|
|
@@ -228,7 +251,7 @@ export type ContextCompactionSegment = {
|
|
|
228
251
|
summary?: string
|
|
229
252
|
}
|
|
230
253
|
|
|
231
|
-
export type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
|
|
254
|
+
export type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | AskSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
|
|
232
255
|
|
|
233
256
|
export type MessageContent = string | MessageSegment[]
|
|
234
257
|
|
|
@@ -253,6 +276,16 @@ export interface GuideMessageData extends MessageDataBase {
|
|
|
253
276
|
text?: string
|
|
254
277
|
}
|
|
255
278
|
|
|
279
|
+
/** Persisted `ASK` row (GraphQL `AskData`). `text` is the intro sentence, which
|
|
280
|
+
* history replays as a text segment ahead of the card — same split the live
|
|
281
|
+
* `ASK` chunk carries. */
|
|
282
|
+
export interface AskMessageData extends MessageDataBase {
|
|
283
|
+
type: 'ASK'
|
|
284
|
+
text?: string
|
|
285
|
+
question?: string
|
|
286
|
+
options?: AskOptionData[]
|
|
287
|
+
}
|
|
288
|
+
|
|
256
289
|
export interface ExecutingToolMessageData extends MessageDataBase {
|
|
257
290
|
type: 'EXECUTING_TOOL'
|
|
258
291
|
integratedToolType?: string
|
|
@@ -335,6 +368,7 @@ export type MessageData =
|
|
|
335
368
|
| TextMessageData
|
|
336
369
|
| ThinkingMessageData
|
|
337
370
|
| GuideMessageData
|
|
371
|
+
| AskMessageData
|
|
338
372
|
| ExecutingToolMessageData
|
|
339
373
|
| ExecutedToolMessageData
|
|
340
374
|
| ApprovalRequestMessageData
|
|
@@ -392,3 +392,77 @@ describe('processHistoricalMessages — GUIDE rows', () => {
|
|
|
392
392
|
expect(processHistoricalMessages(empty).messages).toEqual([])
|
|
393
393
|
})
|
|
394
394
|
})
|
|
395
|
+
|
|
396
|
+
// ─── ASK rows ──────────────────────────────────────────────────────────────
|
|
397
|
+
//
|
|
398
|
+
// Same contract as GUIDE above: history must replay a persisted `ASK` row into
|
|
399
|
+
// the SAME shape the live `ASK` chunk produces — the intro sentence as answer
|
|
400
|
+
// text, then the card — or a reloaded dialog loses the clarification the user
|
|
401
|
+
// was about to answer.
|
|
402
|
+
|
|
403
|
+
describe('processHistoricalMessages — ASK rows', () => {
|
|
404
|
+
const askRow = {
|
|
405
|
+
type: 'ASK',
|
|
406
|
+
text: 'Do you want the docs, or your own workspace?',
|
|
407
|
+
question: 'What do you want to work on?',
|
|
408
|
+
options: [
|
|
409
|
+
{ label: 'Find documentation', description: 'How scripting works and how to set it up' },
|
|
410
|
+
{ label: 'Your scripts', description: 'List, edit or run the scripts in your workspace' },
|
|
411
|
+
],
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
it('replays an ASK row as intro text followed by the card', () => {
|
|
415
|
+
const dialog: HistoricalMessage[] = [
|
|
416
|
+
{
|
|
417
|
+
id: 'a1',
|
|
418
|
+
chatType: 'CLIENT_CHAT',
|
|
419
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
420
|
+
owner: { type: 'ASSISTANT' },
|
|
421
|
+
messageData: [askRow as any],
|
|
422
|
+
},
|
|
423
|
+
]
|
|
424
|
+
expect(processHistoricalMessages(dialog).messages[0].content).toEqual([
|
|
425
|
+
{ type: 'text', text: 'Do you want the docs, or your own workspace?' },
|
|
426
|
+
{
|
|
427
|
+
type: 'ask',
|
|
428
|
+
question: 'What do you want to work on?',
|
|
429
|
+
options: [
|
|
430
|
+
{ label: 'Find documentation', description: 'How scripting works and how to set it up' },
|
|
431
|
+
{ label: 'Your scripts', description: 'List, edit or run the scripts in your workspace' },
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
])
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('keeps consecutive ASK rows as SEPARATE segments (the card pages them)', () => {
|
|
438
|
+
const dialog: HistoricalMessage[] = [
|
|
439
|
+
{
|
|
440
|
+
id: 'a1',
|
|
441
|
+
chatType: 'CLIENT_CHAT',
|
|
442
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
443
|
+
owner: { type: 'ASSISTANT' },
|
|
444
|
+
messageData: [
|
|
445
|
+
{ type: 'ASK', question: 'First?', options: [{ label: 'A' }] } as any,
|
|
446
|
+
{ type: 'ASK', question: 'Second?', options: [{ label: 'B' }] } as any,
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
]
|
|
450
|
+
expect(processHistoricalMessages(dialog).messages[0].content).toEqual([
|
|
451
|
+
{ type: 'ask', question: 'First?', options: [{ label: 'A', description: undefined }] },
|
|
452
|
+
{ type: 'ask', question: 'Second?', options: [{ label: 'B', description: undefined }] },
|
|
453
|
+
])
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('an ASK row with no answerable options decodes to nothing', () => {
|
|
457
|
+
const dialog: HistoricalMessage[] = [
|
|
458
|
+
{
|
|
459
|
+
id: 'a1',
|
|
460
|
+
chatType: 'CLIENT_CHAT',
|
|
461
|
+
createdAt: '2026-07-19T09:00:05Z',
|
|
462
|
+
owner: { type: 'ASSISTANT' },
|
|
463
|
+
messageData: [{ type: 'ASK', question: 'Which one?', options: [{ description: 'no label' }] } as any],
|
|
464
|
+
},
|
|
465
|
+
]
|
|
466
|
+
expect(processHistoricalMessages(dialog).messages).toEqual([])
|
|
467
|
+
})
|
|
468
|
+
})
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
|
+
AskOptionData,
|
|
12
13
|
MessageSegment,
|
|
13
14
|
ToolExecutionSegment,
|
|
14
15
|
ApprovalRequestSegment,
|
|
@@ -163,6 +164,17 @@ export class MessageSegmentAccumulator {
|
|
|
163
164
|
return this.getSegments()
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Add a clarification (ask) card. Unlike the three delta streams an ask
|
|
169
|
+
* arrives whole in one chunk, so it is always a NEW segment — never merged
|
|
170
|
+
* into a trailing one. Consecutive cards stay separate segments; the renderer
|
|
171
|
+
* is what pages through a run of them.
|
|
172
|
+
*/
|
|
173
|
+
addAsk(question: string, options: AskOptionData[]): MessageSegment[] {
|
|
174
|
+
this.segments.push({ type: 'ask', question, options })
|
|
175
|
+
return this.getSegments()
|
|
176
|
+
}
|
|
177
|
+
|
|
166
178
|
/**
|
|
167
179
|
* Add a tool execution segment.
|
|
168
180
|
*
|
|
@@ -530,6 +542,9 @@ export class MessageSegmentAccumulator {
|
|
|
530
542
|
case 'guide':
|
|
531
543
|
if (segment.text) this.appendGuide(segment.text)
|
|
532
544
|
break
|
|
545
|
+
case 'ask':
|
|
546
|
+
this.addAsk(segment.question, segment.options)
|
|
547
|
+
break
|
|
533
548
|
case 'tool_execution':
|
|
534
549
|
this.addToolExecution(segment)
|
|
535
550
|
break
|
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
type MessageOwner,
|
|
39
39
|
} from '../types'
|
|
40
40
|
import type { ChatStreamEvent } from '../../../chat-protocol/events'
|
|
41
|
+
// One normalizer for ask rows, shared with the live decoder — history and the
|
|
42
|
+
// stream must agree on which options are usable.
|
|
43
|
+
import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder'
|
|
41
44
|
import { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'
|
|
42
45
|
import { getCommandText } from './tool-call-helpers'
|
|
43
46
|
|
|
@@ -117,6 +120,22 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
|
|
|
117
120
|
}
|
|
118
121
|
return null
|
|
119
122
|
|
|
123
|
+
// Same completeness gate as the live decoder (`decodeNatsChunk`): a
|
|
124
|
+
// persisted row without a question or without options is not a card the
|
|
125
|
+
// user can answer, so it replays as nothing rather than as empty chrome.
|
|
126
|
+
case MESSAGE_TYPE.ASK: {
|
|
127
|
+
if (!('question' in data)) return null
|
|
128
|
+
const question = typeof data.question === 'string' ? data.question.trim() : ''
|
|
129
|
+
const options = normalizeAskOptions(data.options)
|
|
130
|
+
if (!question || options.length === 0) return null
|
|
131
|
+
return {
|
|
132
|
+
type: 'ask',
|
|
133
|
+
...(data.text ? { text: data.text } : {}),
|
|
134
|
+
question,
|
|
135
|
+
options,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
case MESSAGE_TYPE.EXECUTING_TOOL:
|
|
121
140
|
if ('integratedToolType' in data) {
|
|
122
141
|
return {
|
|
@@ -252,6 +271,13 @@ function applyHistoryEvent(
|
|
|
252
271
|
accumulator.appendGuide(event.text)
|
|
253
272
|
break
|
|
254
273
|
|
|
274
|
+
// Mirror of the live path: the intro sentence replays as answer text in
|
|
275
|
+
// front of the card, so a reloaded thread reads exactly like the stream did.
|
|
276
|
+
case 'ask':
|
|
277
|
+
if (event.text) accumulator.appendText(event.text)
|
|
278
|
+
accumulator.addAsk(event.question, event.options)
|
|
279
|
+
break
|
|
280
|
+
|
|
255
281
|
case 'tool-execution':
|
|
256
282
|
accumulator.addToolExecution({ type: 'tool_execution', data: event.data })
|
|
257
283
|
break
|