@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/README.md +33 -0
- package/dist/index.cjs +31 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -2
- package/dist/index.d.ts +149 -2
- package/dist/index.global.js +53 -53
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +31 -31
- package/dist/index.js.map +1 -1
- package/dist/widget.css +68 -0
- package/package.json +1 -1
- package/src/client.ts +123 -0
- package/src/components/approval-bubble.ts +223 -0
- package/src/components/event-stream-view.test.ts +26 -0
- package/src/components/event-stream-view.ts +6 -3
- package/src/components/message-bubble.ts +10 -5
- package/src/components/reasoning-bubble.ts +1 -1
- package/src/components/tool-bubble.ts +4 -4
- package/src/index.ts +3 -0
- package/src/plugins/types.ts +10 -0
- package/src/session.ts +120 -0
- package/src/styles/widget.css +68 -0
- package/src/types.ts +87 -1
- package/src/ui.ts +153 -14
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;
|
package/src/styles/widget.css
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
4113
|
-
|
|
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.
|
|
4241
|
+
controller.focusInput();
|
|
4122
4242
|
}
|
|
4123
4243
|
};
|
|
4124
|
-
window.addEventListener("persona:
|
|
4125
|
-
window.addEventListener("persona:hideEventStream", handleHideEvent);
|
|
4244
|
+
window.addEventListener("persona:focusInput", handleFocusInput);
|
|
4126
4245
|
destroyCallbacks.push(() => {
|
|
4127
|
-
window.removeEventListener("persona:
|
|
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
|
// ============================================================================
|