@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "1.45.0",
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
- events = getFullHistory
1014
- ? await getFullHistory()
1015
- : buffer.getAll();
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, updateApprovalBubbleUI } from "./components/approval-bubble";
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 (showEventStreamToggle && typeof window !== "undefined") {
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
- const handleShowEvent = (e: Event) => {
4206
- const detail = (e as CustomEvent).detail;
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.hideEventStream();
4241
+ controller.focusInput();
4215
4242
  }
4216
4243
  };
4217
- window.addEventListener("persona:showEventStream", handleShowEvent);
4218
- window.addEventListener("persona:hideEventStream", handleHideEvent);
4244
+ window.addEventListener("persona:focusInput", handleFocusInput);
4219
4245
  destroyCallbacks.push(() => {
4220
- window.removeEventListener("persona:showEventStream", handleShowEvent);
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
  // ============================================================================