@runtypelabs/persona 1.45.0 → 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 +30 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.global.js +48 -48
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +30 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/event-stream-view.test.ts +26 -0
- package/src/components/event-stream-view.ts +6 -3
- package/src/types.ts +7 -0
- package/src/ui.ts +61 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -555,6 +555,32 @@ describe("createEventStreamView", () => {
|
|
|
555
555
|
// Should call getFullHistory
|
|
556
556
|
expect(getFullHistory).toHaveBeenCalled();
|
|
557
557
|
});
|
|
558
|
+
|
|
559
|
+
it("should fall back to buffer when getFullHistory returns empty", async () => {
|
|
560
|
+
const { createEventStreamView } = await loadModule();
|
|
561
|
+
const events = [
|
|
562
|
+
makeEvent("step_chunk", 1),
|
|
563
|
+
makeEvent("flow_complete", 2),
|
|
564
|
+
];
|
|
565
|
+
const buffer = createMockBuffer(events);
|
|
566
|
+
const getFullHistory = vi.fn().mockResolvedValue([]);
|
|
567
|
+
const { element } = createEventStreamView({
|
|
568
|
+
buffer: buffer as any,
|
|
569
|
+
getFullHistory,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const copyAllBtn = getCopyAllBtn(element);
|
|
573
|
+
|
|
574
|
+
// Click copy all with no filters (All events)
|
|
575
|
+
await copyAllBtn.__listeners.click[0]();
|
|
576
|
+
|
|
577
|
+
expect(getFullHistory).toHaveBeenCalled();
|
|
578
|
+
const writeCall = (globalThis.navigator.clipboard.writeText as any).mock.calls[0][0];
|
|
579
|
+
const parsed = JSON.parse(writeCall);
|
|
580
|
+
expect(parsed).toHaveLength(2);
|
|
581
|
+
expect(parsed[0].index).toBe(1);
|
|
582
|
+
expect(parsed[1].index).toBe(2);
|
|
583
|
+
});
|
|
558
584
|
});
|
|
559
585
|
|
|
560
586
|
describe("keyboard shortcuts", () => {
|
|
@@ -1010,9 +1010,12 @@ export function createEventStreamView(
|
|
|
1010
1010
|
if (hasActiveFilters()) {
|
|
1011
1011
|
events = filteredEvents;
|
|
1012
1012
|
} else {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1013
|
+
if (getFullHistory) {
|
|
1014
|
+
events = await getFullHistory();
|
|
1015
|
+
if (events.length === 0) events = buffer.getAll();
|
|
1016
|
+
} else {
|
|
1017
|
+
events = buffer.getAll();
|
|
1018
|
+
}
|
|
1016
1019
|
}
|
|
1017
1020
|
const parsed = events.map((e) => {
|
|
1018
1021
|
try {
|
package/src/types.ts
CHANGED
|
@@ -1814,6 +1814,13 @@ export type AgentWidgetConfig = {
|
|
|
1814
1814
|
*/
|
|
1815
1815
|
colorScheme?: 'auto' | 'light' | 'dark';
|
|
1816
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;
|
|
1817
1824
|
launcher?: AgentWidgetLauncherConfig;
|
|
1818
1825
|
initialMessages?: AgentWidgetMessage[];
|
|
1819
1826
|
suggestionChips?: string[];
|
package/src/ui.ts
CHANGED
|
@@ -37,7 +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
|
|
40
|
+
import { createApprovalBubble } from "./components/approval-bubble";
|
|
41
41
|
import { createSuggestions } from "./components/suggestions";
|
|
42
42
|
import { EventStreamBuffer } from "./utils/event-stream-buffer";
|
|
43
43
|
import { EventStreamStore } from "./utils/event-stream-store";
|
|
@@ -276,6 +276,11 @@ type Controller = {
|
|
|
276
276
|
hideEventStream: () => void;
|
|
277
277
|
/** Returns current visibility state of the event stream panel */
|
|
278
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;
|
|
279
284
|
/**
|
|
280
285
|
* Programmatically resolve a pending approval.
|
|
281
286
|
* @param approvalId - The approval ID to resolve
|
|
@@ -457,6 +462,7 @@ export const createAgentExperience = (
|
|
|
457
462
|
|
|
458
463
|
let launcherEnabled = config.launcher?.enabled ?? true;
|
|
459
464
|
let autoExpand = config.launcher?.autoExpand ?? false;
|
|
465
|
+
const autoFocusInput = config.autoFocusInput ?? false;
|
|
460
466
|
let prevAutoExpand = autoExpand;
|
|
461
467
|
let prevLauncherEnabled = launcherEnabled;
|
|
462
468
|
let prevHeaderLayout = config.layout?.header?.layout;
|
|
@@ -1698,8 +1704,10 @@ export const createAgentExperience = (
|
|
|
1698
1704
|
|
|
1699
1705
|
// Also check if there's a recently completed assistant message (streaming just ended)
|
|
1700
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
|
|
1701
1709
|
const lastMessage = messages[messages.length - 1];
|
|
1702
|
-
const hasRecentAssistantResponse = lastMessage?.role === "assistant" && !lastMessage.streaming;
|
|
1710
|
+
const hasRecentAssistantResponse = lastMessage?.role === "assistant" && !lastMessage.streaming && lastMessage.variant !== "approval";
|
|
1703
1711
|
|
|
1704
1712
|
if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
|
|
1705
1713
|
// Get loading indicator using priority chain: plugin -> config -> default
|
|
@@ -1920,6 +1928,16 @@ export const createAgentExperience = (
|
|
|
1920
1928
|
});
|
|
1921
1929
|
};
|
|
1922
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
|
+
|
|
1923
1941
|
const updateCopy = () => {
|
|
1924
1942
|
introTitle.textContent = config.copy?.welcomeTitle ?? "Hello 👋";
|
|
1925
1943
|
introSubtitle.textContent =
|
|
@@ -2502,6 +2520,14 @@ export const createAgentExperience = (
|
|
|
2502
2520
|
scheduleAutoScroll(true);
|
|
2503
2521
|
maybeRestoreVoiceFromMetadata();
|
|
2504
2522
|
|
|
2523
|
+
if (autoFocusInput) {
|
|
2524
|
+
if (!launcherEnabled) {
|
|
2525
|
+
setTimeout(() => maybeFocusInput(), 0);
|
|
2526
|
+
} else if (open) {
|
|
2527
|
+
setTimeout(() => maybeFocusInput(), 200);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2505
2531
|
const recalcPanelHeight = () => {
|
|
2506
2532
|
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
2507
2533
|
const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
@@ -4055,6 +4081,12 @@ export const createAgentExperience = (
|
|
|
4055
4081
|
isEventStreamVisible(): boolean {
|
|
4056
4082
|
return eventStreamVisible;
|
|
4057
4083
|
},
|
|
4084
|
+
focusInput(): boolean {
|
|
4085
|
+
if (launcherEnabled && !open) return false;
|
|
4086
|
+
if (!textarea) return false;
|
|
4087
|
+
textarea.focus();
|
|
4088
|
+
return true;
|
|
4089
|
+
},
|
|
4058
4090
|
async resolveApproval(approvalId: string, decision: 'approved' | 'denied'): Promise<void> {
|
|
4059
4091
|
const messages = session.getMessages();
|
|
4060
4092
|
const approvalMessage = messages.find(
|
|
@@ -4200,26 +4232,40 @@ export const createAgentExperience = (
|
|
|
4200
4232
|
// ============================================================================
|
|
4201
4233
|
// INSTANCE-SCOPED WINDOW EVENTS FOR PROGRAMMATIC CONTROL
|
|
4202
4234
|
// ============================================================================
|
|
4203
|
-
if (
|
|
4235
|
+
if (typeof window !== "undefined") {
|
|
4204
4236
|
const instanceId = mount.getAttribute("data-persona-instance") || mount.id || "persona-" + Math.random().toString(36).slice(2, 8);
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
if (!detail?.instanceId || detail.instanceId === instanceId) {
|
|
4208
|
-
controller.showEventStream();
|
|
4209
|
-
}
|
|
4210
|
-
};
|
|
4211
|
-
const handleHideEvent = (e: Event) => {
|
|
4237
|
+
|
|
4238
|
+
const handleFocusInput = (e: Event) => {
|
|
4212
4239
|
const detail = (e as CustomEvent).detail;
|
|
4213
4240
|
if (!detail?.instanceId || detail.instanceId === instanceId) {
|
|
4214
|
-
controller.
|
|
4241
|
+
controller.focusInput();
|
|
4215
4242
|
}
|
|
4216
4243
|
};
|
|
4217
|
-
window.addEventListener("persona:
|
|
4218
|
-
window.addEventListener("persona:hideEventStream", handleHideEvent);
|
|
4244
|
+
window.addEventListener("persona:focusInput", handleFocusInput);
|
|
4219
4245
|
destroyCallbacks.push(() => {
|
|
4220
|
-
window.removeEventListener("persona:
|
|
4221
|
-
window.removeEventListener("persona:hideEventStream", handleHideEvent);
|
|
4246
|
+
window.removeEventListener("persona:focusInput", handleFocusInput);
|
|
4222
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
|
+
}
|
|
4223
4269
|
}
|
|
4224
4270
|
|
|
4225
4271
|
// ============================================================================
|