@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.
Files changed (46) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-C6tFDxKy.d.cts → types-8RICZWQe.d.cts} +6 -0
  4. package/dist/animations/{types-C6tFDxKy.d.ts → types-8RICZWQe.d.ts} +6 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/chunk-DFBSCFYN.js +1 -0
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/index.cjs +50 -50
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +286 -11
  13. package/dist/index.d.ts +286 -11
  14. package/dist/index.global.js +39 -39
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +51 -51
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js.map +1 -1
  19. package/dist/markdown-parsers-entry-NVFT3TE6.js +1 -0
  20. package/dist/runtype-tts-entry-HFUV2UF7.js +1 -0
  21. package/dist/session-reconnect-U77QFUR7.js +1 -0
  22. package/dist/smart-dom-reader.d.cts +147 -8
  23. package/dist/smart-dom-reader.d.ts +147 -8
  24. package/dist/theme-editor-preview.cjs +48 -48
  25. package/dist/theme-editor-preview.d.cts +187 -9
  26. package/dist/theme-editor-preview.d.ts +187 -9
  27. package/dist/theme-editor-preview.js +48 -48
  28. package/dist/theme-editor.cjs +1 -1
  29. package/dist/theme-editor.d.cts +147 -8
  30. package/dist/theme-editor.d.ts +147 -8
  31. package/dist/theme-editor.js +1 -1
  32. package/package.json +2 -2
  33. package/src/client.ts +48 -7
  34. package/src/defaults.ts +9 -1
  35. package/src/generated/runtype-openapi-contract.ts +6 -0
  36. package/src/index-core.ts +5 -0
  37. package/src/reconnect-wake.test.ts +162 -0
  38. package/src/reconnect.test.ts +430 -0
  39. package/src/session-reconnect.ts +282 -0
  40. package/src/session.ts +408 -5
  41. package/src/types.ts +165 -9
  42. package/src/ui.scroll-additive.test.ts +451 -0
  43. package/src/ui.scroll.test.ts +8 -2
  44. package/src/ui.stream-animation-update.test.ts +99 -0
  45. package/src/ui.ts +371 -41
  46. package/src/utils/constants.ts +3 -1
@@ -0,0 +1 @@
1
+ import{Marked as o}from"marked";import f from"dompurify";export{f as DOMPurify,o as Marked};
@@ -0,0 +1 @@
1
+ import{a as c}from"./chunk-DFBSCFYN.js";function g(o){return o.replace(/\/+$/,"")}var m=class{constructor(e){this.opts=e;this.id="runtype-tts";this.supportsPause=!0;this.player=null;this.playerPromise=null;this.generation=0}ensurePlayer(){var e,t;return(t=this.playerPromise)!=null?t:this.playerPromise=Promise.resolve(this.opts.createPlaybackEngine?this.opts.createPlaybackEngine():new c(24e3,{prebufferMs:(e=this.opts.prebufferMs)!=null?e:200})).then(r=>this.player=r)}speak(e,t){let r=++this.generation;this.run(r,e,t)}async run(e,t,r){var i,a,p,h;try{let n=await this.ensurePlayer();if(e!==this.generation)return;n.flush(),n.resume(),n.onStarted(()=>{var s;e===this.generation&&((s=r.onStart)==null||s.call(r))}),n.onFinished(()=>{var s;e===this.generation&&((s=r.onEnd)==null||s.call(r))});let u=`${g(this.opts.host)}/v1/agents/${encodeURIComponent(this.opts.agentId)}/speak`,y=await fetch(u,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.opts.clientToken}`},body:JSON.stringify({text:t.text,voice:(i=t.voice)!=null?i:this.opts.voice,format:"pcm"})});if(e!==this.generation)return;if(!y.ok||!y.body)throw new Error(await S(y));let v=y.body.getReader();for(;;){let{done:s,value:d}=await v.read();if(e!==this.generation){await v.cancel().catch(()=>{});return}if(s)break;d&&d.byteLength>0&&n.enqueue(d)}n.markStreamEnd()}catch(n){if(e!==this.generation)return;let u=n instanceof Error?n:new Error(String(n));(p=(a=this.opts).onError)==null||p.call(a,u),(h=r.onError)==null||h.call(r,u)}}pause(){var e;(e=this.player)==null||e.pause()}resume(){var e;(e=this.player)==null||e.resume()}stop(){var e;this.generation++,(e=this.player)==null||e.flush()}destroy(){var e;this.generation++,(e=this.player)==null||e.destroy(),this.player=null,this.playerPromise=null}};async function S(o){var e,t;try{let r=await o.json();return r.detail?`${(e=r.error)!=null?e:`Runtype TTS ${o.status}`}: ${r.detail}`:(t=r.error)!=null?t:`Runtype TTS request failed (${o.status})`}catch{return`Runtype TTS request failed (${o.status})`}}var f=class{constructor(e,t,r={}){this.primary=e;this.fallback=t;this.options=r;this.id="fallback";this.active=e}get supportsPause(){return this.active.supportsPause}speak(e,t){this.active=this.primary;let r=!1;this.primary.speak(e,{onStart:()=>{var i;r=!0,(i=t.onStart)==null||i.call(t)},onEnd:()=>{var i;return(i=t.onEnd)==null?void 0:i.call(t)},onError:i=>{var a,p,h;if(r){(a=t.onError)==null||a.call(t,i);return}(h=(p=this.options).onFallback)==null||h.call(p,i),this.active=this.fallback,this.fallback.speak(e,t)}})}pause(){this.active.pause()}resume(){this.active.resume()}stop(){this.active.stop()}destroy(){var e,t,r,i;(t=(e=this.primary).destroy)==null||t.call(e),(i=(r=this.fallback).destroy)==null||i.call(r)}};export{f as FallbackSpeechEngine,m as RuntypeSpeechEngine};
@@ -0,0 +1 @@
1
+ var k=[1e3,2e3,4e3,8e3,8e3];function T(e){let t=0,r=null,i=null,g=!1,b=()=>{if(i&&(clearTimeout(i),i=null),r){let n=r;r=null,n()}},v=()=>{let n=e.getStatus();n!=="resuming"&&n!=="paused"||b()},p=()=>{typeof document!="undefined"&&document.visibilityState==="hidden"||v()},R=()=>{v()},C=()=>{g||(typeof document!="undefined"&&document.addEventListener("visibilitychange",p),typeof window!="undefined"&&window.addEventListener("online",R),g=!0)},m=()=>{g&&(typeof document!="undefined"&&document.removeEventListener("visibilitychange",p),typeof window!="undefined"&&window.removeEventListener("online",R),g=!1)},M=n=>new Promise(s=>{r=s,i=setTimeout(()=>{i=null,r=null,s()},n)}),E=()=>{var d;let n=e.getResumable();e.clearResumable(),e.setReconnecting(!1),t=0,m(),e.setAbortController(null);let s=!1;for(let c of e.getMessages())c.streaming&&(c.streaming=!1,s=!0);let l=e.buildErrorContent("Connection lost and the response could not be resumed.");l?e.appendMessage({id:`reconnect-failed-${(d=n==null?void 0:n.executionId)!=null?d:e.nextSequence()}`,role:"assistant",content:l,createdAt:new Date().toISOString(),streaming:!1,sequence:e.nextSequence()}):s&&e.notifyMessagesChanged(),e.setStreaming(!1),e.setStatus("idle"),e.onError(new Error("Durable session reconnect failed."))},x=async()=>{var d,c,y,S,w,A;let n=(c=(d=e.config.reconnect)==null?void 0:d.backoffMs)!=null?c:k,s=(S=(y=e.config.reconnect)==null?void 0:y.maxAttempts)!=null?S:n.length,l=e.config.reconnectStream;if(!l){e.setReconnecting(!1);return}for(;e.getResumable()&&t<s;){t+=1,e.setStatus("resuming");let o=e.getResumable(),I=o.lastEventId,u=new AbortController;e.setAbortController(u),e.emitReconnect({phase:"resuming",handle:o,attempt:t});let f=null;try{let a=await l({executionId:o.executionId,after:o.lastEventId,signal:u.signal});a&&a.ok&&a.body&&(f=a.body)}catch{f=null}if(u.signal.aborted)return;if(f){let a=(w=e.getMessages().find(W=>W.id===o.assistantMessageId))==null?void 0:w.content,L=typeof a=="string"?a:"";try{await e.resumeConnect(f,o.assistantMessageId,L)}catch{}if(u.signal.aborted)return;if(!e.getResumable()){e.setReconnecting(!1),t=0,m(),e.emitReconnect({phase:"resumed",handle:o});return}e.getResumable().lastEventId!==I&&(t=0)}if(e.getResumable()&&t<s&&(e.setStatus("paused"),await M((A=n[Math.min(t-1,n.length-1)])!=null?A:1e3),u.signal.aborted))return}e.getResumable()&&E()};return{begin(){t=0,C(),x()},teardown(){i&&(clearTimeout(i),i=null),r=null,t=0,m()},wake:b}}export{T as createReconnectController};
@@ -1322,12 +1322,15 @@ type RuntypeExecutionStreamEvent = ({
1322
1322
  seq: number;
1323
1323
  type: "approval_complete";
1324
1324
  }) | ({
1325
+ awaitReason?: string;
1325
1326
  awaitedAt?: string;
1327
+ crawlId?: string;
1326
1328
  executionId: string;
1327
1329
  origin?: "webmcp" | "sdk";
1328
1330
  pageOrigin?: string;
1329
1331
  parameters?: Record<string, unknown>;
1330
1332
  seq: number;
1333
+ stepId?: string;
1331
1334
  toolCallId?: string;
1332
1335
  toolId?: string;
1333
1336
  toolName?: string;
@@ -1416,13 +1419,16 @@ type RuntypeFlowSSEEvent = {
1416
1419
  type: "flow_error";
1417
1420
  upgradeUrl?: string;
1418
1421
  }) | ({
1422
+ awaitReason?: string;
1419
1423
  awaitedAt: string;
1424
+ crawlId?: string;
1420
1425
  executionId?: string;
1421
1426
  flowId: string;
1422
1427
  origin?: "webmcp" | "sdk";
1423
1428
  pageOrigin?: string;
1424
1429
  parameters?: Record<string, unknown>;
1425
1430
  seq?: number;
1431
+ stepId?: string;
1426
1432
  toolCallId?: string;
1427
1433
  toolId?: string;
1428
1434
  toolName?: string;
@@ -1962,6 +1968,27 @@ type AgentWidgetWebMcpConfig = {
1962
1968
  */
1963
1969
  onConfirm?: WebMcpConfirmHandler;
1964
1970
  };
1971
+ /**
1972
+ * The coordinates needed to resume a durable agent turn after the SSE
1973
+ * connection drops (any resumable, server-persisted execution, e.g. Claude
1974
+ * Managed agents or async/background runs). Held on the session while a
1975
+ * resumable stream is in flight
1976
+ * and surfaced to the host via {@link AgentWidgetConfig.onExecutionState} so it
1977
+ * can persist `{ executionId, lastEventId }` next to its own `conversationId`
1978
+ * for the tab-reload path.
1979
+ *
1980
+ * `conversationId` is intentionally absent: it is host-owned (it lives in the
1981
+ * host's `reconnectStream`/`customFetch` closure; the widget never sees it).
1982
+ */
1983
+ type ResumableHandle = {
1984
+ /** The durable turn to reconnect to (from `agentMetadata.executionId`). */
1985
+ executionId: string;
1986
+ /** Highest SSE `id:` seq applied: the `?after=` reconnect cursor. */
1987
+ lastEventId: string;
1988
+ /** The open assistant message being streamed into, kept open on resume. */
1989
+ assistantMessageId: string;
1990
+ status: 'running';
1991
+ };
1965
1992
  /**
1966
1993
  * Metadata attached to messages created during agent execution.
1967
1994
  */
@@ -2328,25 +2355,74 @@ type AgentWidgetArtifactsFeature = {
2328
2355
  /**
2329
2356
  * How the transcript scrolls while an assistant response streams in.
2330
2357
  *
2331
- * - `"follow"` (default): keep the newest content pinned to the bottom of the
2332
- * viewport, pausing when the user scrolls up and resuming when they return
2333
- * to the bottom.
2334
- * - `"anchor-top"`: on send, scroll the user's message near the top of the
2335
- * viewport and hold it there while the response streams in beneath it
2336
- * (ChatGPT-style). The transcript never auto-scrolls during streaming.
2358
+ * - `"anchor-top"` (default): on send, scroll the user's message near the top
2359
+ * of the viewport and hold it there while the response streams in beneath it
2360
+ * (ChatGPT-style). When a turn has no user send to anchor to (a proactive
2361
+ * greeting, an injected assistant message, a resubmit, or first-load
2362
+ * streaming), it falls back to `"follow"` for that turn so content never
2363
+ * streams in off-screen.
2364
+ * - `"follow"`: keep the newest content pinned to the bottom of the viewport,
2365
+ * pausing when the user scrolls up and resuming when they return to the
2366
+ * bottom. This was the default before 4.x.
2337
2367
  * - `"none"`: never auto-scroll; the scroll-to-bottom affordance is the only
2338
2368
  * way back to the latest content.
2339
2369
  */
2340
2370
  type AgentWidgetScrollMode = "follow" | "anchor-top" | "none";
2371
+ /**
2372
+ * Where the transcript lands when a *saved* conversation is reopened (restored
2373
+ * from the storage adapter / `initialMessages`), as opposed to a fresh send.
2374
+ *
2375
+ * - `"bottom"` (default): jump to the absolute end of the transcript, matching
2376
+ * the historical behavior.
2377
+ * - `"last-user-turn"`: pin the last user message near the top of the viewport
2378
+ * (the last point the reader was actively driving the conversation) so a long
2379
+ * restored thread opens at a readable place rather than at the very bottom.
2380
+ */
2381
+ type AgentWidgetScrollRestorePosition = "bottom" | "last-user-turn";
2341
2382
  type AgentWidgetScrollBehaviorFeature = {
2342
- /** Scroll behavior during streamed responses. @default "follow" */
2383
+ /** Scroll behavior during streamed responses. @default "anchor-top" */
2343
2384
  mode?: AgentWidgetScrollMode;
2344
2385
  /**
2345
2386
  * Gap (px) kept between the anchored user message and the top of the
2346
- * viewport in `"anchor-top"` mode.
2387
+ * viewport in `"anchor-top"` mode. Also used as the gap for
2388
+ * `restorePosition: "last-user-turn"`.
2347
2389
  * @default 16
2348
2390
  */
2349
2391
  anchorTopOffset?: number;
2392
+ /**
2393
+ * Where to land when a saved conversation reopens. Additive and opt-in: the
2394
+ * default preserves the historical "jump to the bottom" behavior.
2395
+ * @default "bottom"
2396
+ */
2397
+ restorePosition?: AgentWidgetScrollRestorePosition;
2398
+ /**
2399
+ * When true, interactions beyond text selection — keyboard navigation
2400
+ * (PageUp/PageDown/Home/End/arrows) inside the transcript and focusing a
2401
+ * link or other interactive element within it — also pause auto-follow in
2402
+ * `"follow"` mode. Treats "the reader is doing something here" as intent to
2403
+ * stay put, not just "the reader dragged a selection". Opt-in; default keeps
2404
+ * only the existing wheel/scroll/selection triggers.
2405
+ * @default false
2406
+ */
2407
+ pauseOnInteraction?: boolean;
2408
+ /**
2409
+ * When true, the scroll-to-bottom affordance also surfaces new-content and
2410
+ * still-streaming activity while the reader is pinned away from the latest
2411
+ * content in `"anchor-top"` mode. Lets the reader know a response is arriving
2412
+ * offscreen below. Defaults on alongside the `"anchor-top"` default; set
2413
+ * `false` to keep the count + streaming hint silent while pinned.
2414
+ * @default true
2415
+ */
2416
+ showActivityWhilePinned?: boolean;
2417
+ /**
2418
+ * When true, Persona maintains a visually-hidden `aria-live="polite"` region
2419
+ * that announces important transcript events — a response starting, a
2420
+ * response finishing, and "N new messages below" while the reader is scrolled
2421
+ * away — at a comfortable cadence (never token-by-token). Opt-in so existing
2422
+ * embeds don't suddenly gain screen-reader announcements.
2423
+ * @default false
2424
+ */
2425
+ announce?: boolean;
2350
2426
  };
2351
2427
  type AgentWidgetScrollToBottomFeature = {
2352
2428
  /**
@@ -3229,6 +3305,10 @@ type AgentWidgetStatusIndicatorConfig = {
3229
3305
  connectingText?: string;
3230
3306
  connectedText?: string;
3231
3307
  errorText?: string;
3308
+ /** Status text while a dropped durable stream is awaiting reconnect. */
3309
+ pausedText?: string;
3310
+ /** Status text while a reconnect attempt is in flight. */
3311
+ resumingText?: string;
3232
3312
  };
3233
3313
  type AgentWidgetVoiceRecognitionConfig = {
3234
3314
  enabled?: boolean;
@@ -5310,6 +5390,65 @@ type AgentWidgetConfig = {
5310
5390
  * ```
5311
5391
  */
5312
5392
  customFetch?: AgentWidgetCustomFetch;
5393
+ /**
5394
+ * Durable-session reconnect transport (host-owned, symmetric to
5395
+ * {@link customFetch}). When a durable agent stream drops mid-turn (any
5396
+ * resumable, server-persisted execution, e.g. Claude Managed agents or
5397
+ * async/background runs), the widget calls this to fetch the read-only
5398
+ * reconnect stream and pipes its body through the normal event pipeline to
5399
+ * resume from where it left off.
5400
+ *
5401
+ * The host closure owns `agentId` / `conversationId` / base URL / auth and
5402
+ * builds the events request, e.g.:
5403
+ *
5404
+ * ```typescript
5405
+ * reconnectStream: ({ executionId, after, signal }) =>
5406
+ * fetch(
5407
+ * `${baseUrl}/v1/agents/${agentId}/executions/${executionId}` +
5408
+ * `/events?conversationId=${conversationId}&after=${after}`,
5409
+ * { headers: { Authorization: `Bearer ${token}`, "X-Persona-Version": ver }, signal }
5410
+ * )
5411
+ * ```
5412
+ *
5413
+ * Resolve with a `text/event-stream` `Response`. Throw or resolve non-ok to
5414
+ * signal "this attempt failed" (the widget backs off and retries, then gives
5415
+ * up after the bounded attempts). Reconnect only ever arms on the durable
5416
+ * lane (streams carrying SSE `id:` lines); without this hook a drop finalizes
5417
+ * exactly as before.
5418
+ */
5419
+ reconnectStream?: (ctx: {
5420
+ executionId: string;
5421
+ after: string;
5422
+ signal: AbortSignal;
5423
+ }) => Promise<Response>;
5424
+ /**
5425
+ * Tuning for the auto-reconnect backoff. Defaults to ~5 attempts over ~30s
5426
+ * (`backoffMs: [1000, 2000, 4000, 8000, 8000]`).
5427
+ */
5428
+ reconnect?: {
5429
+ /** Max reconnect attempts before giving up and finalizing. @default backoffMs.length */
5430
+ maxAttempts?: number;
5431
+ /** Per-attempt delay (ms) before each retry. @default [1000, 2000, 4000, 8000, 8000] */
5432
+ backoffMs?: number[];
5433
+ };
5434
+ /**
5435
+ * Called whenever the durable resume handle changes: created when a durable
5436
+ * turn starts streaming, updated as the cursor advances (throttled), and
5437
+ * `null` when the turn finishes, errors, or is torn down. Persist
5438
+ * `{ executionId, lastEventId }` next to your `conversationId` and pass it
5439
+ * back via {@link resume} on the next mount to survive a tab reload.
5440
+ */
5441
+ onExecutionState?: (handle: ResumableHandle | null) => void;
5442
+ /**
5443
+ * Resume a durable turn on boot (tab-reload path). When present alongside
5444
+ * {@link reconnectStream}, the widget enters `resuming` immediately on mount
5445
+ * and reconnects from `after`, replaying everything past the cursor into the
5446
+ * restored conversation.
5447
+ */
5448
+ resume?: {
5449
+ executionId: string;
5450
+ after: string;
5451
+ };
5313
5452
  /**
5314
5453
  * Custom SSE event parser for non-standard streaming response formats.
5315
5454
  *
@@ -1322,12 +1322,15 @@ type RuntypeExecutionStreamEvent = ({
1322
1322
  seq: number;
1323
1323
  type: "approval_complete";
1324
1324
  }) | ({
1325
+ awaitReason?: string;
1325
1326
  awaitedAt?: string;
1327
+ crawlId?: string;
1326
1328
  executionId: string;
1327
1329
  origin?: "webmcp" | "sdk";
1328
1330
  pageOrigin?: string;
1329
1331
  parameters?: Record<string, unknown>;
1330
1332
  seq: number;
1333
+ stepId?: string;
1331
1334
  toolCallId?: string;
1332
1335
  toolId?: string;
1333
1336
  toolName?: string;
@@ -1416,13 +1419,16 @@ type RuntypeFlowSSEEvent = {
1416
1419
  type: "flow_error";
1417
1420
  upgradeUrl?: string;
1418
1421
  }) | ({
1422
+ awaitReason?: string;
1419
1423
  awaitedAt: string;
1424
+ crawlId?: string;
1420
1425
  executionId?: string;
1421
1426
  flowId: string;
1422
1427
  origin?: "webmcp" | "sdk";
1423
1428
  pageOrigin?: string;
1424
1429
  parameters?: Record<string, unknown>;
1425
1430
  seq?: number;
1431
+ stepId?: string;
1426
1432
  toolCallId?: string;
1427
1433
  toolId?: string;
1428
1434
  toolName?: string;
@@ -1962,6 +1968,27 @@ type AgentWidgetWebMcpConfig = {
1962
1968
  */
1963
1969
  onConfirm?: WebMcpConfirmHandler;
1964
1970
  };
1971
+ /**
1972
+ * The coordinates needed to resume a durable agent turn after the SSE
1973
+ * connection drops (any resumable, server-persisted execution, e.g. Claude
1974
+ * Managed agents or async/background runs). Held on the session while a
1975
+ * resumable stream is in flight
1976
+ * and surfaced to the host via {@link AgentWidgetConfig.onExecutionState} so it
1977
+ * can persist `{ executionId, lastEventId }` next to its own `conversationId`
1978
+ * for the tab-reload path.
1979
+ *
1980
+ * `conversationId` is intentionally absent: it is host-owned (it lives in the
1981
+ * host's `reconnectStream`/`customFetch` closure; the widget never sees it).
1982
+ */
1983
+ type ResumableHandle = {
1984
+ /** The durable turn to reconnect to (from `agentMetadata.executionId`). */
1985
+ executionId: string;
1986
+ /** Highest SSE `id:` seq applied: the `?after=` reconnect cursor. */
1987
+ lastEventId: string;
1988
+ /** The open assistant message being streamed into, kept open on resume. */
1989
+ assistantMessageId: string;
1990
+ status: 'running';
1991
+ };
1965
1992
  /**
1966
1993
  * Metadata attached to messages created during agent execution.
1967
1994
  */
@@ -2328,25 +2355,74 @@ type AgentWidgetArtifactsFeature = {
2328
2355
  /**
2329
2356
  * How the transcript scrolls while an assistant response streams in.
2330
2357
  *
2331
- * - `"follow"` (default): keep the newest content pinned to the bottom of the
2332
- * viewport, pausing when the user scrolls up and resuming when they return
2333
- * to the bottom.
2334
- * - `"anchor-top"`: on send, scroll the user's message near the top of the
2335
- * viewport and hold it there while the response streams in beneath it
2336
- * (ChatGPT-style). The transcript never auto-scrolls during streaming.
2358
+ * - `"anchor-top"` (default): on send, scroll the user's message near the top
2359
+ * of the viewport and hold it there while the response streams in beneath it
2360
+ * (ChatGPT-style). When a turn has no user send to anchor to (a proactive
2361
+ * greeting, an injected assistant message, a resubmit, or first-load
2362
+ * streaming), it falls back to `"follow"` for that turn so content never
2363
+ * streams in off-screen.
2364
+ * - `"follow"`: keep the newest content pinned to the bottom of the viewport,
2365
+ * pausing when the user scrolls up and resuming when they return to the
2366
+ * bottom. This was the default before 4.x.
2337
2367
  * - `"none"`: never auto-scroll; the scroll-to-bottom affordance is the only
2338
2368
  * way back to the latest content.
2339
2369
  */
2340
2370
  type AgentWidgetScrollMode = "follow" | "anchor-top" | "none";
2371
+ /**
2372
+ * Where the transcript lands when a *saved* conversation is reopened (restored
2373
+ * from the storage adapter / `initialMessages`), as opposed to a fresh send.
2374
+ *
2375
+ * - `"bottom"` (default): jump to the absolute end of the transcript, matching
2376
+ * the historical behavior.
2377
+ * - `"last-user-turn"`: pin the last user message near the top of the viewport
2378
+ * (the last point the reader was actively driving the conversation) so a long
2379
+ * restored thread opens at a readable place rather than at the very bottom.
2380
+ */
2381
+ type AgentWidgetScrollRestorePosition = "bottom" | "last-user-turn";
2341
2382
  type AgentWidgetScrollBehaviorFeature = {
2342
- /** Scroll behavior during streamed responses. @default "follow" */
2383
+ /** Scroll behavior during streamed responses. @default "anchor-top" */
2343
2384
  mode?: AgentWidgetScrollMode;
2344
2385
  /**
2345
2386
  * Gap (px) kept between the anchored user message and the top of the
2346
- * viewport in `"anchor-top"` mode.
2387
+ * viewport in `"anchor-top"` mode. Also used as the gap for
2388
+ * `restorePosition: "last-user-turn"`.
2347
2389
  * @default 16
2348
2390
  */
2349
2391
  anchorTopOffset?: number;
2392
+ /**
2393
+ * Where to land when a saved conversation reopens. Additive and opt-in: the
2394
+ * default preserves the historical "jump to the bottom" behavior.
2395
+ * @default "bottom"
2396
+ */
2397
+ restorePosition?: AgentWidgetScrollRestorePosition;
2398
+ /**
2399
+ * When true, interactions beyond text selection — keyboard navigation
2400
+ * (PageUp/PageDown/Home/End/arrows) inside the transcript and focusing a
2401
+ * link or other interactive element within it — also pause auto-follow in
2402
+ * `"follow"` mode. Treats "the reader is doing something here" as intent to
2403
+ * stay put, not just "the reader dragged a selection". Opt-in; default keeps
2404
+ * only the existing wheel/scroll/selection triggers.
2405
+ * @default false
2406
+ */
2407
+ pauseOnInteraction?: boolean;
2408
+ /**
2409
+ * When true, the scroll-to-bottom affordance also surfaces new-content and
2410
+ * still-streaming activity while the reader is pinned away from the latest
2411
+ * content in `"anchor-top"` mode. Lets the reader know a response is arriving
2412
+ * offscreen below. Defaults on alongside the `"anchor-top"` default; set
2413
+ * `false` to keep the count + streaming hint silent while pinned.
2414
+ * @default true
2415
+ */
2416
+ showActivityWhilePinned?: boolean;
2417
+ /**
2418
+ * When true, Persona maintains a visually-hidden `aria-live="polite"` region
2419
+ * that announces important transcript events — a response starting, a
2420
+ * response finishing, and "N new messages below" while the reader is scrolled
2421
+ * away — at a comfortable cadence (never token-by-token). Opt-in so existing
2422
+ * embeds don't suddenly gain screen-reader announcements.
2423
+ * @default false
2424
+ */
2425
+ announce?: boolean;
2350
2426
  };
2351
2427
  type AgentWidgetScrollToBottomFeature = {
2352
2428
  /**
@@ -3229,6 +3305,10 @@ type AgentWidgetStatusIndicatorConfig = {
3229
3305
  connectingText?: string;
3230
3306
  connectedText?: string;
3231
3307
  errorText?: string;
3308
+ /** Status text while a dropped durable stream is awaiting reconnect. */
3309
+ pausedText?: string;
3310
+ /** Status text while a reconnect attempt is in flight. */
3311
+ resumingText?: string;
3232
3312
  };
3233
3313
  type AgentWidgetVoiceRecognitionConfig = {
3234
3314
  enabled?: boolean;
@@ -5310,6 +5390,65 @@ type AgentWidgetConfig = {
5310
5390
  * ```
5311
5391
  */
5312
5392
  customFetch?: AgentWidgetCustomFetch;
5393
+ /**
5394
+ * Durable-session reconnect transport (host-owned, symmetric to
5395
+ * {@link customFetch}). When a durable agent stream drops mid-turn (any
5396
+ * resumable, server-persisted execution, e.g. Claude Managed agents or
5397
+ * async/background runs), the widget calls this to fetch the read-only
5398
+ * reconnect stream and pipes its body through the normal event pipeline to
5399
+ * resume from where it left off.
5400
+ *
5401
+ * The host closure owns `agentId` / `conversationId` / base URL / auth and
5402
+ * builds the events request, e.g.:
5403
+ *
5404
+ * ```typescript
5405
+ * reconnectStream: ({ executionId, after, signal }) =>
5406
+ * fetch(
5407
+ * `${baseUrl}/v1/agents/${agentId}/executions/${executionId}` +
5408
+ * `/events?conversationId=${conversationId}&after=${after}`,
5409
+ * { headers: { Authorization: `Bearer ${token}`, "X-Persona-Version": ver }, signal }
5410
+ * )
5411
+ * ```
5412
+ *
5413
+ * Resolve with a `text/event-stream` `Response`. Throw or resolve non-ok to
5414
+ * signal "this attempt failed" (the widget backs off and retries, then gives
5415
+ * up after the bounded attempts). Reconnect only ever arms on the durable
5416
+ * lane (streams carrying SSE `id:` lines); without this hook a drop finalizes
5417
+ * exactly as before.
5418
+ */
5419
+ reconnectStream?: (ctx: {
5420
+ executionId: string;
5421
+ after: string;
5422
+ signal: AbortSignal;
5423
+ }) => Promise<Response>;
5424
+ /**
5425
+ * Tuning for the auto-reconnect backoff. Defaults to ~5 attempts over ~30s
5426
+ * (`backoffMs: [1000, 2000, 4000, 8000, 8000]`).
5427
+ */
5428
+ reconnect?: {
5429
+ /** Max reconnect attempts before giving up and finalizing. @default backoffMs.length */
5430
+ maxAttempts?: number;
5431
+ /** Per-attempt delay (ms) before each retry. @default [1000, 2000, 4000, 8000, 8000] */
5432
+ backoffMs?: number[];
5433
+ };
5434
+ /**
5435
+ * Called whenever the durable resume handle changes: created when a durable
5436
+ * turn starts streaming, updated as the cursor advances (throttled), and
5437
+ * `null` when the turn finishes, errors, or is torn down. Persist
5438
+ * `{ executionId, lastEventId }` next to your `conversationId` and pass it
5439
+ * back via {@link resume} on the next mount to survive a tab reload.
5440
+ */
5441
+ onExecutionState?: (handle: ResumableHandle | null) => void;
5442
+ /**
5443
+ * Resume a durable turn on boot (tab-reload path). When present alongside
5444
+ * {@link reconnectStream}, the widget enters `resuming` immediately on mount
5445
+ * and reconnects from `after`, replaying everything past the cursor into the
5446
+ * restored conversation.
5447
+ */
5448
+ resume?: {
5449
+ executionId: string;
5450
+ after: string;
5451
+ };
5313
5452
  /**
5314
5453
  * Custom SSE event parser for non-standard streaming response formats.
5315
5454
  *