@runtypelabs/persona 4.4.2 → 4.6.0
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/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-C6tFDxKy.d.cts → types-8RICZWQe.d.cts} +6 -0
- package/dist/animations/{types-C6tFDxKy.d.ts → types-8RICZWQe.d.ts} +6 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-DFBSCFYN.js +1 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +50 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +286 -11
- package/dist/index.d.ts +286 -11
- package/dist/index.global.js +39 -39
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +51 -51
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers-entry-NVFT3TE6.js +1 -0
- package/dist/runtype-tts-entry-HFUV2UF7.js +1 -0
- package/dist/session-reconnect-U77QFUR7.js +1 -0
- package/dist/smart-dom-reader.d.cts +147 -8
- package/dist/smart-dom-reader.d.ts +147 -8
- package/dist/theme-editor-preview.cjs +48 -48
- package/dist/theme-editor-preview.d.cts +187 -9
- package/dist/theme-editor-preview.d.ts +187 -9
- package/dist/theme-editor-preview.js +48 -48
- package/dist/theme-editor.cjs +1 -1
- package/dist/theme-editor.d.cts +147 -8
- package/dist/theme-editor.d.ts +147 -8
- package/dist/theme-editor.js +1 -1
- package/package.json +2 -2
- package/src/client.ts +48 -7
- package/src/defaults.ts +9 -1
- package/src/generated/runtype-openapi-contract.ts +6 -0
- package/src/index-core.ts +5 -0
- package/src/reconnect-wake.test.ts +162 -0
- package/src/reconnect.test.ts +430 -0
- package/src/session-reconnect.ts +282 -0
- package/src/session.ts +408 -5
- package/src/types.ts +165 -9
- package/src/ui.scroll-additive.test.ts +451 -0
- package/src/ui.scroll.test.ts +8 -2
- package/src/ui.stream-animation-update.test.ts +99 -0
- package/src/ui.ts +371 -41
- package/src/utils/constants.ts +3 -1
package/src/client.ts
CHANGED
|
@@ -954,11 +954,12 @@ export class AgentWidgetClient {
|
|
|
954
954
|
public async processStream(
|
|
955
955
|
body: ReadableStream<Uint8Array>,
|
|
956
956
|
onEvent: SSEHandler,
|
|
957
|
-
assistantMessageId?: string
|
|
957
|
+
assistantMessageId?: string,
|
|
958
|
+
seedContent?: string
|
|
958
959
|
): Promise<void> {
|
|
959
960
|
onEvent({ type: "status", status: "connected" });
|
|
960
961
|
try {
|
|
961
|
-
await this.streamResponse(body, onEvent, assistantMessageId);
|
|
962
|
+
await this.streamResponse(body, onEvent, assistantMessageId, seedContent);
|
|
962
963
|
} finally {
|
|
963
964
|
onEvent({ type: "status", status: "idle" });
|
|
964
965
|
}
|
|
@@ -1353,7 +1354,12 @@ export class AgentWidgetClient {
|
|
|
1353
1354
|
private async streamResponse(
|
|
1354
1355
|
body: ReadableStream<Uint8Array>,
|
|
1355
1356
|
onEvent: SSEHandler,
|
|
1356
|
-
assistantMessageId?: string
|
|
1357
|
+
assistantMessageId?: string,
|
|
1358
|
+
// Durable reconnect: seed the assistant accumulator with the text already
|
|
1359
|
+
// shown before the drop, so replayed post-cursor deltas APPEND to it
|
|
1360
|
+
// instead of a fresh stream clobbering it (the replay carries only
|
|
1361
|
+
// `seq > after`, i.e. the new deltas, not the full text).
|
|
1362
|
+
seedContent?: string
|
|
1357
1363
|
) {
|
|
1358
1364
|
const reader = body.getReader();
|
|
1359
1365
|
const decoder = new TextDecoder();
|
|
@@ -1490,10 +1496,14 @@ export class AgentWidgetClient {
|
|
|
1490
1496
|
const ensureAssistantMessage = () => {
|
|
1491
1497
|
if (assistantMessage) return assistantMessage;
|
|
1492
1498
|
let id: string;
|
|
1499
|
+
let initialContent = "";
|
|
1493
1500
|
const segment = currentTextBlockId;
|
|
1494
1501
|
if (!assistantIdConsumed && baseAssistantId) {
|
|
1495
1502
|
id = baseAssistantId;
|
|
1496
1503
|
assistantIdConsumed = true;
|
|
1504
|
+
// First (and only) time we reuse the caller-supplied id: this is the
|
|
1505
|
+
// bubble a durable reconnect resumes into, so continue its text.
|
|
1506
|
+
initialContent = seedContent ?? "";
|
|
1497
1507
|
} else if (baseAssistantId && segment) {
|
|
1498
1508
|
id = `${baseAssistantId}_${segment}`;
|
|
1499
1509
|
} else {
|
|
@@ -1502,7 +1512,7 @@ export class AgentWidgetClient {
|
|
|
1502
1512
|
assistantMessage = {
|
|
1503
1513
|
id,
|
|
1504
1514
|
role: "assistant",
|
|
1505
|
-
content:
|
|
1515
|
+
content: initialContent,
|
|
1506
1516
|
createdAt: new Date().toISOString(),
|
|
1507
1517
|
streaming: true,
|
|
1508
1518
|
sequence: nextSequence()
|
|
@@ -2877,7 +2887,11 @@ export class AgentWidgetClient {
|
|
|
2877
2887
|
pendingFlowRaw = "";
|
|
2878
2888
|
lastSealedFlowBubble = null;
|
|
2879
2889
|
|
|
2880
|
-
|
|
2890
|
+
// `terminal: true` marks this as a graceful finish (not a drop). The
|
|
2891
|
+
// session uses it to distinguish the real end-of-turn from the plain
|
|
2892
|
+
// `idle` the dispatch wrappers emit in their `finally` when a durable
|
|
2893
|
+
// connection drops mid-stream (durable-reconnect drop detection).
|
|
2894
|
+
onEvent({ type: "status", status: "idle", terminal: true });
|
|
2881
2895
|
} else if (payloadType === "execution_error") {
|
|
2882
2896
|
// Terminal failure. The non-terminal `error` is handled
|
|
2883
2897
|
// separately (recoverable → warn).
|
|
@@ -3163,20 +3177,43 @@ export class AgentWidgetClient {
|
|
|
3163
3177
|
const lines = event.split("\n");
|
|
3164
3178
|
let eventType = "message";
|
|
3165
3179
|
let data = "";
|
|
3180
|
+
// Durable-reconnect cursor: the SSE `id:` line (the durable row seq).
|
|
3181
|
+
// Only durable, resumable agent executions stamp these (e.g. Claude
|
|
3182
|
+
// Managed agents, or any async/background run the backend persists and
|
|
3183
|
+
// can replay); other streams carry no cursor. We emit a `cursor`
|
|
3184
|
+
// event AFTER the frame is fully parsed and dispatched, so the session's
|
|
3185
|
+
// `lastEventId` only advances past frames it has actually applied, so the
|
|
3186
|
+
// happy path has no dupes against the server's `seq > after` replay.
|
|
3187
|
+
let frameId: string | null = null;
|
|
3166
3188
|
|
|
3167
3189
|
for (const line of lines) {
|
|
3168
3190
|
if (line.startsWith("event:")) {
|
|
3169
3191
|
eventType = line.replace("event:", "").trim();
|
|
3170
3192
|
} else if (line.startsWith("data:")) {
|
|
3171
3193
|
data += line.replace("data:", "").trim();
|
|
3194
|
+
} else if (line.startsWith("id:")) {
|
|
3195
|
+
frameId = line.slice(3).trim();
|
|
3172
3196
|
}
|
|
3173
3197
|
}
|
|
3174
3198
|
|
|
3175
|
-
|
|
3199
|
+
const advanceCursor = () => {
|
|
3200
|
+
if (frameId !== null && frameId !== "") {
|
|
3201
|
+
onEvent({ type: "cursor", id: frameId });
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
|
|
3205
|
+
// A frame with an `id:` but no `data:` (e.g. a bare keepalive line) is
|
|
3206
|
+
// still a received durable row, so advance the cursor past it.
|
|
3207
|
+
if (!data) {
|
|
3208
|
+
advanceCursor();
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3176
3211
|
let payload: any;
|
|
3177
3212
|
try {
|
|
3178
3213
|
payload = JSON.parse(data);
|
|
3179
3214
|
} catch (error) {
|
|
3215
|
+
// Parse failure: the frame was NOT applied. Do NOT advance the cursor
|
|
3216
|
+
// so a reconnect re-fetches this row.
|
|
3180
3217
|
onEvent({
|
|
3181
3218
|
type: "error",
|
|
3182
3219
|
error:
|
|
@@ -3209,7 +3246,10 @@ export class AgentWidgetClient {
|
|
|
3209
3246
|
if (assistantMessageRef.current && assistantMessageRef.current !== assistantMessage) {
|
|
3210
3247
|
assistantMessage = assistantMessageRef.current;
|
|
3211
3248
|
}
|
|
3212
|
-
if (handled)
|
|
3249
|
+
if (handled) {
|
|
3250
|
+
advanceCursor();
|
|
3251
|
+
continue; // Skip default handling if custom handler processed it
|
|
3252
|
+
}
|
|
3213
3253
|
}
|
|
3214
3254
|
|
|
3215
3255
|
// The wire is the wire vocabulary; the handler consumes it
|
|
@@ -3217,6 +3257,7 @@ export class AgentWidgetClient {
|
|
|
3217
3257
|
// drains straight through.
|
|
3218
3258
|
seqReadyQueue.push({ payloadType, payload });
|
|
3219
3259
|
drainReadyQueue();
|
|
3260
|
+
advanceCursor();
|
|
3220
3261
|
}
|
|
3221
3262
|
}
|
|
3222
3263
|
|
package/src/defaults.ts
CHANGED
|
@@ -129,8 +129,16 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
129
129
|
label: "",
|
|
130
130
|
},
|
|
131
131
|
scrollBehavior: {
|
|
132
|
-
|
|
132
|
+
// ChatGPT-style default: pin the just-sent message near the top and let
|
|
133
|
+
// the reply stream into the space below. Restore the old "stick to the
|
|
134
|
+
// bottom" behavior with `mode: "follow"`.
|
|
135
|
+
mode: "anchor-top",
|
|
133
136
|
anchorTopOffset: 16,
|
|
137
|
+
// Surface the unread count + "streaming below" hint while pinned, so the
|
|
138
|
+
// reader still sees activity arriving off-screen under the pinned turn.
|
|
139
|
+
// (Suppressed by default historically; opted on alongside the anchor-top
|
|
140
|
+
// default so the default UX keeps the affordance.)
|
|
141
|
+
showActivityWhilePinned: true,
|
|
134
142
|
},
|
|
135
143
|
toolCallDisplay: {
|
|
136
144
|
collapsedMode: "tool-call",
|
|
@@ -311,12 +311,15 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
311
311
|
seq: number;
|
|
312
312
|
type: "approval_complete";
|
|
313
313
|
}) | ({
|
|
314
|
+
awaitReason?: string;
|
|
314
315
|
awaitedAt?: string;
|
|
316
|
+
crawlId?: string;
|
|
315
317
|
executionId: string;
|
|
316
318
|
origin?: "webmcp" | "sdk";
|
|
317
319
|
pageOrigin?: string;
|
|
318
320
|
parameters?: Record<string, unknown>;
|
|
319
321
|
seq: number;
|
|
322
|
+
stepId?: string;
|
|
320
323
|
toolCallId?: string;
|
|
321
324
|
toolId?: string;
|
|
322
325
|
toolName?: string;
|
|
@@ -406,13 +409,16 @@ export type RuntypeFlowSSEEvent = {
|
|
|
406
409
|
type: "flow_error";
|
|
407
410
|
upgradeUrl?: string;
|
|
408
411
|
}) | ({
|
|
412
|
+
awaitReason?: string;
|
|
409
413
|
awaitedAt: string;
|
|
414
|
+
crawlId?: string;
|
|
410
415
|
executionId?: string;
|
|
411
416
|
flowId: string;
|
|
412
417
|
origin?: "webmcp" | "sdk";
|
|
413
418
|
pageOrigin?: string;
|
|
414
419
|
parameters?: Record<string, unknown>;
|
|
415
420
|
seq?: number;
|
|
421
|
+
stepId?: string;
|
|
416
422
|
toolCallId?: string;
|
|
417
423
|
toolId?: string;
|
|
418
424
|
toolName?: string;
|
package/src/index-core.ts
CHANGED
|
@@ -263,6 +263,11 @@ export type {
|
|
|
263
263
|
AgentWidgetStreamAnimationType,
|
|
264
264
|
AgentWidgetStreamAnimationFeature,
|
|
265
265
|
AgentWidgetStreamAnimationPlaceholder,
|
|
266
|
+
AgentWidgetScrollMode,
|
|
267
|
+
AgentWidgetScrollRestorePosition,
|
|
268
|
+
AgentWidgetScrollBehaviorFeature,
|
|
269
|
+
AgentWidgetScrollToBottomFeature,
|
|
270
|
+
AgentWidgetComponentRenderer,
|
|
266
271
|
} from "./types";
|
|
267
272
|
|
|
268
273
|
// Action system types: needed to type the `actionHandlers` / `actionParsers`
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// Wake-listener behavior for durable reconnect (`session-reconnect.ts`). These
|
|
4
|
+
// need a DOM so the `visibilitychange` / `online` listeners actually attach, so
|
|
5
|
+
// this file runs in jsdom (the sibling `reconnect.test.ts` is node-env, where
|
|
6
|
+
// the `typeof document/window` guards skip the listeners entirely).
|
|
7
|
+
//
|
|
8
|
+
// The crux: `online` must short-circuit the backoff sleep even when the tab is
|
|
9
|
+
// backgrounded, while `visibilitychange` must only wake on the show transition.
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
12
|
+
import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
|
|
13
|
+
import { AgentWidgetMessage } from "./types";
|
|
14
|
+
|
|
15
|
+
const enc = new TextEncoder();
|
|
16
|
+
|
|
17
|
+
function sseStream(frames: string[]): ReadableStream<Uint8Array> {
|
|
18
|
+
return new ReadableStream({
|
|
19
|
+
start(controller) {
|
|
20
|
+
for (const f of frames) controller.enqueue(enc.encode(f));
|
|
21
|
+
controller.close();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function frame(
|
|
27
|
+
id: number | null,
|
|
28
|
+
type: string,
|
|
29
|
+
data: Record<string, unknown> = {}
|
|
30
|
+
): string {
|
|
31
|
+
const head = id !== null ? `id: ${id}\n` : "";
|
|
32
|
+
return `${head}event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function waitFor(pred: () => boolean, timeoutMs = 1500): Promise<void> {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
// eslint-disable-next-line no-constant-condition
|
|
38
|
+
while (true) {
|
|
39
|
+
if (pred()) return;
|
|
40
|
+
if (Date.now() - start > timeoutMs) throw new Error("waitFor: timed out");
|
|
41
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setVisibility(state: "visible" | "hidden"): void {
|
|
46
|
+
Object.defineProperty(document, "visibilityState", {
|
|
47
|
+
configurable: true,
|
|
48
|
+
get: () => state,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("AgentWidgetSession - reconnect wake listeners", () => {
|
|
53
|
+
let messages: AgentWidgetMessage[];
|
|
54
|
+
let status: AgentWidgetSessionStatus;
|
|
55
|
+
let reconnectPhases: string[];
|
|
56
|
+
|
|
57
|
+
const baseCallbacks = () => ({
|
|
58
|
+
onMessagesChanged: (m: AgentWidgetMessage[]) => {
|
|
59
|
+
messages = m;
|
|
60
|
+
},
|
|
61
|
+
onStatusChanged: (s: AgentWidgetSessionStatus) => {
|
|
62
|
+
status = s;
|
|
63
|
+
},
|
|
64
|
+
onStreamingChanged: () => {},
|
|
65
|
+
onError: () => {},
|
|
66
|
+
onReconnect: (ev: { phase: string }) => {
|
|
67
|
+
reconnectPhases.push(ev.phase);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const assistantText = () =>
|
|
72
|
+
messages.find((m) => m.role === "assistant" && !m.variant)?.content ?? "";
|
|
73
|
+
|
|
74
|
+
// A session that drops mid-stream, fails its first reconnect attempt (forcing
|
|
75
|
+
// a deliberately long backoff sleep), then succeeds on the second. A prompt
|
|
76
|
+
// 2nd attempt can therefore only mean a wake fired — never the 10s timer.
|
|
77
|
+
function dropThenResumeSession() {
|
|
78
|
+
const initial = sseStream([
|
|
79
|
+
frame(1, "text_delta", {
|
|
80
|
+
id: "text_0",
|
|
81
|
+
delta: "Hello",
|
|
82
|
+
executionId: "exec_1",
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
const resume = sseStream([
|
|
86
|
+
frame(2, "text_delta", {
|
|
87
|
+
id: "text_0",
|
|
88
|
+
delta: " world",
|
|
89
|
+
executionId: "exec_1",
|
|
90
|
+
}),
|
|
91
|
+
frame(3, "execution_complete", { success: true, kind: "agent" }),
|
|
92
|
+
]);
|
|
93
|
+
let calls = 0;
|
|
94
|
+
const session = new AgentWidgetSession(
|
|
95
|
+
{
|
|
96
|
+
apiUrl: "http://x",
|
|
97
|
+
customFetch: async () => ({ ok: true, body: initial }) as any,
|
|
98
|
+
reconnectStream: async () => {
|
|
99
|
+
calls += 1;
|
|
100
|
+
if (calls === 1) return { ok: false } as any;
|
|
101
|
+
return { ok: true, body: resume } as any;
|
|
102
|
+
},
|
|
103
|
+
reconnect: { backoffMs: [10000], maxAttempts: 5 },
|
|
104
|
+
},
|
|
105
|
+
baseCallbacks()
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
session,
|
|
109
|
+
getCalls: () => calls,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
messages = [];
|
|
115
|
+
status = "idle";
|
|
116
|
+
reconnectPhases = [];
|
|
117
|
+
setVisibility("visible");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
setVisibility("visible");
|
|
122
|
+
vi.restoreAllMocks();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("online wakes the backoff immediately even when the tab is backgrounded", async () => {
|
|
126
|
+
const { session, getCalls } = dropThenResumeSession();
|
|
127
|
+
|
|
128
|
+
await session.sendMessage("hi");
|
|
129
|
+
// Wait until the first reconnect attempt has failed and we're sleeping in
|
|
130
|
+
// the (10s) backoff before the second attempt.
|
|
131
|
+
await waitFor(() => status === "paused");
|
|
132
|
+
expect(getCalls()).toBe(1);
|
|
133
|
+
|
|
134
|
+
// Background the tab, then regain connectivity. The visibility guard must
|
|
135
|
+
// NOT suppress the `online` wake.
|
|
136
|
+
setVisibility("hidden");
|
|
137
|
+
window.dispatchEvent(new Event("online"));
|
|
138
|
+
|
|
139
|
+
// Without the fix this times out (the loop would wait the full 10s).
|
|
140
|
+
await waitFor(() => reconnectPhases.includes("resumed"));
|
|
141
|
+
expect(getCalls()).toBe(2);
|
|
142
|
+
expect(assistantText()).toBe("Hello world");
|
|
143
|
+
expect(status).toBe("idle");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("visibilitychange wakes the backoff when the tab becomes visible", async () => {
|
|
147
|
+
const { session, getCalls } = dropThenResumeSession();
|
|
148
|
+
|
|
149
|
+
await session.sendMessage("hi");
|
|
150
|
+
await waitFor(() => status === "paused");
|
|
151
|
+
expect(getCalls()).toBe(1);
|
|
152
|
+
|
|
153
|
+
// Tab brought back to the foreground: the show transition should wake.
|
|
154
|
+
setVisibility("visible");
|
|
155
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
156
|
+
|
|
157
|
+
await waitFor(() => reconnectPhases.includes("resumed"));
|
|
158
|
+
expect(getCalls()).toBe(2);
|
|
159
|
+
expect(assistantText()).toBe("Hello world");
|
|
160
|
+
expect(status).toBe("idle");
|
|
161
|
+
});
|
|
162
|
+
});
|