@runtypelabs/persona 3.14.0 → 3.15.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 +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.global.js +64 -64
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +341 -219
- package/dist/theme-editor.d.cts +9 -0
- package/dist/theme-editor.d.ts +9 -0
- package/dist/theme-editor.js +341 -219
- package/dist/widget.css +11 -8
- package/package.json +1 -1
- package/src/client.test.ts +361 -0
- package/src/client.ts +183 -156
- package/src/styles/widget.css +11 -8
- package/src/types.ts +9 -0
- package/src/ui.ts +32 -5
- package/src/utils/sequence-buffer.test.ts +256 -0
- package/src/utils/sequence-buffer.ts +130 -0
package/src/client.ts
CHANGED
|
@@ -20,13 +20,14 @@ import {
|
|
|
20
20
|
ClientFeedbackType,
|
|
21
21
|
PersonaArtifactKind
|
|
22
22
|
} from "./types";
|
|
23
|
-
import {
|
|
24
|
-
extractTextFromJson,
|
|
23
|
+
import {
|
|
24
|
+
extractTextFromJson,
|
|
25
25
|
createPlainTextParser,
|
|
26
26
|
createJsonStreamParser,
|
|
27
27
|
createRegexJsonParser,
|
|
28
28
|
createXmlParser
|
|
29
29
|
} from "./utils/formatting";
|
|
30
|
+
import { SequenceReorderBuffer } from "./utils/sequence-buffer";
|
|
30
31
|
// artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
|
|
31
32
|
// artifact events are always processed here regardless of config.
|
|
32
33
|
|
|
@@ -1295,44 +1296,45 @@ export class AgentWidgetClient {
|
|
|
1295
1296
|
const streamParsers = new Map<string, AgentWidgetStreamParser>();
|
|
1296
1297
|
// Track accumulated raw content for structured formats (JSON, XML, etc.)
|
|
1297
1298
|
const rawContentBuffers = new Map<string, string>();
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1299
|
+
// Rebuild incremental text by sequence so late arrivals can repair already-emitted
|
|
1300
|
+
// content after the reorder buffer's gap-timeout flush.
|
|
1301
|
+
const orderedChunkBuffers = new Map<string, Array<{ seq: number; text: string }>>();
|
|
1302
|
+
const assistantMessagesByPartId = new Map<string, AgentWidgetMessage>();
|
|
1303
|
+
// Only the most-recently sealed segment is reconciled with step_complete's
|
|
1304
|
+
// final response. Earlier segments rely on their own async parser microtasks
|
|
1305
|
+
// resolving via the closure-captured `assistant` variable.
|
|
1306
|
+
let lastSealedTextSegment: AgentWidgetMessage | null = null;
|
|
1302
1307
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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);
|
|
1308
|
+
const insertOrderedChunk = (key: string, seq: number, text: string): string => {
|
|
1309
|
+
let chunks = orderedChunkBuffers.get(key);
|
|
1310
|
+
if (!chunks) {
|
|
1311
|
+
chunks = [];
|
|
1312
|
+
orderedChunkBuffers.set(key, chunks);
|
|
1317
1313
|
}
|
|
1318
|
-
|
|
1319
|
-
let lo = 0
|
|
1314
|
+
|
|
1315
|
+
let lo = 0;
|
|
1316
|
+
let hi = chunks.length;
|
|
1320
1317
|
while (lo < hi) {
|
|
1321
1318
|
const mid = (lo + hi) >>> 1;
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1319
|
+
if (chunks[mid].seq < seq) {
|
|
1320
|
+
lo = mid + 1;
|
|
1321
|
+
} else {
|
|
1322
|
+
hi = mid;
|
|
1323
|
+
}
|
|
1324
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
1325
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1326
|
+
if (chunks[lo]?.seq === seq) {
|
|
1327
|
+
chunks[lo] = { seq, text };
|
|
1328
|
+
} else {
|
|
1329
|
+
chunks.splice(lo, 0, { seq, text });
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
let accumulated = "";
|
|
1333
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
1334
|
+
accumulated += chunks[index].text;
|
|
1335
|
+
}
|
|
1336
|
+
return accumulated;
|
|
1337
|
+
};
|
|
1336
1338
|
|
|
1337
1339
|
/**
|
|
1338
1340
|
* After text_end + didSplitByPartId, merge the authoritative final response into the
|
|
@@ -1426,73 +1428,45 @@ export class AgentWidgetClient {
|
|
|
1426
1428
|
finalizeCleanup();
|
|
1427
1429
|
};
|
|
1428
1430
|
|
|
1431
|
+
// Sequence reorder buffer: SSE events carrying a `seq` (or `sequenceIndex`)
|
|
1432
|
+
// field are held and re-emitted in sequence order so that transport-level
|
|
1433
|
+
// reordering doesn't produce garbled output.
|
|
1434
|
+
const seqReadyQueue: Array<{ payloadType: string; payload: any }> = [];
|
|
1435
|
+
let isDrainScheduled = false;
|
|
1436
|
+
// Declared here so scheduleReadyQueueDrain can reference it; assigned
|
|
1437
|
+
// after all handler-scoped variables are initialised (before the SSE loop).
|
|
1438
|
+
let drainReadyQueue: () => void;
|
|
1439
|
+
// Two drain paths — both are intentional, do not remove either:
|
|
1440
|
+
// 1. Microtask drain (scheduleReadyQueueDrain): required when the
|
|
1441
|
+
// buffer's emitter fires from the gap-timeout setTimeout callback,
|
|
1442
|
+
// because there is no surrounding synchronous drain site there.
|
|
1443
|
+
// 2. Synchronous drain (drainReadyQueue() after each seqBuffer.push):
|
|
1444
|
+
// skips an extra microtask hop on the hot in-order push path.
|
|
1445
|
+
const scheduleReadyQueueDrain = () => {
|
|
1446
|
+
if (isDrainScheduled) return;
|
|
1447
|
+
isDrainScheduled = true;
|
|
1448
|
+
queueMicrotask(() => {
|
|
1449
|
+
isDrainScheduled = false;
|
|
1450
|
+
drainReadyQueue();
|
|
1451
|
+
});
|
|
1452
|
+
};
|
|
1453
|
+
const seqBuffer = new SequenceReorderBuffer((payloadType: string, payload: any) => {
|
|
1454
|
+
seqReadyQueue.push({ payloadType, payload });
|
|
1455
|
+
scheduleReadyQueueDrain();
|
|
1456
|
+
});
|
|
1429
1457
|
// Agent execution state tracking
|
|
1430
1458
|
let agentExecution: AgentExecutionState | null = null;
|
|
1431
1459
|
// Track assistant messages per agent iteration for 'separate' mode
|
|
1432
1460
|
const agentIterationMessages = new Map<number, AgentWidgetMessage>();
|
|
1433
1461
|
const iterationDisplay = this.config.iterationDisplay ?? 'separate';
|
|
1434
1462
|
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
buffer = events.pop() ?? "";
|
|
1443
|
-
|
|
1444
|
-
for (const event of events) {
|
|
1445
|
-
const lines = event.split("\n");
|
|
1446
|
-
let eventType = "message";
|
|
1447
|
-
let data = "";
|
|
1448
|
-
|
|
1449
|
-
for (const line of lines) {
|
|
1450
|
-
if (line.startsWith("event:")) {
|
|
1451
|
-
eventType = line.replace("event:", "").trim();
|
|
1452
|
-
} else if (line.startsWith("data:")) {
|
|
1453
|
-
data += line.replace("data:", "").trim();
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (!data) continue;
|
|
1458
|
-
let payload: any;
|
|
1459
|
-
try {
|
|
1460
|
-
payload = JSON.parse(data);
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
onEvent({
|
|
1463
|
-
type: "error",
|
|
1464
|
-
error:
|
|
1465
|
-
error instanceof Error
|
|
1466
|
-
? error
|
|
1467
|
-
: new Error("Failed to parse chat stream payload")
|
|
1468
|
-
});
|
|
1469
|
-
continue;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
const payloadType =
|
|
1473
|
-
eventType !== "message" ? eventType : payload.type ?? "message";
|
|
1474
|
-
|
|
1475
|
-
// Tap: capture raw SSE event for event stream inspector
|
|
1476
|
-
this.onSSEEvent?.(payloadType, payload);
|
|
1477
|
-
|
|
1478
|
-
// If custom SSE event parser is provided, try it first
|
|
1479
|
-
if (this.parseSSEEvent) {
|
|
1480
|
-
// Keep assistant message ref in sync
|
|
1481
|
-
assistantMessageRef.current = assistantMessage;
|
|
1482
|
-
const handled = await this.handleCustomSSEEvent(
|
|
1483
|
-
payload,
|
|
1484
|
-
onEvent,
|
|
1485
|
-
assistantMessageRef,
|
|
1486
|
-
emitMessage,
|
|
1487
|
-
nextSequence,
|
|
1488
|
-
partIdState
|
|
1489
|
-
);
|
|
1490
|
-
// Update assistantMessage from ref (in case it was created or replaced by partId segmentation)
|
|
1491
|
-
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
1492
|
-
assistantMessage = assistantMessageRef.current;
|
|
1493
|
-
}
|
|
1494
|
-
if (handled) continue; // Skip default handling if custom handler processed it
|
|
1495
|
-
}
|
|
1463
|
+
// Drains reorder-buffered events through the main event handler.
|
|
1464
|
+
// Also invoked after the SSE loop exits so any events buffered at
|
|
1465
|
+
// end-of-stream are processed.
|
|
1466
|
+
drainReadyQueue = () => {
|
|
1467
|
+
for (let i = 0; i < seqReadyQueue.length; i++) {
|
|
1468
|
+
const payloadType = seqReadyQueue[i].payloadType;
|
|
1469
|
+
const payload = seqReadyQueue[i].payload;
|
|
1496
1470
|
|
|
1497
1471
|
if (payloadType === "reason_start") {
|
|
1498
1472
|
const reasoningId =
|
|
@@ -1531,12 +1505,11 @@ export class AgentWidgetClient {
|
|
|
1531
1505
|
payload.delta ??
|
|
1532
1506
|
"";
|
|
1533
1507
|
if (chunk && payload.hidden !== true) {
|
|
1534
|
-
const reasonSeq = typeof payload.sequenceIndex ===
|
|
1508
|
+
const reasonSeq = typeof payload.sequenceIndex === "number" ? payload.sequenceIndex : undefined;
|
|
1535
1509
|
if (reasonSeq !== undefined) {
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
// We store a single joined string to avoid duplicated / mis-ordered entries.
|
|
1510
|
+
// Rebuild chunks by seq so late arrivals after a gap-timeout flush
|
|
1511
|
+
// are inserted at the correct position rather than appended.
|
|
1512
|
+
const ordered = insertOrderedChunk(reasoningId, reasonSeq, String(chunk));
|
|
1540
1513
|
reasoningMessage.reasoning.chunks = [ordered];
|
|
1541
1514
|
} else {
|
|
1542
1515
|
reasoningMessage.reasoning.chunks.push(String(chunk));
|
|
@@ -1552,7 +1525,7 @@ export class AgentWidgetClient {
|
|
|
1552
1525
|
0,
|
|
1553
1526
|
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
1554
1527
|
);
|
|
1555
|
-
|
|
1528
|
+
|
|
1556
1529
|
}
|
|
1557
1530
|
reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
|
|
1558
1531
|
emitMessage(reasoningMessage);
|
|
@@ -1573,7 +1546,7 @@ export class AgentWidgetClient {
|
|
|
1573
1546
|
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
1574
1547
|
);
|
|
1575
1548
|
reasoningMessage.streaming = false;
|
|
1576
|
-
|
|
1549
|
+
|
|
1577
1550
|
emitMessage(reasoningMessage);
|
|
1578
1551
|
}
|
|
1579
1552
|
const stepKey = getStepKey(payload);
|
|
@@ -1748,26 +1721,30 @@ export class AgentWidgetClient {
|
|
|
1748
1721
|
partIdState.current = incomingPartId;
|
|
1749
1722
|
}
|
|
1750
1723
|
|
|
1751
|
-
const assistant =
|
|
1752
|
-
|
|
1753
|
-
|
|
1724
|
+
const assistant =
|
|
1725
|
+
incomingPartId !== undefined
|
|
1726
|
+
? (assistantMessagesByPartId.get(incomingPartId) ?? ensureAssistantMessage())
|
|
1727
|
+
: ensureAssistantMessage();
|
|
1728
|
+
if (incomingPartId !== undefined) {
|
|
1729
|
+
if (!assistant.partId) {
|
|
1730
|
+
assistant.partId = incomingPartId;
|
|
1731
|
+
}
|
|
1732
|
+
assistantMessagesByPartId.set(incomingPartId, assistant);
|
|
1754
1733
|
}
|
|
1755
1734
|
// Support various field names: text, delta, content, chunk (Runtype uses 'chunk')
|
|
1756
1735
|
const chunk = payload.text ?? payload.delta ?? payload.content ?? payload.chunk ?? "";
|
|
1757
1736
|
if (chunk) {
|
|
1758
|
-
// Check if the event carries a sequence number for reordering
|
|
1759
|
-
const chunkSeq = typeof payload.seq === 'number' ? payload.seq : undefined;
|
|
1760
|
-
|
|
1761
1737
|
// Accumulate raw content for structured format parsing.
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1738
|
+
// Most out-of-order events are fixed at the dispatch layer, but once the
|
|
1739
|
+
// gap timeout flushes later seqs we can still see genuine late arrivals.
|
|
1740
|
+
// Rebuild chunked content by seq so those events repair prior output
|
|
1741
|
+
// instead of appending in the wrong position.
|
|
1742
|
+
const chunkSeq = typeof payload.seq === "number" ? payload.seq : undefined;
|
|
1743
|
+
const chunkBufferKey = incomingPartId ?? assistant.id;
|
|
1744
|
+
const accumulatedRaw =
|
|
1745
|
+
chunkSeq !== undefined
|
|
1746
|
+
? insertOrderedChunk(chunkBufferKey, chunkSeq, String(chunk))
|
|
1747
|
+
: (rawContentBuffers.get(assistant.id) ?? "") + chunk;
|
|
1771
1748
|
// Store raw content for action parsing, but NEVER set assistant.content to raw JSON
|
|
1772
1749
|
assistant.rawContent = accumulatedRaw;
|
|
1773
1750
|
|
|
@@ -1790,13 +1767,7 @@ export class AgentWidgetClient {
|
|
|
1790
1767
|
|
|
1791
1768
|
// If plain text parser, just append the chunk directly
|
|
1792
1769
|
if (isPlainTextParser) {
|
|
1793
|
-
|
|
1794
|
-
// otherwise fall back to simple append.
|
|
1795
|
-
if (chunkSeq !== undefined) {
|
|
1796
|
-
assistant.content = accumulatedRaw;
|
|
1797
|
-
} else {
|
|
1798
|
-
assistant.content += chunk;
|
|
1799
|
-
}
|
|
1770
|
+
assistant.content = chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1800
1771
|
// Clear any raw buffer/parser since we're in plain text mode
|
|
1801
1772
|
rawContentBuffers.delete(assistant.id);
|
|
1802
1773
|
streamParsers.delete(assistant.id);
|
|
@@ -1822,27 +1793,25 @@ export class AgentWidgetClient {
|
|
|
1822
1793
|
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
1823
1794
|
// Not a structured format - show as plain text
|
|
1824
1795
|
const currentAssistant = assistantMessage;
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
currentAssistant
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1796
|
+
const targetAssistant =
|
|
1797
|
+
currentAssistant && currentAssistant.id === assistant.id
|
|
1798
|
+
? currentAssistant
|
|
1799
|
+
: assistant;
|
|
1800
|
+
if (targetAssistant.id === assistant.id) {
|
|
1801
|
+
targetAssistant.content =
|
|
1802
|
+
chunkSeq !== undefined ? accumulatedRaw : targetAssistant.content + chunk;
|
|
1803
|
+
rawContentBuffers.delete(targetAssistant.id);
|
|
1804
|
+
streamParsers.delete(targetAssistant.id);
|
|
1805
|
+
targetAssistant.rawContent = undefined;
|
|
1806
|
+
emitMessage(targetAssistant);
|
|
1835
1807
|
}
|
|
1836
1808
|
}
|
|
1837
1809
|
// Otherwise wait for more chunks (incomplete structured format)
|
|
1838
1810
|
// Don't emit message if parser hasn't extracted text yet
|
|
1839
1811
|
}).catch(() => {
|
|
1840
1812
|
// On error, treat as plain text
|
|
1841
|
-
|
|
1842
|
-
assistant.content
|
|
1843
|
-
} else {
|
|
1844
|
-
assistant.content += chunk;
|
|
1845
|
-
}
|
|
1813
|
+
assistant.content =
|
|
1814
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1846
1815
|
rawContentBuffers.delete(assistant.id);
|
|
1847
1816
|
streamParsers.delete(assistant.id);
|
|
1848
1817
|
assistant.rawContent = undefined;
|
|
@@ -1860,11 +1829,8 @@ export class AgentWidgetClient {
|
|
|
1860
1829
|
emitMessage(assistant);
|
|
1861
1830
|
} else if (!looksLikeJson && !accumulatedRaw.trim().startsWith('<')) {
|
|
1862
1831
|
// Not a structured format - show as plain text
|
|
1863
|
-
|
|
1864
|
-
assistant.content
|
|
1865
|
-
} else {
|
|
1866
|
-
assistant.content += chunk;
|
|
1867
|
-
}
|
|
1832
|
+
assistant.content =
|
|
1833
|
+
chunkSeq !== undefined ? accumulatedRaw : assistant.content + chunk;
|
|
1868
1834
|
// Clear any raw buffer/parser if we were in structured format mode
|
|
1869
1835
|
rawContentBuffers.delete(assistant.id);
|
|
1870
1836
|
streamParsers.delete(assistant.id);
|
|
@@ -1917,7 +1883,6 @@ export class AgentWidgetClient {
|
|
|
1917
1883
|
// Clean up
|
|
1918
1884
|
streamParsers.delete(currentAssistant.id);
|
|
1919
1885
|
rawContentBuffers.delete(currentAssistant.id);
|
|
1920
|
-
seqChunkBuffers.delete(currentAssistant.id);
|
|
1921
1886
|
emitMessage(currentAssistant);
|
|
1922
1887
|
}
|
|
1923
1888
|
}
|
|
@@ -1949,7 +1914,6 @@ export class AgentWidgetClient {
|
|
|
1949
1914
|
streamParsers.delete(assistant.id);
|
|
1950
1915
|
}
|
|
1951
1916
|
rawContentBuffers.delete(assistant.id);
|
|
1952
|
-
seqChunkBuffers.delete(assistant.id);
|
|
1953
1917
|
assistant.streaming = false;
|
|
1954
1918
|
emitMessage(assistant);
|
|
1955
1919
|
}
|
|
@@ -1970,7 +1934,6 @@ export class AgentWidgetClient {
|
|
|
1970
1934
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
1971
1935
|
streamParsers.delete(msg.id);
|
|
1972
1936
|
rawContentBuffers.delete(msg.id);
|
|
1973
|
-
seqChunkBuffers.delete(msg.id);
|
|
1974
1937
|
if (msg.streaming !== false) {
|
|
1975
1938
|
msg.streaming = false;
|
|
1976
1939
|
emitMessage(msg);
|
|
@@ -2035,7 +1998,6 @@ export class AgentWidgetClient {
|
|
|
2035
1998
|
// Clean up
|
|
2036
1999
|
streamParsers.delete(currentAssistant.id);
|
|
2037
2000
|
rawContentBuffers.delete(currentAssistant.id);
|
|
2038
|
-
seqChunkBuffers.delete(currentAssistant.id);
|
|
2039
2001
|
emitMessage(currentAssistant);
|
|
2040
2002
|
}
|
|
2041
2003
|
} else {
|
|
@@ -2053,7 +2015,6 @@ export class AgentWidgetClient {
|
|
|
2053
2015
|
// Clean up
|
|
2054
2016
|
streamParsers.delete(currentAssistant.id);
|
|
2055
2017
|
rawContentBuffers.delete(currentAssistant.id);
|
|
2056
|
-
seqChunkBuffers.delete(currentAssistant.id);
|
|
2057
2018
|
emitMessage(currentAssistant);
|
|
2058
2019
|
}
|
|
2059
2020
|
}
|
|
@@ -2101,7 +2062,6 @@ export class AgentWidgetClient {
|
|
|
2101
2062
|
}
|
|
2102
2063
|
streamParsers.delete(assistant.id);
|
|
2103
2064
|
rawContentBuffers.delete(assistant.id);
|
|
2104
|
-
seqChunkBuffers.delete(assistant.id);
|
|
2105
2065
|
assistant.streaming = false;
|
|
2106
2066
|
emitMessage(assistant);
|
|
2107
2067
|
}
|
|
@@ -2109,7 +2069,6 @@ export class AgentWidgetClient {
|
|
|
2109
2069
|
// No final content, just mark as complete and clean up
|
|
2110
2070
|
streamParsers.delete(assistant.id);
|
|
2111
2071
|
rawContentBuffers.delete(assistant.id);
|
|
2112
|
-
seqChunkBuffers.delete(assistant.id);
|
|
2113
2072
|
assistant.streaming = false;
|
|
2114
2073
|
emitMessage(assistant);
|
|
2115
2074
|
}
|
|
@@ -2122,7 +2081,6 @@ export class AgentWidgetClient {
|
|
|
2122
2081
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
2123
2082
|
streamParsers.delete(msg.id);
|
|
2124
2083
|
rawContentBuffers.delete(msg.id);
|
|
2125
|
-
seqChunkBuffers.delete(msg.id);
|
|
2126
2084
|
if (msg.streaming !== false) {
|
|
2127
2085
|
msg.streaming = false;
|
|
2128
2086
|
emitMessage(msg);
|
|
@@ -2164,7 +2122,6 @@ export class AgentWidgetClient {
|
|
|
2164
2122
|
// Clean up parser and buffer
|
|
2165
2123
|
streamParsers.delete(assistant.id);
|
|
2166
2124
|
rawContentBuffers.delete(assistant.id);
|
|
2167
|
-
seqChunkBuffers.delete(assistant.id);
|
|
2168
2125
|
|
|
2169
2126
|
// Only emit if something actually changed to avoid flicker
|
|
2170
2127
|
const contentChanged = displayContent !== assistant.content;
|
|
@@ -2187,8 +2144,6 @@ export class AgentWidgetClient {
|
|
|
2187
2144
|
const msg: AgentWidgetMessage = assistantMessage;
|
|
2188
2145
|
streamParsers.delete(msg.id);
|
|
2189
2146
|
rawContentBuffers.delete(msg.id);
|
|
2190
|
-
seqChunkBuffers.delete(msg.id);
|
|
2191
|
-
|
|
2192
2147
|
// Only emit if streaming state changed
|
|
2193
2148
|
if (msg.streaming !== false) {
|
|
2194
2149
|
msg.streaming = false;
|
|
@@ -2591,7 +2546,6 @@ export class AgentWidgetClient {
|
|
|
2591
2546
|
assistantMessageRef.current = null;
|
|
2592
2547
|
streamParsers.delete(id);
|
|
2593
2548
|
rawContentBuffers.delete(id);
|
|
2594
|
-
seqChunkBuffers.delete(id);
|
|
2595
2549
|
} else if (
|
|
2596
2550
|
payloadType === "error" ||
|
|
2597
2551
|
payloadType === "step_error" ||
|
|
@@ -2631,6 +2585,79 @@ export class AgentWidgetClient {
|
|
|
2631
2585
|
}
|
|
2632
2586
|
}
|
|
2633
2587
|
}
|
|
2588
|
+
seqReadyQueue.length = 0;
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
// eslint-disable-next-line no-constant-condition
|
|
2592
|
+
while (true) {
|
|
2593
|
+
const { done, value } = await reader.read();
|
|
2594
|
+
if (done) break;
|
|
2595
|
+
|
|
2596
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2597
|
+
const events = buffer.split("\n\n");
|
|
2598
|
+
buffer = events.pop() ?? "";
|
|
2599
|
+
|
|
2600
|
+
for (const event of events) {
|
|
2601
|
+
const lines = event.split("\n");
|
|
2602
|
+
let eventType = "message";
|
|
2603
|
+
let data = "";
|
|
2604
|
+
|
|
2605
|
+
for (const line of lines) {
|
|
2606
|
+
if (line.startsWith("event:")) {
|
|
2607
|
+
eventType = line.replace("event:", "").trim();
|
|
2608
|
+
} else if (line.startsWith("data:")) {
|
|
2609
|
+
data += line.replace("data:", "").trim();
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
if (!data) continue;
|
|
2614
|
+
let payload: any;
|
|
2615
|
+
try {
|
|
2616
|
+
payload = JSON.parse(data);
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
onEvent({
|
|
2619
|
+
type: "error",
|
|
2620
|
+
error:
|
|
2621
|
+
error instanceof Error
|
|
2622
|
+
? error
|
|
2623
|
+
: new Error("Failed to parse chat stream payload")
|
|
2624
|
+
});
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
const payloadType =
|
|
2629
|
+
eventType !== "message" ? eventType : payload.type ?? "message";
|
|
2630
|
+
|
|
2631
|
+
// Tap: capture raw SSE event for event stream inspector
|
|
2632
|
+
this.onSSEEvent?.(payloadType, payload);
|
|
2633
|
+
|
|
2634
|
+
// If custom SSE event parser is provided, try it first
|
|
2635
|
+
if (this.parseSSEEvent) {
|
|
2636
|
+
// Keep assistant message ref in sync
|
|
2637
|
+
assistantMessageRef.current = assistantMessage;
|
|
2638
|
+
const handled = await this.handleCustomSSEEvent(
|
|
2639
|
+
payload,
|
|
2640
|
+
onEvent,
|
|
2641
|
+
assistantMessageRef,
|
|
2642
|
+
emitMessage,
|
|
2643
|
+
nextSequence,
|
|
2644
|
+
partIdState
|
|
2645
|
+
);
|
|
2646
|
+
// Update assistantMessage from ref (in case it was created or replaced by partId segmentation)
|
|
2647
|
+
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
2648
|
+
assistantMessage = assistantMessageRef.current;
|
|
2649
|
+
}
|
|
2650
|
+
if (handled) continue; // Skip default handling if custom handler processed it
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// Push through the sequence reorder buffer
|
|
2654
|
+
seqBuffer.push(payloadType, payload);
|
|
2655
|
+
drainReadyQueue();
|
|
2656
|
+
}
|
|
2634
2657
|
}
|
|
2658
|
+
|
|
2659
|
+
seqBuffer.flushPending();
|
|
2660
|
+
drainReadyQueue();
|
|
2661
|
+
seqBuffer.destroy();
|
|
2635
2662
|
}
|
|
2636
2663
|
}
|
package/src/styles/widget.css
CHANGED
|
@@ -1778,10 +1778,6 @@
|
|
|
1778
1778
|
margin-top: 0.5rem;
|
|
1779
1779
|
padding: 0.25rem 0.5rem;
|
|
1780
1780
|
border-top: none;
|
|
1781
|
-
border-radius: var(--persona-radius-md, 0.75rem);
|
|
1782
|
-
background-color: var(--persona-surface, #ffffff);
|
|
1783
|
-
border: 1px solid var(--persona-divider, #f1f5f9);
|
|
1784
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
1785
1781
|
}
|
|
1786
1782
|
|
|
1787
1783
|
/* Pill alignment in always-visible mode (block flow: use margin to position) */
|
|
@@ -1826,10 +1822,6 @@
|
|
|
1826
1822
|
padding: 0.25rem;
|
|
1827
1823
|
border-top: none;
|
|
1828
1824
|
width: fit-content;
|
|
1829
|
-
background-color: var(--persona-surface, #ffffff);
|
|
1830
|
-
border: 1px solid var(--persona-divider, #f1f5f9);
|
|
1831
|
-
border-radius: var(--persona-radius-md, 0.75rem);
|
|
1832
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
1833
1825
|
}
|
|
1834
1826
|
|
|
1835
1827
|
/* Pill layout - position based on alignment */
|
|
@@ -1932,6 +1924,17 @@
|
|
|
1932
1924
|
opacity: 0.9;
|
|
1933
1925
|
}
|
|
1934
1926
|
|
|
1927
|
+
/* Vote pop animation */
|
|
1928
|
+
@keyframes persona-vote-pop {
|
|
1929
|
+
0% { transform: scale(1); }
|
|
1930
|
+
40% { transform: scale(1.25); }
|
|
1931
|
+
100% { transform: scale(1); }
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
.persona-message-action-btn.persona-message-action-pop {
|
|
1935
|
+
animation: persona-vote-pop 0.3s ease;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1935
1938
|
/* Success state (after copy) */
|
|
1936
1939
|
.persona-message-action-btn.persona-message-action-success {
|
|
1937
1940
|
background-color: #10b981;
|
package/src/types.ts
CHANGED
|
@@ -2901,6 +2901,15 @@ export type AgentWidgetConfig = {
|
|
|
2901
2901
|
* ```
|
|
2902
2902
|
*/
|
|
2903
2903
|
parseSSEEvent?: AgentWidgetSSEEventParser;
|
|
2904
|
+
/**
|
|
2905
|
+
* Called for every parsed SSE frame (after JSON parse), before native handling.
|
|
2906
|
+
* Use for lightweight side effects (e.g. telemetry). Does not replace native
|
|
2907
|
+
* streaming; pair with {@link parseSSEEvent} only when you need to override text mapping.
|
|
2908
|
+
*
|
|
2909
|
+
* When the event stream inspector is enabled, this runs in the same order as
|
|
2910
|
+
* events are appended to the inspector buffer.
|
|
2911
|
+
*/
|
|
2912
|
+
onSSEEvent?: (eventType: string, payload: unknown) => void;
|
|
2904
2913
|
/**
|
|
2905
2914
|
* Layout configuration for customizing widget appearance and structure.
|
|
2906
2915
|
* Provides control over header, messages, and content slots.
|
package/src/ui.ts
CHANGED
|
@@ -1229,22 +1229,47 @@ export const createAgentExperience = (
|
|
|
1229
1229
|
} else if (action === 'upvote' || action === 'downvote') {
|
|
1230
1230
|
const currentVote = messageVoteState.get(messageId) ?? null;
|
|
1231
1231
|
const wasActive = currentVote === action;
|
|
1232
|
+
const iconName = action === 'upvote' ? 'thumbs-up' : 'thumbs-down';
|
|
1232
1233
|
|
|
1233
1234
|
if (wasActive) {
|
|
1234
|
-
// Toggle off
|
|
1235
|
+
// Toggle off — revert to outline icon
|
|
1235
1236
|
messageVoteState.delete(messageId);
|
|
1236
1237
|
actionBtn.classList.remove("persona-message-action-active");
|
|
1238
|
+
const outlineIcon = renderLucideIcon(iconName, 14, "currentColor", 2);
|
|
1239
|
+
if (outlineIcon) {
|
|
1240
|
+
actionBtn.innerHTML = "";
|
|
1241
|
+
actionBtn.appendChild(outlineIcon);
|
|
1242
|
+
}
|
|
1237
1243
|
} else {
|
|
1238
|
-
// Clear opposite vote button
|
|
1244
|
+
// Clear opposite vote button and revert its icon
|
|
1239
1245
|
const oppositeAction = action === 'upvote' ? 'downvote' : 'upvote';
|
|
1240
1246
|
const oppositeBtn = actionsContainer.querySelector(`[data-action="${oppositeAction}"]`);
|
|
1241
1247
|
if (oppositeBtn) {
|
|
1242
1248
|
oppositeBtn.classList.remove("persona-message-action-active");
|
|
1249
|
+
const oppositeIconName = oppositeAction === 'upvote' ? 'thumbs-up' : 'thumbs-down';
|
|
1250
|
+
const outlineIcon = renderLucideIcon(oppositeIconName, 14, "currentColor", 2);
|
|
1251
|
+
if (outlineIcon) {
|
|
1252
|
+
oppositeBtn.innerHTML = "";
|
|
1253
|
+
oppositeBtn.appendChild(outlineIcon);
|
|
1254
|
+
}
|
|
1243
1255
|
}
|
|
1244
1256
|
|
|
1245
1257
|
messageVoteState.set(messageId, action);
|
|
1246
1258
|
actionBtn.classList.add("persona-message-action-active");
|
|
1247
1259
|
|
|
1260
|
+
// Swap to filled icon
|
|
1261
|
+
const filledIcon = renderLucideIcon(iconName, 14, "currentColor", 2);
|
|
1262
|
+
if (filledIcon) {
|
|
1263
|
+
filledIcon.setAttribute("fill", "currentColor");
|
|
1264
|
+
actionBtn.innerHTML = "";
|
|
1265
|
+
actionBtn.appendChild(filledIcon);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Pop animation
|
|
1269
|
+
actionBtn.classList.remove("persona-message-action-pop");
|
|
1270
|
+
void actionBtn.offsetWidth; // force reflow to restart animation
|
|
1271
|
+
actionBtn.classList.add("persona-message-action-pop");
|
|
1272
|
+
|
|
1248
1273
|
// Trigger feedback
|
|
1249
1274
|
const messages = session.getMessages();
|
|
1250
1275
|
const message = messages.find(m => m.id === messageId);
|
|
@@ -3118,9 +3143,10 @@ export const createAgentExperience = (
|
|
|
3118
3143
|
});
|
|
3119
3144
|
}
|
|
3120
3145
|
|
|
3121
|
-
// Wire up event stream buffer to capture SSE events
|
|
3122
|
-
if (eventStreamBuffer) {
|
|
3146
|
+
// Wire up optional SSE tap (host) + event stream buffer to capture SSE events
|
|
3147
|
+
if (eventStreamBuffer || config.onSSEEvent) {
|
|
3123
3148
|
session.setSSEEventCallback((type: string, payload: unknown) => {
|
|
3149
|
+
config.onSSEEvent?.(type, payload);
|
|
3124
3150
|
eventStreamBuffer?.push({
|
|
3125
3151
|
id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3126
3152
|
type,
|
|
@@ -4187,8 +4213,9 @@ export const createAgentExperience = (
|
|
|
4187
4213
|
eventStreamStore = new EventStreamStore(eventStreamDbName);
|
|
4188
4214
|
eventStreamBuffer = new EventStreamBuffer(eventStreamMaxEvents, eventStreamStore);
|
|
4189
4215
|
eventStreamStore.open().then(() => eventStreamBuffer?.restore()).catch(() => {});
|
|
4190
|
-
// Register the SSE event callback
|
|
4216
|
+
// Register the SSE event callback (host tap + buffer)
|
|
4191
4217
|
session.setSSEEventCallback((type: string, payload: unknown) => {
|
|
4218
|
+
config.onSSEEvent?.(type, payload);
|
|
4192
4219
|
eventStreamBuffer!.push({
|
|
4193
4220
|
id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4194
4221
|
type,
|