@runtypelabs/persona 3.9.2 → 3.10.1
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/index.cjs +45 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.global.js +67 -64
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +45 -42
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +959 -214
- package/dist/theme-editor.d.cts +157 -3
- package/dist/theme-editor.d.ts +157 -3
- package/dist/theme-editor.js +955 -214
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +8 -0
- package/dist/theme-reference.d.ts +8 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +154 -0
- package/package.json +1 -1
- package/src/client.test.ts +312 -1
- package/src/client.ts +247 -24
- package/src/components/messages.ts +1 -1
- package/src/components/reasoning-bubble.ts +117 -28
- package/src/components/tool-bubble.ts +161 -27
- package/src/defaults.ts +12 -0
- package/src/styles/widget.css +154 -0
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/preview-utils.test.ts +58 -0
- package/src/theme-editor/preview-utils.ts +220 -4
- package/src/theme-editor/sections.test.ts +20 -0
- package/src/theme-editor/sections.ts +10 -0
- package/src/theme-reference.ts +8 -3
- package/src/tool-call-display-defaults.test.ts +23 -0
- package/src/types.ts +155 -0
- package/src/ui.attachments-drop.test.ts +188 -0
- package/src/ui.scroll.test.ts +150 -0
- package/src/ui.tool-display.test.ts +204 -0
- package/src/ui.ts +275 -7
- package/src/utils/message-fingerprint.test.ts +17 -0
- package/src/utils/message-fingerprint.ts +13 -1
package/src/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AgentWidgetMessage,
|
|
4
4
|
AgentWidgetEvent,
|
|
5
5
|
AgentWidgetStreamParser,
|
|
6
|
+
AgentWidgetStreamParserResult,
|
|
6
7
|
AgentWidgetContextProvider,
|
|
7
8
|
AgentWidgetRequestMiddleware,
|
|
8
9
|
AgentWidgetRequestPayload,
|
|
@@ -85,6 +86,40 @@ function getParserFromType(parserType?: "plain" | "json" | "regex-json" | "xml")
|
|
|
85
86
|
|
|
86
87
|
export type SSEEventCallback = (eventType: string, payload: unknown) => void;
|
|
87
88
|
|
|
89
|
+
const looksStructured = (value: string) =>
|
|
90
|
+
value.startsWith("{") || value.startsWith("[") || value.startsWith("<");
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Choose the best content source for sealed-segment reconciliation.
|
|
94
|
+
* Prefers the final structured payload from step_complete when the raw
|
|
95
|
+
* buffer is only a partial/unparseable prefix of the same structured format.
|
|
96
|
+
*/
|
|
97
|
+
export function preferFinalStructuredContent(
|
|
98
|
+
rawBuffer: string | undefined,
|
|
99
|
+
finalString: string
|
|
100
|
+
): string {
|
|
101
|
+
if (!rawBuffer) return finalString;
|
|
102
|
+
|
|
103
|
+
const rawTrimmed = rawBuffer.trim();
|
|
104
|
+
const finalTrimmed = finalString.trim();
|
|
105
|
+
if (rawTrimmed.length === 0) return finalString;
|
|
106
|
+
if (finalTrimmed.length === 0) return rawBuffer;
|
|
107
|
+
|
|
108
|
+
const rawLooksStructured = looksStructured(rawTrimmed);
|
|
109
|
+
const finalLooksStructured = looksStructured(finalTrimmed);
|
|
110
|
+
|
|
111
|
+
if (!finalLooksStructured) return rawBuffer;
|
|
112
|
+
if (!rawLooksStructured) return finalString;
|
|
113
|
+
if (finalTrimmed === rawTrimmed) return finalString;
|
|
114
|
+
if (finalTrimmed.startsWith(rawTrimmed)) return finalString;
|
|
115
|
+
|
|
116
|
+
const rawJsonText = extractTextFromJson(rawBuffer);
|
|
117
|
+
const finalJsonText = extractTextFromJson(finalString);
|
|
118
|
+
if (finalJsonText !== null && rawJsonText === null) return finalString;
|
|
119
|
+
|
|
120
|
+
return rawBuffer;
|
|
121
|
+
}
|
|
122
|
+
|
|
88
123
|
export class AgentWidgetClient {
|
|
89
124
|
private readonly apiUrl: string;
|
|
90
125
|
private readonly headers: Record<string, string>;
|
|
@@ -1260,6 +1295,136 @@ export class AgentWidgetClient {
|
|
|
1260
1295
|
const streamParsers = new Map<string, AgentWidgetStreamParser>();
|
|
1261
1296
|
// Track accumulated raw content for structured formats (JSON, XML, etc.)
|
|
1262
1297
|
const rawContentBuffers = new Map<string, string>();
|
|
1298
|
+
// Reorder buffer for out-of-order step_delta chunks (keyed by assistant message id)
|
|
1299
|
+
const seqChunkBuffers = new Map<string, Array<{ seq: number; text: string }>>();
|
|
1300
|
+
// Reorder buffer for out-of-order reason_delta chunks (keyed by reasoning id)
|
|
1301
|
+
const reasonSeqBuffers = new Map<string, Array<{ seq: number; text: string }>>();
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Insert a chunk into a sequence-ordered buffer and return
|
|
1305
|
+
* the full accumulated text in correct sequence order.
|
|
1306
|
+
*/
|
|
1307
|
+
function insertSeqChunk(
|
|
1308
|
+
bufferMap: Map<string, Array<{ seq: number; text: string }>>,
|
|
1309
|
+
key: string,
|
|
1310
|
+
seq: number,
|
|
1311
|
+
text: string
|
|
1312
|
+
): string {
|
|
1313
|
+
let buf = bufferMap.get(key);
|
|
1314
|
+
if (!buf) {
|
|
1315
|
+
buf = [];
|
|
1316
|
+
bufferMap.set(key, buf);
|
|
1317
|
+
}
|
|
1318
|
+
// Binary-search insert to keep sorted by seq
|
|
1319
|
+
let lo = 0, hi = buf.length;
|
|
1320
|
+
while (lo < hi) {
|
|
1321
|
+
const mid = (lo + hi) >>> 1;
|
|
1322
|
+
if (buf[mid].seq < seq) lo = mid + 1;
|
|
1323
|
+
else hi = mid;
|
|
1324
|
+
}
|
|
1325
|
+
buf.splice(lo, 0, { seq, text });
|
|
1326
|
+
// Join all chunks in seq order
|
|
1327
|
+
let result = "";
|
|
1328
|
+
for (let i = 0; i < buf.length; i++) result += buf[i].text;
|
|
1329
|
+
return result;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Only the most-recently sealed segment is reconciled with step_complete's
|
|
1333
|
+
// final response. Earlier segments rely on their own async parser microtasks
|
|
1334
|
+
// resolving via the closure-captured `assistant` variable.
|
|
1335
|
+
let lastSealedTextSegment: AgentWidgetMessage | null = null;
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* After text_end + didSplitByPartId, merge the authoritative final response into the
|
|
1339
|
+
* sealed message when streaming left content short (e.g. async parser lag).
|
|
1340
|
+
*/
|
|
1341
|
+
const reconcileSealedAssistantWithFinalResponse = (
|
|
1342
|
+
msg: AgentWidgetMessage,
|
|
1343
|
+
finalContent: unknown
|
|
1344
|
+
) => {
|
|
1345
|
+
const finalString = ensureStringContent(finalContent);
|
|
1346
|
+
const rawBuffer = rawContentBuffers.get(msg.id);
|
|
1347
|
+
const contentToProcess = preferFinalStructuredContent(rawBuffer, finalString);
|
|
1348
|
+
msg.rawContent = contentToProcess;
|
|
1349
|
+
const parser = streamParsers.get(msg.id);
|
|
1350
|
+
|
|
1351
|
+
const mergeIfBetter = (mergedDisplay: string) => {
|
|
1352
|
+
const cur = msg.content ?? "";
|
|
1353
|
+
if (mergedDisplay.trim() === "") return;
|
|
1354
|
+
// Only replace when empty, or when the stream left a strict prefix of the
|
|
1355
|
+
// authoritative final (truncation). Do not use length alone — multi-segment
|
|
1356
|
+
// flows can have a short last bubble whose content is not a prefix of the
|
|
1357
|
+
// full step response.
|
|
1358
|
+
if (
|
|
1359
|
+
cur.trim().length === 0 ||
|
|
1360
|
+
mergedDisplay.startsWith(cur) ||
|
|
1361
|
+
mergedDisplay.trimStart().startsWith(cur.trim())
|
|
1362
|
+
) {
|
|
1363
|
+
msg.content = mergedDisplay;
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const finalizeCleanup = () => {
|
|
1368
|
+
if (parser) {
|
|
1369
|
+
const closeResult = parser.close?.();
|
|
1370
|
+
if (closeResult instanceof Promise) closeResult.catch(() => {});
|
|
1371
|
+
}
|
|
1372
|
+
streamParsers.delete(msg.id);
|
|
1373
|
+
rawContentBuffers.delete(msg.id);
|
|
1374
|
+
msg.streaming = false;
|
|
1375
|
+
emitMessage(msg);
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
if (!parser) {
|
|
1379
|
+
mergeIfBetter(finalString);
|
|
1380
|
+
finalizeCleanup();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Prefer JSON fast path when the final payload is JSON-shaped
|
|
1385
|
+
const extractedFromJson = extractTextFromJson(contentToProcess);
|
|
1386
|
+
if (extractedFromJson !== null && extractedFromJson.trim() !== "") {
|
|
1387
|
+
mergeIfBetter(extractedFromJson);
|
|
1388
|
+
finalizeCleanup();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const bestDisplayText = (
|
|
1393
|
+
result: AgentWidgetStreamParserResult | string | null
|
|
1394
|
+
): string => {
|
|
1395
|
+
const text =
|
|
1396
|
+
typeof result === "string" ? result : result?.text ?? null;
|
|
1397
|
+
if (text !== null && text.trim() !== "") return text;
|
|
1398
|
+
const extracted = parser.getExtractedText();
|
|
1399
|
+
if (extracted !== null && extracted.trim() !== "") return extracted;
|
|
1400
|
+
return finalString;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
let parsedResult: ReturnType<typeof parser.processChunk>;
|
|
1404
|
+
try {
|
|
1405
|
+
parsedResult = parser.processChunk(contentToProcess);
|
|
1406
|
+
} catch {
|
|
1407
|
+
mergeIfBetter(finalString);
|
|
1408
|
+
finalizeCleanup();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (parsedResult instanceof Promise) {
|
|
1413
|
+
parsedResult
|
|
1414
|
+
.then((result) => {
|
|
1415
|
+
mergeIfBetter(bestDisplayText(result));
|
|
1416
|
+
finalizeCleanup();
|
|
1417
|
+
})
|
|
1418
|
+
.catch(() => {
|
|
1419
|
+
mergeIfBetter(finalString);
|
|
1420
|
+
finalizeCleanup();
|
|
1421
|
+
});
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
mergeIfBetter(bestDisplayText(parsedResult));
|
|
1426
|
+
finalizeCleanup();
|
|
1427
|
+
};
|
|
1263
1428
|
|
|
1264
1429
|
// Agent execution state tracking
|
|
1265
1430
|
let agentExecution: AgentExecutionState | null = null;
|
|
@@ -1366,7 +1531,16 @@ export class AgentWidgetClient {
|
|
|
1366
1531
|
payload.delta ??
|
|
1367
1532
|
"";
|
|
1368
1533
|
if (chunk && payload.hidden !== true) {
|
|
1369
|
-
|
|
1534
|
+
const reasonSeq = typeof payload.sequenceIndex === 'number' ? payload.sequenceIndex : undefined;
|
|
1535
|
+
if (reasonSeq !== undefined) {
|
|
1536
|
+
// Use reorder buffer so out-of-order chunks are assembled correctly
|
|
1537
|
+
const ordered = insertSeqChunk(reasonSeqBuffers, reasoningId, reasonSeq, String(chunk));
|
|
1538
|
+
// Rebuild the chunks array from the ordered concatenation.
|
|
1539
|
+
// We store a single joined string to avoid duplicated / mis-ordered entries.
|
|
1540
|
+
reasoningMessage.reasoning.chunks = [ordered];
|
|
1541
|
+
} else {
|
|
1542
|
+
reasoningMessage.reasoning.chunks.push(String(chunk));
|
|
1543
|
+
}
|
|
1370
1544
|
}
|
|
1371
1545
|
reasoningMessage.reasoning.status = payload.done ? "complete" : "streaming";
|
|
1372
1546
|
if (payload.done) {
|
|
@@ -1378,6 +1552,7 @@ export class AgentWidgetClient {
|
|
|
1378
1552
|
0,
|
|
1379
1553
|
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
1380
1554
|
);
|
|
1555
|
+
reasonSeqBuffers.delete(reasoningId);
|
|
1381
1556
|
}
|
|
1382
1557
|
reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
|
|
1383
1558
|
emitMessage(reasoningMessage);
|
|
@@ -1398,6 +1573,7 @@ export class AgentWidgetClient {
|
|
|
1398
1573
|
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
1399
1574
|
);
|
|
1400
1575
|
reasoningMessage.streaming = false;
|
|
1576
|
+
reasonSeqBuffers.delete(reasoningId);
|
|
1401
1577
|
emitMessage(reasoningMessage);
|
|
1402
1578
|
}
|
|
1403
1579
|
const stepKey = getStepKey(payload);
|
|
@@ -1527,6 +1703,7 @@ export class AgentWidgetClient {
|
|
|
1527
1703
|
if (prev) {
|
|
1528
1704
|
prev.streaming = false;
|
|
1529
1705
|
emitMessage(prev);
|
|
1706
|
+
lastSealedTextSegment = prev;
|
|
1530
1707
|
assistantMessage = null;
|
|
1531
1708
|
didSplitByPartId = true;
|
|
1532
1709
|
}
|
|
@@ -1541,6 +1718,7 @@ export class AgentWidgetClient {
|
|
|
1541
1718
|
if (prev) {
|
|
1542
1719
|
prev.streaming = false;
|
|
1543
1720
|
emitMessage(prev);
|
|
1721
|
+
lastSealedTextSegment = prev;
|
|
1544
1722
|
assistantMessage = null;
|
|
1545
1723
|
didSplitByPartId = true;
|
|
1546
1724
|
}
|
|
@@ -1561,6 +1739,7 @@ export class AgentWidgetClient {
|
|
|
1561
1739
|
if (prev) {
|
|
1562
1740
|
prev.streaming = false;
|
|
1563
1741
|
emitMessage(prev);
|
|
1742
|
+
lastSealedTextSegment = prev;
|
|
1564
1743
|
assistantMessage = null;
|
|
1565
1744
|
didSplitByPartId = true;
|
|
1566
1745
|
}
|
|
@@ -1576,9 +1755,19 @@ export class AgentWidgetClient {
|
|
|
1576
1755
|
// Support various field names: text, delta, content, chunk (Runtype uses 'chunk')
|
|
1577
1756
|
const chunk = payload.text ?? payload.delta ?? payload.content ?? payload.chunk ?? "";
|
|
1578
1757
|
if (chunk) {
|
|
1579
|
-
//
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1758
|
+
// Check if the event carries a sequence number for reordering
|
|
1759
|
+
const chunkSeq = typeof payload.seq === 'number' ? payload.seq : undefined;
|
|
1760
|
+
|
|
1761
|
+
// Accumulate raw content for structured format parsing.
|
|
1762
|
+
// When seq is present, use the reorder buffer so out-of-order
|
|
1763
|
+
// chunks are assembled in the correct server-intended order.
|
|
1764
|
+
let accumulatedRaw: string;
|
|
1765
|
+
if (chunkSeq !== undefined) {
|
|
1766
|
+
accumulatedRaw = insertSeqChunk(seqChunkBuffers, assistant.id, chunkSeq, chunk);
|
|
1767
|
+
} else {
|
|
1768
|
+
const rawBuffer = rawContentBuffers.get(assistant.id) ?? "";
|
|
1769
|
+
accumulatedRaw = rawBuffer + chunk;
|
|
1770
|
+
}
|
|
1582
1771
|
// Store raw content for action parsing, but NEVER set assistant.content to raw JSON
|
|
1583
1772
|
assistant.rawContent = accumulatedRaw;
|
|
1584
1773
|
|
|
@@ -1601,7 +1790,13 @@ export class AgentWidgetClient {
|
|
|
1601
1790
|
|
|
1602
1791
|
// If plain text parser, just append the chunk directly
|
|
1603
1792
|
if (isPlainTextParser) {
|
|
1604
|
-
|
|
1793
|
+
// When seq-ordered, rebuild from the reorder buffer;
|
|
1794
|
+
// otherwise fall back to simple append.
|
|
1795
|
+
if (chunkSeq !== undefined) {
|
|
1796
|
+
assistant.content = accumulatedRaw;
|
|
1797
|
+
} else {
|
|
1798
|
+
assistant.content += chunk;
|
|
1799
|
+
}
|
|
1605
1800
|
// Clear any raw buffer/parser since we're in plain text mode
|
|
1606
1801
|
rawContentBuffers.delete(assistant.id);
|
|
1607
1802
|
streamParsers.delete(assistant.id);
|
|
@@ -1620,18 +1815,19 @@ export class AgentWidgetClient {
|
|
|
1620
1815
|
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
1621
1816
|
|
|
1622
1817
|
if (text !== null && text.trim() !== "") {
|
|
1623
|
-
// Parser successfully extracted text
|
|
1624
|
-
//
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
currentAssistant.content = text;
|
|
1628
|
-
emitMessage(currentAssistant);
|
|
1629
|
-
}
|
|
1818
|
+
// Parser successfully extracted text — update the chunk's assistant
|
|
1819
|
+
// (not assistantMessage; text_end may have cleared that ref before microtasks run)
|
|
1820
|
+
assistant.content = text;
|
|
1821
|
+
emitMessage(assistant);
|
|
1630
1822
|
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
1631
1823
|
// Not a structured format - show as plain text
|
|
1632
1824
|
const currentAssistant = assistantMessage;
|
|
1633
1825
|
if (currentAssistant && currentAssistant.id === assistant.id) {
|
|
1634
|
-
|
|
1826
|
+
if (chunkSeq !== undefined) {
|
|
1827
|
+
currentAssistant.content = accumulatedRaw;
|
|
1828
|
+
} else {
|
|
1829
|
+
currentAssistant.content += chunk;
|
|
1830
|
+
}
|
|
1635
1831
|
rawContentBuffers.delete(currentAssistant.id);
|
|
1636
1832
|
streamParsers.delete(currentAssistant.id);
|
|
1637
1833
|
currentAssistant.rawContent = undefined;
|
|
@@ -1642,7 +1838,11 @@ export class AgentWidgetClient {
|
|
|
1642
1838
|
// Don't emit message if parser hasn't extracted text yet
|
|
1643
1839
|
}).catch(() => {
|
|
1644
1840
|
// On error, treat as plain text
|
|
1645
|
-
|
|
1841
|
+
if (chunkSeq !== undefined) {
|
|
1842
|
+
assistant.content = accumulatedRaw;
|
|
1843
|
+
} else {
|
|
1844
|
+
assistant.content += chunk;
|
|
1845
|
+
}
|
|
1646
1846
|
rawContentBuffers.delete(assistant.id);
|
|
1647
1847
|
streamParsers.delete(assistant.id);
|
|
1648
1848
|
assistant.rawContent = undefined;
|
|
@@ -1660,7 +1860,11 @@ export class AgentWidgetClient {
|
|
|
1660
1860
|
emitMessage(assistant);
|
|
1661
1861
|
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
1662
1862
|
// Not a structured format - show as plain text
|
|
1663
|
-
|
|
1863
|
+
if (chunkSeq !== undefined) {
|
|
1864
|
+
assistant.content = accumulatedRaw;
|
|
1865
|
+
} else {
|
|
1866
|
+
assistant.content += chunk;
|
|
1867
|
+
}
|
|
1664
1868
|
// Clear any raw buffer/parser if we were in structured format mode
|
|
1665
1869
|
rawContentBuffers.delete(assistant.id);
|
|
1666
1870
|
streamParsers.delete(assistant.id);
|
|
@@ -1713,6 +1917,7 @@ export class AgentWidgetClient {
|
|
|
1713
1917
|
// Clean up
|
|
1714
1918
|
streamParsers.delete(currentAssistant.id);
|
|
1715
1919
|
rawContentBuffers.delete(currentAssistant.id);
|
|
1920
|
+
seqChunkBuffers.delete(currentAssistant.id);
|
|
1716
1921
|
emitMessage(currentAssistant);
|
|
1717
1922
|
}
|
|
1718
1923
|
}
|
|
@@ -1744,6 +1949,7 @@ export class AgentWidgetClient {
|
|
|
1744
1949
|
streamParsers.delete(assistant.id);
|
|
1745
1950
|
}
|
|
1746
1951
|
rawContentBuffers.delete(assistant.id);
|
|
1952
|
+
seqChunkBuffers.delete(assistant.id);
|
|
1747
1953
|
assistant.streaming = false;
|
|
1748
1954
|
emitMessage(assistant);
|
|
1749
1955
|
}
|
|
@@ -1758,17 +1964,29 @@ export class AgentWidgetClient {
|
|
|
1758
1964
|
continue;
|
|
1759
1965
|
}
|
|
1760
1966
|
if (didSplitByPartId) {
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1967
|
+
// Sealed segment(s) — do not create a second bubble from step_complete.
|
|
1968
|
+
// Merge authoritative final response into the last sealed segment (fixes async lag).
|
|
1763
1969
|
if (assistantMessage !== null) {
|
|
1764
1970
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
1765
1971
|
streamParsers.delete(msg.id);
|
|
1766
1972
|
rawContentBuffers.delete(msg.id);
|
|
1973
|
+
seqChunkBuffers.delete(msg.id);
|
|
1767
1974
|
if (msg.streaming !== false) {
|
|
1768
1975
|
msg.streaming = false;
|
|
1769
1976
|
emitMessage(msg);
|
|
1770
1977
|
}
|
|
1771
1978
|
}
|
|
1979
|
+
const splitFinalContent = payload.result?.response;
|
|
1980
|
+
const sealedForReconcile = lastSealedTextSegment;
|
|
1981
|
+
if (sealedForReconcile) {
|
|
1982
|
+
if (splitFinalContent !== undefined && splitFinalContent !== null) {
|
|
1983
|
+
reconcileSealedAssistantWithFinalResponse(sealedForReconcile, splitFinalContent);
|
|
1984
|
+
} else {
|
|
1985
|
+
streamParsers.delete(sealedForReconcile.id);
|
|
1986
|
+
rawContentBuffers.delete(sealedForReconcile.id);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
lastSealedTextSegment = null;
|
|
1772
1990
|
continue;
|
|
1773
1991
|
}
|
|
1774
1992
|
const finalContent = payload.result?.response;
|
|
@@ -1817,6 +2035,7 @@ export class AgentWidgetClient {
|
|
|
1817
2035
|
// Clean up
|
|
1818
2036
|
streamParsers.delete(currentAssistant.id);
|
|
1819
2037
|
rawContentBuffers.delete(currentAssistant.id);
|
|
2038
|
+
seqChunkBuffers.delete(currentAssistant.id);
|
|
1820
2039
|
emitMessage(currentAssistant);
|
|
1821
2040
|
}
|
|
1822
2041
|
} else {
|
|
@@ -1834,6 +2053,7 @@ export class AgentWidgetClient {
|
|
|
1834
2053
|
// Clean up
|
|
1835
2054
|
streamParsers.delete(currentAssistant.id);
|
|
1836
2055
|
rawContentBuffers.delete(currentAssistant.id);
|
|
2056
|
+
seqChunkBuffers.delete(currentAssistant.id);
|
|
1837
2057
|
emitMessage(currentAssistant);
|
|
1838
2058
|
}
|
|
1839
2059
|
}
|
|
@@ -1881,6 +2101,7 @@ export class AgentWidgetClient {
|
|
|
1881
2101
|
}
|
|
1882
2102
|
streamParsers.delete(assistant.id);
|
|
1883
2103
|
rawContentBuffers.delete(assistant.id);
|
|
2104
|
+
seqChunkBuffers.delete(assistant.id);
|
|
1884
2105
|
assistant.streaming = false;
|
|
1885
2106
|
emitMessage(assistant);
|
|
1886
2107
|
}
|
|
@@ -1888,6 +2109,7 @@ export class AgentWidgetClient {
|
|
|
1888
2109
|
// No final content, just mark as complete and clean up
|
|
1889
2110
|
streamParsers.delete(assistant.id);
|
|
1890
2111
|
rawContentBuffers.delete(assistant.id);
|
|
2112
|
+
seqChunkBuffers.delete(assistant.id);
|
|
1891
2113
|
assistant.streaming = false;
|
|
1892
2114
|
emitMessage(assistant);
|
|
1893
2115
|
}
|
|
@@ -1900,6 +2122,7 @@ export class AgentWidgetClient {
|
|
|
1900
2122
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
1901
2123
|
streamParsers.delete(msg.id);
|
|
1902
2124
|
rawContentBuffers.delete(msg.id);
|
|
2125
|
+
seqChunkBuffers.delete(msg.id);
|
|
1903
2126
|
if (msg.streaming !== false) {
|
|
1904
2127
|
msg.streaming = false;
|
|
1905
2128
|
emitMessage(msg);
|
|
@@ -1926,12 +2149,9 @@ export class AgentWidgetClient {
|
|
|
1926
2149
|
// Extract text from result (could be string or object)
|
|
1927
2150
|
const text = typeof result === 'string' ? result : result?.text ?? null;
|
|
1928
2151
|
if (text !== null) {
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
currentAssistant.streaming = false;
|
|
1933
|
-
emitMessage(currentAssistant);
|
|
1934
|
-
}
|
|
2152
|
+
assistant.content = text;
|
|
2153
|
+
assistant.streaming = false;
|
|
2154
|
+
emitMessage(assistant);
|
|
1935
2155
|
}
|
|
1936
2156
|
});
|
|
1937
2157
|
}
|
|
@@ -1944,7 +2164,8 @@ export class AgentWidgetClient {
|
|
|
1944
2164
|
// Clean up parser and buffer
|
|
1945
2165
|
streamParsers.delete(assistant.id);
|
|
1946
2166
|
rawContentBuffers.delete(assistant.id);
|
|
1947
|
-
|
|
2167
|
+
seqChunkBuffers.delete(assistant.id);
|
|
2168
|
+
|
|
1948
2169
|
// Only emit if something actually changed to avoid flicker
|
|
1949
2170
|
const contentChanged = displayContent !== assistant.content;
|
|
1950
2171
|
const streamingChanged = assistant.streaming !== false;
|
|
@@ -1966,6 +2187,7 @@ export class AgentWidgetClient {
|
|
|
1966
2187
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
1967
2188
|
streamParsers.delete(msg.id);
|
|
1968
2189
|
rawContentBuffers.delete(msg.id);
|
|
2190
|
+
seqChunkBuffers.delete(msg.id);
|
|
1969
2191
|
|
|
1970
2192
|
// Only emit if streaming state changed
|
|
1971
2193
|
if (msg.streaming !== false) {
|
|
@@ -2369,6 +2591,7 @@ export class AgentWidgetClient {
|
|
|
2369
2591
|
assistantMessageRef.current = null;
|
|
2370
2592
|
streamParsers.delete(id);
|
|
2371
2593
|
rawContentBuffers.delete(id);
|
|
2594
|
+
seqChunkBuffers.delete(id);
|
|
2372
2595
|
} else if (payloadType === "error" && payload.error) {
|
|
2373
2596
|
onEvent({
|
|
2374
2597
|
type: "error",
|
|
@@ -21,7 +21,7 @@ export const renderMessages = (
|
|
|
21
21
|
let bubble: HTMLElement;
|
|
22
22
|
if (message.variant === "reasoning" && message.reasoning) {
|
|
23
23
|
if (!showReasoning) return;
|
|
24
|
-
bubble = createReasoningBubble(message);
|
|
24
|
+
bubble = createReasoningBubble(message, config);
|
|
25
25
|
} else if (message.variant === "tool" && message.toolCall) {
|
|
26
26
|
if (!showToolCalls) return;
|
|
27
27
|
bubble = createToolBubble(message, config);
|
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import { AgentWidgetMessage } from "../types";
|
|
2
|
+
import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
3
3
|
import { describeReasonStatus } from "../utils/formatting";
|
|
4
4
|
import { renderLucideIcon } from "../utils/icons";
|
|
5
5
|
|
|
6
6
|
// Expansion state per widget instance
|
|
7
7
|
export const reasoningExpansionState = new Set<string>();
|
|
8
8
|
|
|
9
|
+
const appendRenderedValue = (
|
|
10
|
+
container: HTMLElement,
|
|
11
|
+
value: HTMLElement | string | null | undefined
|
|
12
|
+
): boolean => {
|
|
13
|
+
if (value == null) return false;
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
container.textContent = value;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
container.appendChild(value);
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getReasoningPreviewText = (message: AgentWidgetMessage, maxLines: number): string => {
|
|
23
|
+
const text = message.reasoning?.chunks.join("").trim() ?? "";
|
|
24
|
+
if (!text) return "";
|
|
25
|
+
return text
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.slice(0, maxLines)
|
|
30
|
+
.join("\n");
|
|
31
|
+
};
|
|
32
|
+
|
|
9
33
|
// Helper function to update reasoning bubble UI after expansion state changes
|
|
10
34
|
export const updateReasoningBubbleUI = (messageId: string, bubble: HTMLElement): void => {
|
|
11
35
|
const expanded = reasoningExpansionState.has(messageId);
|
|
12
36
|
const header = bubble.querySelector('button[data-expand-header="true"]') as HTMLElement;
|
|
13
37
|
const content = bubble.querySelector('.persona-border-t') as HTMLElement;
|
|
38
|
+
const preview = bubble.querySelector('[data-persona-collapsed-preview="reasoning"]') as HTMLElement | null;
|
|
14
39
|
|
|
15
40
|
if (!header || !content) return;
|
|
16
41
|
|
|
@@ -31,9 +56,14 @@ export const updateReasoningBubbleUI = (messageId: string, bubble: HTMLElement):
|
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
content.style.display = expanded ? "" : "none";
|
|
59
|
+
if (preview) {
|
|
60
|
+
preview.style.display = expanded
|
|
61
|
+
? "none"
|
|
62
|
+
: ((preview.textContent || preview.childNodes.length) ? "" : "none");
|
|
63
|
+
}
|
|
34
64
|
};
|
|
35
65
|
|
|
36
|
-
export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement => {
|
|
66
|
+
export const createReasoningBubble = (message: AgentWidgetMessage, config?: AgentWidgetConfig): HTMLElement => {
|
|
37
67
|
const reasoning = message.reasoning;
|
|
38
68
|
const bubble = createElement(
|
|
39
69
|
"div",
|
|
@@ -61,20 +91,44 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
|
|
|
61
91
|
return bubble;
|
|
62
92
|
}
|
|
63
93
|
|
|
64
|
-
|
|
94
|
+
const reasoningDisplayConfig = config?.features?.reasoningDisplay ?? {};
|
|
95
|
+
const expandable = reasoningDisplayConfig.expandable !== false;
|
|
96
|
+
let expanded = expandable && reasoningExpansionState.has(message.id);
|
|
97
|
+
const isActive = reasoning.status !== "complete";
|
|
98
|
+
const previewText = getReasoningPreviewText(message, reasoningDisplayConfig.previewMaxLines ?? 3);
|
|
65
99
|
const header = createElement(
|
|
66
100
|
"button",
|
|
67
|
-
|
|
101
|
+
expandable
|
|
102
|
+
? "persona-flex persona-w-full persona-items-center persona-justify-between persona-gap-3 persona-bg-transparent persona-px-4 persona-py-3 persona-text-left persona-cursor-pointer persona-border-none"
|
|
103
|
+
: "persona-flex persona-w-full persona-items-center persona-justify-between persona-gap-3 persona-bg-transparent persona-px-4 persona-py-3 persona-text-left persona-cursor-default persona-border-none"
|
|
68
104
|
) as HTMLButtonElement;
|
|
69
105
|
header.type = "button";
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
if (expandable) {
|
|
107
|
+
header.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
108
|
+
header.setAttribute("data-expand-header", "true");
|
|
109
|
+
}
|
|
72
110
|
header.setAttribute("data-bubble-type", "reasoning");
|
|
73
111
|
|
|
74
112
|
const headerContent = createElement("div", "persona-flex persona-flex-col persona-text-left");
|
|
75
113
|
const title = createElement("span", "persona-text-xs persona-text-persona-primary");
|
|
76
|
-
|
|
77
|
-
|
|
114
|
+
const defaultSummary = "Thinking...";
|
|
115
|
+
const customSummary = config?.reasoning?.renderCollapsedSummary?.({
|
|
116
|
+
message,
|
|
117
|
+
reasoning,
|
|
118
|
+
defaultSummary,
|
|
119
|
+
previewText,
|
|
120
|
+
isActive,
|
|
121
|
+
config: config ?? {},
|
|
122
|
+
});
|
|
123
|
+
if (typeof customSummary === "string" && customSummary.trim()) {
|
|
124
|
+
title.textContent = customSummary;
|
|
125
|
+
headerContent.appendChild(title);
|
|
126
|
+
} else if (customSummary instanceof HTMLElement) {
|
|
127
|
+
headerContent.appendChild(customSummary);
|
|
128
|
+
} else {
|
|
129
|
+
title.textContent = defaultSummary;
|
|
130
|
+
headerContent.appendChild(title);
|
|
131
|
+
}
|
|
78
132
|
|
|
79
133
|
const status = createElement("span", "persona-text-xs persona-text-persona-primary");
|
|
80
134
|
status.textContent = describeReasonStatus(reasoning);
|
|
@@ -86,20 +140,54 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
|
|
|
86
140
|
title.style.display = "";
|
|
87
141
|
}
|
|
88
142
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
143
|
+
let toggleIcon: HTMLElement | null = null;
|
|
144
|
+
if (expandable) {
|
|
145
|
+
toggleIcon = createElement("div", "persona-flex persona-items-center");
|
|
146
|
+
const iconColor = "currentColor";
|
|
147
|
+
const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
|
|
148
|
+
if (chevronIcon) {
|
|
149
|
+
toggleIcon.appendChild(chevronIcon);
|
|
150
|
+
} else {
|
|
151
|
+
toggleIcon.textContent = expanded ? "Hide" : "Show";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const headerMeta = createElement("div", "persona-flex persona-items-center persona-ml-auto");
|
|
155
|
+
headerMeta.append(toggleIcon);
|
|
156
|
+
header.append(headerContent, headerMeta);
|
|
94
157
|
} else {
|
|
95
|
-
|
|
96
|
-
toggleIcon.textContent = expanded ? "Hide" : "Show";
|
|
158
|
+
header.append(headerContent);
|
|
97
159
|
}
|
|
98
160
|
|
|
99
|
-
const
|
|
100
|
-
|
|
161
|
+
const collapsedPreview = createElement(
|
|
162
|
+
"div",
|
|
163
|
+
"persona-px-4 persona-py-3 persona-text-xs persona-leading-snug persona-text-persona-muted"
|
|
164
|
+
);
|
|
165
|
+
collapsedPreview.setAttribute("data-persona-collapsed-preview", "reasoning");
|
|
166
|
+
collapsedPreview.style.display = "none";
|
|
167
|
+
collapsedPreview.style.whiteSpace = "pre-wrap";
|
|
168
|
+
|
|
169
|
+
if (!expanded && isActive && reasoningDisplayConfig.activePreview && previewText) {
|
|
170
|
+
const renderedPreview = config?.reasoning?.renderCollapsedPreview?.({
|
|
171
|
+
message,
|
|
172
|
+
reasoning,
|
|
173
|
+
defaultPreview: previewText,
|
|
174
|
+
isActive,
|
|
175
|
+
config: config ?? {},
|
|
176
|
+
});
|
|
177
|
+
if (!appendRenderedValue(collapsedPreview, renderedPreview)) {
|
|
178
|
+
collapsedPreview.textContent = previewText;
|
|
179
|
+
}
|
|
180
|
+
collapsedPreview.style.display = "";
|
|
181
|
+
}
|
|
101
182
|
|
|
102
|
-
|
|
183
|
+
if (!expanded && isActive && reasoningDisplayConfig.activeMinHeight) {
|
|
184
|
+
bubble.style.minHeight = reasoningDisplayConfig.activeMinHeight;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!expandable) {
|
|
188
|
+
bubble.append(header, collapsedPreview);
|
|
189
|
+
return bubble;
|
|
190
|
+
}
|
|
103
191
|
|
|
104
192
|
const content = createElement(
|
|
105
193
|
"div",
|
|
@@ -121,22 +209,23 @@ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement
|
|
|
121
209
|
|
|
122
210
|
const applyExpansionState = () => {
|
|
123
211
|
header.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
212
|
+
if (toggleIcon) {
|
|
213
|
+
toggleIcon.innerHTML = "";
|
|
214
|
+
const iconColor = "currentColor";
|
|
215
|
+
const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
|
|
216
|
+
if (chevronIcon) {
|
|
217
|
+
toggleIcon.appendChild(chevronIcon);
|
|
218
|
+
} else {
|
|
219
|
+
toggleIcon.textContent = expanded ? "Hide" : "Show";
|
|
220
|
+
}
|
|
133
221
|
}
|
|
134
222
|
content.style.display = expanded ? "" : "none";
|
|
223
|
+
collapsedPreview.style.display = expanded ? "none" : ((collapsedPreview.textContent || collapsedPreview.childNodes.length) ? "" : "none");
|
|
135
224
|
};
|
|
136
225
|
|
|
137
226
|
applyExpansionState();
|
|
138
227
|
|
|
139
|
-
bubble.append(header, content);
|
|
228
|
+
bubble.append(header, collapsedPreview, content);
|
|
140
229
|
return bubble;
|
|
141
230
|
};
|
|
142
231
|
|