@runtypelabs/persona 1.44.2 → 1.46.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/src/session.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  AgentWidgetConfig,
4
4
  AgentWidgetEvent,
5
5
  AgentWidgetMessage,
6
+ AgentWidgetApproval,
6
7
  AgentExecutionState,
7
8
  ClientSession,
8
9
  ContentPart,
@@ -540,6 +541,125 @@ export class AgentWidgetSession {
540
541
  }
541
542
  }
542
543
 
544
+ /**
545
+ * Connect an external SSE stream (e.g. from an approval endpoint) and
546
+ * process it through the SDK's native event pipeline.
547
+ */
548
+ public async connectStream(
549
+ stream: ReadableStream<Uint8Array>,
550
+ options?: { assistantMessageId?: string }
551
+ ): Promise<void> {
552
+ if (this.streaming) return;
553
+ this.abortController?.abort();
554
+ this.setStreaming(true);
555
+
556
+ try {
557
+ await this.client.processStream(
558
+ stream,
559
+ this.handleEvent,
560
+ options?.assistantMessageId
561
+ );
562
+ } catch (error) {
563
+ this.setStatus("error");
564
+ this.setStreaming(false);
565
+ this.abortController = null;
566
+ this.callbacks.onError?.(
567
+ error instanceof Error ? error : new Error(String(error))
568
+ );
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Resolve a tool approval request (approve or deny).
574
+ * Updates the approval message status, calls the API (or custom onDecision),
575
+ * and pipes the response stream through connectStream().
576
+ */
577
+ public async resolveApproval(
578
+ approval: AgentWidgetApproval,
579
+ decision: 'approved' | 'denied'
580
+ ): Promise<void> {
581
+ // 1. Update approval message status immediately for responsive UI
582
+ const approvalMessageId = `approval-${approval.id}`;
583
+ const updatedApproval: AgentWidgetApproval = {
584
+ ...approval,
585
+ status: decision,
586
+ resolvedAt: Date.now(),
587
+ };
588
+ const updatedMessage: AgentWidgetMessage = {
589
+ id: approvalMessageId,
590
+ role: "assistant",
591
+ content: "",
592
+ createdAt: new Date().toISOString(),
593
+ streaming: false,
594
+ variant: "approval",
595
+ approval: updatedApproval,
596
+ };
597
+ this.upsertMessage(updatedMessage);
598
+
599
+ // 2. Call onDecision callback if provided, otherwise use client.resolveApproval()
600
+ const approvalConfig = this.config.approval;
601
+ const onDecision = approvalConfig && typeof approvalConfig === 'object' ? approvalConfig.onDecision : undefined;
602
+
603
+ try {
604
+ let response: Response | ReadableStream<Uint8Array> | void;
605
+
606
+ if (onDecision) {
607
+ response = await onDecision(
608
+ {
609
+ approvalId: approval.id,
610
+ executionId: approval.executionId,
611
+ agentId: approval.agentId,
612
+ toolName: approval.toolName,
613
+ },
614
+ decision
615
+ );
616
+ } else {
617
+ response = await this.client.resolveApproval(
618
+ {
619
+ agentId: approval.agentId,
620
+ executionId: approval.executionId,
621
+ approvalId: approval.id,
622
+ },
623
+ decision
624
+ );
625
+ }
626
+
627
+ // 3. Pipe through connectStream if we got a response with a body
628
+ if (response) {
629
+ let stream: ReadableStream<Uint8Array> | null = null;
630
+ if (response instanceof Response) {
631
+ if (!response.ok) {
632
+ const errorData = await response.json().catch(() => null);
633
+ throw new Error(
634
+ errorData?.error ?? `Approval request failed: ${response.status}`
635
+ );
636
+ }
637
+ stream = response.body;
638
+ } else if (response instanceof ReadableStream) {
639
+ stream = response;
640
+ }
641
+
642
+ if (stream) {
643
+ await this.connectStream(stream);
644
+ } else if (decision === 'denied') {
645
+ // No stream body for denied — inject a denial message
646
+ this.appendMessage({
647
+ id: `denial-${approval.id}`,
648
+ role: "assistant",
649
+ content: "Tool execution was denied by user.",
650
+ createdAt: new Date().toISOString(),
651
+ streaming: false,
652
+ sequence: this.nextSequence(),
653
+ });
654
+ }
655
+ }
656
+ } catch (error) {
657
+ this.callbacks.onError?.(
658
+ error instanceof Error ? error : new Error(String(error))
659
+ );
660
+ }
661
+ }
662
+
543
663
  public cancel() {
544
664
  this.abortController?.abort();
545
665
  this.abortController = null;
@@ -1817,3 +1817,71 @@
1817
1817
  .tvw-feedback-shake {
1818
1818
  animation: tvw-feedback-shake 0.5s ease-in-out;
1819
1819
  }
1820
+
1821
+ /* ============================================
1822
+ Tool & Reasoning Bubble Theme Styles
1823
+ ============================================ */
1824
+
1825
+ /* Content areas: replace ghost tvw-bg-gray-50 / tvw-border-gray-200 */
1826
+ .vanilla-tool-bubble .tvw-border-t,
1827
+ .vanilla-reasoning-bubble .tvw-border-t {
1828
+ border-top-color: var(--cw-border, #e5e7eb);
1829
+ background-color: var(--cw-container, #f9fafb);
1830
+ }
1831
+
1832
+ /* Tool bubble code blocks: replace ghost tvw-bg-white / tvw-border-gray-100 */
1833
+ .vanilla-tool-bubble pre {
1834
+ background-color: var(--cw-surface, #ffffff);
1835
+ border-color: var(--cw-border, #f1f5f9);
1836
+ }
1837
+
1838
+ /* Collapsible header hover (tool + reasoning) */
1839
+ .vanilla-tool-bubble button[data-expand-header],
1840
+ .vanilla-reasoning-bubble button[data-expand-header] {
1841
+ transition: background-color 0.15s ease;
1842
+ }
1843
+ .vanilla-tool-bubble button[data-expand-header]:hover,
1844
+ .vanilla-reasoning-bubble button[data-expand-header]:hover {
1845
+ background-color: var(--cw-container, #f8fafc);
1846
+ }
1847
+
1848
+ /* ============================================
1849
+ Approval Bubble Theme Styles
1850
+ ============================================ */
1851
+
1852
+ /* Approval bubble defaults (overridden by inline styles when config exists) */
1853
+ .vanilla-approval-bubble {
1854
+ background-color: var(--cw-surface, #ffffff);
1855
+ border-color: var(--cw-border, #e5e7eb);
1856
+ }
1857
+
1858
+ /* Approval status badges — rgba() so they work on any surface */
1859
+ .tvw-approval-badge-approved {
1860
+ background-color: rgba(22, 163, 74, 0.12);
1861
+ color: #16a34a;
1862
+ }
1863
+ .tvw-approval-badge-denied {
1864
+ background-color: rgba(220, 38, 38, 0.12);
1865
+ color: #dc2626;
1866
+ }
1867
+ .tvw-approval-badge-timeout {
1868
+ background-color: rgba(202, 138, 4, 0.12);
1869
+ color: #ca8a04;
1870
+ }
1871
+
1872
+ /* Approval button hover/active states */
1873
+ .vanilla-approval-bubble [data-approval-action] {
1874
+ transition: opacity 0.15s ease, transform 0.1s ease, background-color 0.15s ease;
1875
+ }
1876
+ .vanilla-approval-bubble [data-approval-action="approve"]:hover {
1877
+ opacity: 0.85;
1878
+ }
1879
+ .vanilla-approval-bubble [data-approval-action="approve"]:active {
1880
+ transform: scale(0.97);
1881
+ }
1882
+ .vanilla-approval-bubble [data-approval-action="deny"]:hover {
1883
+ background-color: rgba(220, 38, 38, 0.08);
1884
+ }
1885
+ .vanilla-approval-bubble [data-approval-action="deny"]:active {
1886
+ transform: scale(0.97);
1887
+ }
package/src/types.ts CHANGED
@@ -357,6 +357,8 @@ export type AgentWidgetControllerEventMap = {
357
357
  "message:copy": AgentWidgetMessage;
358
358
  "eventStream:opened": { timestamp: number };
359
359
  "eventStream:closed": { timestamp: number };
360
+ "approval:requested": { approval: AgentWidgetApproval; message: AgentWidgetMessage };
361
+ "approval:resolved": { approval: AgentWidgetApproval; decision: string };
360
362
  };
361
363
 
362
364
  export type AgentWidgetFeatureFlags = {
@@ -693,6 +695,48 @@ export type AgentWidgetVoiceRecognitionConfig = {
693
695
  autoResume?: boolean | "assistant";
694
696
  };
695
697
 
698
+ /**
699
+ * Configuration for tool approval bubbles.
700
+ * Controls styling, labels, and behavior of the approval UI.
701
+ */
702
+ export type AgentWidgetApprovalConfig = {
703
+ /** Background color of the approval bubble */
704
+ backgroundColor?: string;
705
+ /** Border color of the approval bubble */
706
+ borderColor?: string;
707
+ /** Color for the title text */
708
+ titleColor?: string;
709
+ /** Color for the description text */
710
+ descriptionColor?: string;
711
+ /** Background color for the approve button */
712
+ approveButtonColor?: string;
713
+ /** Text color for the approve button */
714
+ approveButtonTextColor?: string;
715
+ /** Background color for the deny button */
716
+ denyButtonColor?: string;
717
+ /** Text color for the deny button */
718
+ denyButtonTextColor?: string;
719
+ /** Background color for the parameters block */
720
+ parameterBackgroundColor?: string;
721
+ /** Text color for the parameters block */
722
+ parameterTextColor?: string;
723
+ /** Title text displayed above the description */
724
+ title?: string;
725
+ /** Label for the approve button */
726
+ approveLabel?: string;
727
+ /** Label for the deny button */
728
+ denyLabel?: string;
729
+ /**
730
+ * Custom handler for approval decisions.
731
+ * Return void to let the SDK auto-resolve via the API,
732
+ * or return a Response/ReadableStream for custom handling.
733
+ */
734
+ onDecision?: (
735
+ data: { approvalId: string; executionId: string; agentId: string; toolName: string },
736
+ decision: 'approved' | 'denied'
737
+ ) => Promise<Response | ReadableStream<Uint8Array> | void>;
738
+ };
739
+
696
740
  export type AgentWidgetToolCallConfig = {
697
741
  backgroundColor?: string;
698
742
  borderColor?: string;
@@ -1770,6 +1814,13 @@ export type AgentWidgetConfig = {
1770
1814
  */
1771
1815
  colorScheme?: 'auto' | 'light' | 'dark';
1772
1816
  features?: AgentWidgetFeatureFlags;
1817
+ /**
1818
+ * When true, focus the chat input after the panel opens and the open animation completes.
1819
+ * Applies to launcher mode (user click, controller.open(), autoExpand) and inline mode (on init).
1820
+ * Skip when voice is active to avoid stealing focus from voice UI.
1821
+ * @default false
1822
+ */
1823
+ autoFocusInput?: boolean;
1773
1824
  launcher?: AgentWidgetLauncherConfig;
1774
1825
  initialMessages?: AgentWidgetMessage[];
1775
1826
  suggestionChips?: string[];
@@ -1781,6 +1832,23 @@ export type AgentWidgetConfig = {
1781
1832
  statusIndicator?: AgentWidgetStatusIndicatorConfig;
1782
1833
  voiceRecognition?: AgentWidgetVoiceRecognitionConfig;
1783
1834
  toolCall?: AgentWidgetToolCallConfig;
1835
+ /**
1836
+ * Configuration for tool approval bubbles.
1837
+ * Set to `false` to disable built-in approval handling entirely.
1838
+ *
1839
+ * @example
1840
+ * ```typescript
1841
+ * config: {
1842
+ * approval: {
1843
+ * title: "Permission Required",
1844
+ * approveLabel: "Allow",
1845
+ * denyLabel: "Block",
1846
+ * approveButtonColor: "#16a34a"
1847
+ * }
1848
+ * }
1849
+ * ```
1850
+ */
1851
+ approval?: AgentWidgetApprovalConfig | false;
1784
1852
  postprocessMessage?: (context: {
1785
1853
  text: string;
1786
1854
  message: AgentWidgetMessage;
@@ -2129,7 +2197,23 @@ export type AgentWidgetToolCall = {
2129
2197
  durationMs?: number;
2130
2198
  };
2131
2199
 
2132
- export type AgentWidgetMessageVariant = "assistant" | "reasoning" | "tool";
2200
+ /**
2201
+ * Represents a tool approval request in the chat conversation.
2202
+ * Created when the agent requires human approval before executing a tool.
2203
+ */
2204
+ export type AgentWidgetApproval = {
2205
+ id: string;
2206
+ status: "pending" | "approved" | "denied" | "timeout";
2207
+ agentId: string;
2208
+ executionId: string;
2209
+ toolName: string;
2210
+ toolType?: string;
2211
+ description: string;
2212
+ parameters?: unknown;
2213
+ resolvedAt?: number;
2214
+ };
2215
+
2216
+ export type AgentWidgetMessageVariant = "assistant" | "reasoning" | "tool" | "approval";
2133
2217
 
2134
2218
  /**
2135
2219
  * Represents a message in the chat conversation.
@@ -2165,6 +2249,8 @@ export type AgentWidgetMessage = {
2165
2249
  reasoning?: AgentWidgetReasoning;
2166
2250
  toolCall?: AgentWidgetToolCall;
2167
2251
  tools?: AgentWidgetToolCall[];
2252
+ /** Approval data for messages with variant "approval" */
2253
+ approval?: AgentWidgetApproval;
2168
2254
  viaVoice?: boolean;
2169
2255
  /**
2170
2256
  * Raw structured payload for this message (e.g., JSON action response).
package/src/ui.ts CHANGED
@@ -37,6 +37,7 @@ import { MessageTransform, MessageActionCallbacks, LoadingIndicatorRenderer } fr
37
37
  import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
38
38
  import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
39
39
  import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
40
+ import { createApprovalBubble } from "./components/approval-bubble";
40
41
  import { createSuggestions } from "./components/suggestions";
41
42
  import { EventStreamBuffer } from "./utils/event-stream-buffer";
42
43
  import { EventStreamStore } from "./utils/event-stream-store";
@@ -259,6 +260,14 @@ type Controller = {
259
260
  showNPSFeedback: (options?: Partial<NPSFeedbackOptions>) => void;
260
261
  submitCSATFeedback: (rating: number, comment?: string) => Promise<void>;
261
262
  submitNPSFeedback: (rating: number, comment?: string) => Promise<void>;
263
+ /**
264
+ * Connect an external SSE stream and process it through the SDK's
265
+ * native event pipeline (tools, reasoning, streaming text, etc.).
266
+ */
267
+ connectStream: (
268
+ stream: ReadableStream<Uint8Array>,
269
+ options?: { assistantMessageId?: string }
270
+ ) => Promise<void>;
262
271
  /** Push a raw event into the event stream buffer (for testing/debugging) */
263
272
  __pushEventStreamEvent: (event: { type: string; payload: unknown }) => void;
264
273
  /** Opens the event stream panel */
@@ -267,6 +276,17 @@ type Controller = {
267
276
  hideEventStream: () => void;
268
277
  /** Returns current visibility state of the event stream panel */
269
278
  isEventStreamVisible: () => boolean;
279
+ /**
280
+ * Focus the chat input. Returns true if focus succeeded, false if panel is closed
281
+ * (launcher mode) or textarea is unavailable.
282
+ */
283
+ focusInput: () => boolean;
284
+ /**
285
+ * Programmatically resolve a pending approval.
286
+ * @param approvalId - The approval ID to resolve
287
+ * @param decision - "approved" or "denied"
288
+ */
289
+ resolveApproval: (approvalId: string, decision: 'approved' | 'denied') => Promise<void>;
270
290
  };
271
291
 
272
292
  const buildPostprocessor = (
@@ -442,6 +462,7 @@ export const createAgentExperience = (
442
462
 
443
463
  let launcherEnabled = config.launcher?.enabled ?? true;
444
464
  let autoExpand = config.launcher?.autoExpand ?? false;
465
+ const autoFocusInput = config.autoFocusInput ?? false;
445
466
  let prevAutoExpand = autoExpand;
446
467
  let prevLauncherEnabled = launcherEnabled;
447
468
  let prevHeaderLayout = config.layout?.header?.layout;
@@ -959,6 +980,46 @@ export const createAgentExperience = (
959
980
  }
960
981
  });
961
982
 
983
+ // Add event delegation for approval action buttons (approve/deny)
984
+ messagesWrapper.addEventListener('click', (event) => {
985
+ const target = event.target as HTMLElement;
986
+ const approvalButton = target.closest('button[data-approval-action]') as HTMLElement;
987
+ if (!approvalButton) return;
988
+
989
+ event.preventDefault();
990
+ event.stopPropagation();
991
+
992
+ const approvalBubble = approvalButton.closest('.vanilla-approval-bubble') as HTMLElement;
993
+ if (!approvalBubble) return;
994
+
995
+ const messageId = approvalBubble.getAttribute('data-message-id');
996
+ if (!messageId) return;
997
+
998
+ const action = approvalButton.getAttribute('data-approval-action') as 'approve' | 'deny';
999
+ if (!action) return;
1000
+
1001
+ const decision = action === 'approve' ? 'approved' as const : 'denied' as const;
1002
+
1003
+ // Find the approval message
1004
+ const messages = session.getMessages();
1005
+ const approvalMessage = messages.find(m => m.id === messageId);
1006
+ if (!approvalMessage?.approval) return;
1007
+
1008
+ // Disable buttons immediately for responsive UI
1009
+ const buttonsContainer = approvalBubble.querySelector('[data-approval-buttons]') as HTMLElement;
1010
+ if (buttonsContainer) {
1011
+ const buttons = buttonsContainer.querySelectorAll('button');
1012
+ buttons.forEach(btn => {
1013
+ (btn as HTMLButtonElement).disabled = true;
1014
+ btn.style.opacity = '0.5';
1015
+ btn.style.cursor = 'not-allowed';
1016
+ });
1017
+ }
1018
+
1019
+ // Resolve the approval
1020
+ session.resolveApproval(approvalMessage.approval, decision);
1021
+ });
1022
+
962
1023
  panel.appendChild(container);
963
1024
  mount.appendChild(wrapper);
964
1025
 
@@ -1414,6 +1475,15 @@ export const createAgentExperience = (
1414
1475
  ) {
1415
1476
  eventBus.emit("assistant:complete", message);
1416
1477
  }
1478
+
1479
+ // Emit approval events
1480
+ if (message.variant === "approval" && message.approval) {
1481
+ if (!previous) {
1482
+ eventBus.emit("approval:requested", { approval: message.approval, message });
1483
+ } else if (message.approval.status !== "pending") {
1484
+ eventBus.emit("approval:resolved", { approval: message.approval, decision: message.approval.status });
1485
+ }
1486
+ }
1417
1487
  });
1418
1488
 
1419
1489
  messageState.clear();
@@ -1462,6 +1532,9 @@ export const createAgentExperience = (
1462
1532
  if (message.variant === "tool" && p.renderToolCall) {
1463
1533
  return true;
1464
1534
  }
1535
+ if (message.variant === "approval" && p.renderApproval) {
1536
+ return true;
1537
+ }
1465
1538
  if (!message.variant && p.renderMessage) {
1466
1539
  return true;
1467
1540
  }
@@ -1486,6 +1559,13 @@ export const createAgentExperience = (
1486
1559
  defaultRenderer: () => createToolBubble(message, config),
1487
1560
  config
1488
1561
  });
1562
+ } else if (message.variant === "approval" && message.approval && matchingPlugin.renderApproval) {
1563
+ if (config.approval === false) return;
1564
+ bubble = matchingPlugin.renderApproval({
1565
+ message,
1566
+ defaultRenderer: () => createApprovalBubble(message, config),
1567
+ config
1568
+ });
1489
1569
  } else if (matchingPlugin.renderMessage) {
1490
1570
  bubble = matchingPlugin.renderMessage({
1491
1571
  message,
@@ -1566,6 +1646,9 @@ export const createAgentExperience = (
1566
1646
  } else if (message.variant === "tool" && message.toolCall) {
1567
1647
  if (!showToolCalls) return;
1568
1648
  bubble = createToolBubble(message, config);
1649
+ } else if (message.variant === "approval" && message.approval) {
1650
+ if (config.approval === false) return;
1651
+ bubble = createApprovalBubble(message, config);
1569
1652
  } else {
1570
1653
  // Check for custom message renderers in layout config
1571
1654
  const messageLayoutConfig = config.layout?.messages;
@@ -1621,8 +1704,10 @@ export const createAgentExperience = (
1621
1704
 
1622
1705
  // Also check if there's a recently completed assistant message (streaming just ended)
1623
1706
  // This prevents flicker when the message completes but isStreaming hasn't updated yet
1707
+ // Approval-variant messages are UI controls, not content — exclude them so the typing
1708
+ // indicator still shows while the agent resumes after approval
1624
1709
  const lastMessage = messages[messages.length - 1];
1625
- const hasRecentAssistantResponse = lastMessage?.role === "assistant" && !lastMessage.streaming;
1710
+ const hasRecentAssistantResponse = lastMessage?.role === "assistant" && !lastMessage.streaming && lastMessage.variant !== "approval";
1626
1711
 
1627
1712
  if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
1628
1713
  // Get loading indicator using priority chain: plugin -> config -> default
@@ -1843,6 +1928,16 @@ export const createAgentExperience = (
1843
1928
  });
1844
1929
  };
1845
1930
 
1931
+ const maybeFocusInput = () => {
1932
+ if (voiceState.active) return;
1933
+ if (!textarea) return;
1934
+ textarea.focus();
1935
+ };
1936
+
1937
+ eventBus.on("widget:opened", () => {
1938
+ if (config.autoFocusInput) setTimeout(() => maybeFocusInput(), 200);
1939
+ });
1940
+
1846
1941
  const updateCopy = () => {
1847
1942
  introTitle.textContent = config.copy?.welcomeTitle ?? "Hello 👋";
1848
1943
  introSubtitle.textContent =
@@ -2425,6 +2520,14 @@ export const createAgentExperience = (
2425
2520
  scheduleAutoScroll(true);
2426
2521
  maybeRestoreVoiceFromMetadata();
2427
2522
 
2523
+ if (autoFocusInput) {
2524
+ if (!launcherEnabled) {
2525
+ setTimeout(() => maybeFocusInput(), 0);
2526
+ } else if (open) {
2527
+ setTimeout(() => maybeFocusInput(), 200);
2528
+ }
2529
+ }
2530
+
2428
2531
  const recalcPanelHeight = () => {
2429
2532
  const sidebarMode = config.launcher?.sidebarMode ?? false;
2430
2533
  const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
@@ -3950,6 +4053,12 @@ export const createAgentExperience = (
3950
4053
  }
3951
4054
  session.injectTestEvent(event);
3952
4055
  },
4056
+ async connectStream(
4057
+ stream: ReadableStream<Uint8Array>,
4058
+ options?: { assistantMessageId?: string }
4059
+ ): Promise<void> {
4060
+ return session.connectStream(stream, options);
4061
+ },
3953
4062
  /** Push a raw event into the event stream buffer (for testing/debugging) */
3954
4063
  __pushEventStreamEvent(event: { type: string; payload: unknown }): void {
3955
4064
  if (eventStreamBuffer) {
@@ -3972,6 +4081,22 @@ export const createAgentExperience = (
3972
4081
  isEventStreamVisible(): boolean {
3973
4082
  return eventStreamVisible;
3974
4083
  },
4084
+ focusInput(): boolean {
4085
+ if (launcherEnabled && !open) return false;
4086
+ if (!textarea) return false;
4087
+ textarea.focus();
4088
+ return true;
4089
+ },
4090
+ async resolveApproval(approvalId: string, decision: 'approved' | 'denied'): Promise<void> {
4091
+ const messages = session.getMessages();
4092
+ const approvalMessage = messages.find(
4093
+ m => m.variant === "approval" && m.approval?.id === approvalId
4094
+ );
4095
+ if (!approvalMessage?.approval) {
4096
+ throw new Error(`Approval not found: ${approvalId}`);
4097
+ }
4098
+ return session.resolveApproval(approvalMessage.approval, decision);
4099
+ },
3975
4100
  getMessages() {
3976
4101
  return session.getMessages();
3977
4102
  },
@@ -4107,26 +4232,40 @@ export const createAgentExperience = (
4107
4232
  // ============================================================================
4108
4233
  // INSTANCE-SCOPED WINDOW EVENTS FOR PROGRAMMATIC CONTROL
4109
4234
  // ============================================================================
4110
- if (showEventStreamToggle && typeof window !== "undefined") {
4235
+ if (typeof window !== "undefined") {
4111
4236
  const instanceId = mount.getAttribute("data-persona-instance") || mount.id || "persona-" + Math.random().toString(36).slice(2, 8);
4112
- const handleShowEvent = (e: Event) => {
4113
- const detail = (e as CustomEvent).detail;
4114
- if (!detail?.instanceId || detail.instanceId === instanceId) {
4115
- controller.showEventStream();
4116
- }
4117
- };
4118
- const handleHideEvent = (e: Event) => {
4237
+
4238
+ const handleFocusInput = (e: Event) => {
4119
4239
  const detail = (e as CustomEvent).detail;
4120
4240
  if (!detail?.instanceId || detail.instanceId === instanceId) {
4121
- controller.hideEventStream();
4241
+ controller.focusInput();
4122
4242
  }
4123
4243
  };
4124
- window.addEventListener("persona:showEventStream", handleShowEvent);
4125
- window.addEventListener("persona:hideEventStream", handleHideEvent);
4244
+ window.addEventListener("persona:focusInput", handleFocusInput);
4126
4245
  destroyCallbacks.push(() => {
4127
- window.removeEventListener("persona:showEventStream", handleShowEvent);
4128
- window.removeEventListener("persona:hideEventStream", handleHideEvent);
4246
+ window.removeEventListener("persona:focusInput", handleFocusInput);
4129
4247
  });
4248
+
4249
+ if (showEventStreamToggle) {
4250
+ const handleShowEvent = (e: Event) => {
4251
+ const detail = (e as CustomEvent).detail;
4252
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
4253
+ controller.showEventStream();
4254
+ }
4255
+ };
4256
+ const handleHideEvent = (e: Event) => {
4257
+ const detail = (e as CustomEvent).detail;
4258
+ if (!detail?.instanceId || detail.instanceId === instanceId) {
4259
+ controller.hideEventStream();
4260
+ }
4261
+ };
4262
+ window.addEventListener("persona:showEventStream", handleShowEvent);
4263
+ window.addEventListener("persona:hideEventStream", handleHideEvent);
4264
+ destroyCallbacks.push(() => {
4265
+ window.removeEventListener("persona:showEventStream", handleShowEvent);
4266
+ window.removeEventListener("persona:hideEventStream", handleHideEvent);
4267
+ });
4268
+ }
4130
4269
  }
4131
4270
 
4132
4271
  // ============================================================================