@runtypelabs/persona 3.8.3 → 3.9.1

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/client.ts CHANGED
@@ -1018,6 +1018,7 @@ export class AgentWidgetClient {
1018
1018
  const assistantMessageRef = { current: null as AgentWidgetMessage | null };
1019
1019
  // Track current partId for message segmentation at tool boundaries
1020
1020
  const partIdState = { current: null as string | null };
1021
+ let didSplitByPartId = false;
1021
1022
  const reasoningMessages = new Map<string, AgentWidgetMessage>();
1022
1023
  const toolMessages = new Map<string, AgentWidgetMessage>();
1023
1024
  const reasoningContext = {
@@ -1060,11 +1061,22 @@ export class AgentWidgetClient {
1060
1061
  payload.step_id
1061
1062
  );
1062
1063
 
1064
+ const baseAssistantId = assistantMessageId;
1065
+ let assistantIdConsumed = false;
1066
+
1063
1067
  const ensureAssistantMessage = () => {
1064
1068
  if (assistantMessage) return assistantMessage;
1069
+ let id: string;
1070
+ if (!assistantIdConsumed && baseAssistantId) {
1071
+ id = baseAssistantId;
1072
+ assistantIdConsumed = true;
1073
+ } else if (baseAssistantId && partIdState.current) {
1074
+ id = `${baseAssistantId}_${partIdState.current}`;
1075
+ } else {
1076
+ id = `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1077
+ }
1065
1078
  assistantMessage = {
1066
- // Use pre-generated ID if provided, otherwise generate one
1067
- id: assistantMessageId ?? `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
1079
+ id,
1068
1080
  role: "assistant",
1069
1081
  content: "",
1070
1082
  createdAt: new Date().toISOString(),
@@ -1507,6 +1519,31 @@ export class AgentWidgetClient {
1507
1519
  if (callKey) {
1508
1520
  toolContext.byCall.delete(callKey);
1509
1521
  }
1522
+ } else if (payloadType === "text_start") {
1523
+ // Lifecycle event: a new text segment is beginning (emitted at tool boundaries)
1524
+ const incomingPartId = payload.partId;
1525
+ if (incomingPartId !== undefined && partIdState.current !== null && incomingPartId !== partIdState.current) {
1526
+ const prev = assistantMessage as AgentWidgetMessage | null;
1527
+ if (prev) {
1528
+ prev.streaming = false;
1529
+ emitMessage(prev);
1530
+ assistantMessage = null;
1531
+ didSplitByPartId = true;
1532
+ }
1533
+ }
1534
+ if (incomingPartId !== undefined) {
1535
+ partIdState.current = incomingPartId;
1536
+ }
1537
+ } else if (payloadType === "text_end") {
1538
+ // Lifecycle event: current text segment ended (tool call about to start)
1539
+ // Seal the current assistant message so the next segment gets a new one
1540
+ const prev = assistantMessage as AgentWidgetMessage | null;
1541
+ if (prev) {
1542
+ prev.streaming = false;
1543
+ emitMessage(prev);
1544
+ assistantMessage = null;
1545
+ didSplitByPartId = true;
1546
+ }
1510
1547
  } else if (payloadType === "step_chunk" || payloadType === "step_delta") {
1511
1548
  // Only process chunks for prompt steps, not tool/context steps
1512
1549
  const stepType = (payload as any).stepType;
@@ -1515,7 +1552,27 @@ export class AgentWidgetClient {
1515
1552
  // Skip tool-related chunks - they're handled by tool_start/tool_complete
1516
1553
  continue;
1517
1554
  }
1555
+
1556
+ // partId-based segmentation: when partId changes, seal current message
1557
+ // and start a new one so text and tools render in chronological order
1558
+ const incomingPartId = payload.partId;
1559
+ if (incomingPartId !== undefined && partIdState.current !== null && incomingPartId !== partIdState.current) {
1560
+ const prev = assistantMessage as AgentWidgetMessage | null;
1561
+ if (prev) {
1562
+ prev.streaming = false;
1563
+ emitMessage(prev);
1564
+ assistantMessage = null;
1565
+ didSplitByPartId = true;
1566
+ }
1567
+ }
1568
+ if (incomingPartId !== undefined) {
1569
+ partIdState.current = incomingPartId;
1570
+ }
1571
+
1518
1572
  const assistant = ensureAssistantMessage();
1573
+ if (incomingPartId !== undefined && !assistant.partId) {
1574
+ assistant.partId = incomingPartId;
1575
+ }
1519
1576
  // Support various field names: text, delta, content, chunk (Runtype uses 'chunk')
1520
1577
  const chunk = payload.text ?? payload.delta ?? payload.content ?? payload.chunk ?? "";
1521
1578
  if (chunk) {
@@ -1700,6 +1757,20 @@ export class AgentWidgetClient {
1700
1757
  // Skip tool-related completions - they're handled by tool_complete
1701
1758
  continue;
1702
1759
  }
1760
+ if (didSplitByPartId) {
1761
+ // text_end already sealed the assistant message(s) — don't recreate
1762
+ // one from step_complete's full response (would cause duplication)
1763
+ if (assistantMessage !== null) {
1764
+ const msg: AgentWidgetMessage = assistantMessage;
1765
+ streamParsers.delete(msg.id);
1766
+ rawContentBuffers.delete(msg.id);
1767
+ if (msg.streaming !== false) {
1768
+ msg.streaming = false;
1769
+ emitMessage(msg);
1770
+ }
1771
+ }
1772
+ continue;
1773
+ }
1703
1774
  const finalContent = payload.result?.response;
1704
1775
  const assistant = ensureAssistantMessage();
1705
1776
  if (finalContent !== undefined && finalContent !== null) {
@@ -1822,7 +1893,19 @@ export class AgentWidgetClient {
1822
1893
  }
1823
1894
  } else if (payloadType === "flow_complete") {
1824
1895
  const finalContent = payload.result?.response;
1825
- if (finalContent !== undefined && finalContent !== null) {
1896
+ if (didSplitByPartId) {
1897
+ // Content was split into multiple assistant messages — the full response
1898
+ // in flow_complete would overwrite the last segment. Just finalize streaming.
1899
+ if (assistantMessage !== null) {
1900
+ const msg: AgentWidgetMessage = assistantMessage;
1901
+ streamParsers.delete(msg.id);
1902
+ rawContentBuffers.delete(msg.id);
1903
+ if (msg.streaming !== false) {
1904
+ msg.streaming = false;
1905
+ emitMessage(msg);
1906
+ }
1907
+ }
1908
+ } else if (finalContent !== undefined && finalContent !== null) {
1826
1909
  const assistant = ensureAssistantMessage();
1827
1910
  // Check if we have raw content buffer that needs final processing
1828
1911
  const rawBuffer = rawContentBuffers.get(assistant.id);
@@ -1,4 +1,5 @@
1
1
  import { createElement } from "../utils/dom";
2
+ import { DEFAULT_FLOATING_LAUNCHER_WIDTH } from "../defaults";
2
3
  import { AgentWidgetConfig } from "../types";
3
4
  import { positionMap } from "../utils/positioning";
4
5
  import { isDockedMountMode } from "../utils/dock";
@@ -68,7 +69,7 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
68
69
  "persona-widget-panel persona-relative persona-min-h-[320px]"
69
70
  );
70
71
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
71
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
72
+ const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
72
73
  panel.style.width = width;
73
74
  panel.style.maxWidth = width;
74
75
 
package/src/defaults.ts CHANGED
@@ -2,6 +2,16 @@ import type { AgentWidgetConfig } from "./types";
2
2
  import type { DeepPartial, PersonaTheme } from "./types/theme";
3
3
  import { deepMerge } from "./utils/deep-merge";
4
4
 
5
+ /**
6
+ * Default width for the floating launcher panel (when not overridden).
7
+ * Benchmarks: many chat products use ~300–400px; 400px is a frequent “standard” default.
8
+ * We use 440px to better fit code/JSON and structured replies while staying responsive via `min(..., 100vw)`.
9
+ */
10
+ export const DEFAULT_FLOATING_LAUNCHER_WIDTH = "min(440px, calc(100vw - 24px))";
11
+
12
+ /** Max width cap paired with {@link DEFAULT_FLOATING_LAUNCHER_WIDTH} for theme defaults. */
13
+ export const DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH = "440px";
14
+
5
15
  /**
6
16
  * Default widget configuration
7
17
  * Single source of truth for all default values
@@ -26,7 +36,7 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
26
36
  agentIconName: "bot",
27
37
  headerIconName: "bot",
28
38
  position: "bottom-right",
29
- width: "min(400px, calc(100vw - 24px))",
39
+ width: DEFAULT_FLOATING_LAUNCHER_WIDTH,
30
40
  heightOffset: 0,
31
41
  autoExpand: false,
32
42
  callToActionIconHidden: false,
package/src/index.ts CHANGED
@@ -274,6 +274,8 @@ export {
274
274
  // Default configuration exports
275
275
  export {
276
276
  DEFAULT_WIDGET_CONFIG,
277
+ DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
278
+ DEFAULT_FLOATING_LAUNCHER_WIDTH,
277
279
  mergeWithDefaults
278
280
  } from "./defaults";
279
281
  export {
package/src/presets.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AgentWidgetConfig } from "./types";
2
2
  import type { DeepPartial, PersonaTheme } from "./types/theme";
3
+ import { DEFAULT_FLOATING_LAUNCHER_WIDTH } from "./defaults";
3
4
 
4
5
  /**
5
6
  * A named preset containing partial widget configuration.
@@ -65,7 +66,7 @@ export const PRESET_SHOP: WidgetPreset = {
65
66
  subtitle: "Here to help you find what you need",
66
67
  agentIconText: "🛍️",
67
68
  position: "bottom-right",
68
- width: "min(400px, calc(100vw - 24px))",
69
+ width: DEFAULT_FLOATING_LAUNCHER_WIDTH,
69
70
  },
70
71
  copy: {
71
72
  welcomeTitle: "Welcome to our shop!",
@@ -1,6 +1,10 @@
1
1
  /** Declarative section/field definitions for the theme editor (pure data — no DOM, no render logic) */
2
2
 
3
3
  import type { SectionDef, TabDef, SubGroupDef, FieldDef } from './types';
4
+ import {
5
+ DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
6
+ DEFAULT_FLOATING_LAUNCHER_WIDTH,
7
+ } from '../defaults';
4
8
  import { COLOR_FAMILIES } from './color-utils';
5
9
  import {
6
10
  ROLE_SURFACES,
@@ -273,8 +277,8 @@ const panelLayoutSectionDef: SectionDef = {
273
277
  title: 'Panel',
274
278
  collapsed: false,
275
279
  fields: [
276
- { id: 'panel-width', label: 'Width', type: 'text', path: 'theme.components.panel.width', defaultValue: 'min(400px, calc(100vw - 24px))' },
277
- { id: 'panel-max-width', label: 'Max Width', type: 'text', path: 'theme.components.panel.maxWidth', defaultValue: '400px' },
280
+ { id: 'panel-width', label: 'Width', type: 'text', path: 'theme.components.panel.width', defaultValue: DEFAULT_FLOATING_LAUNCHER_WIDTH },
281
+ { id: 'panel-max-width', label: 'Max Width', type: 'text', path: 'theme.components.panel.maxWidth', defaultValue: DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH },
278
282
  { id: 'panel-height', label: 'Height', type: 'text', path: 'theme.components.panel.height', defaultValue: '600px' },
279
283
  { id: 'panel-max-height', label: 'Max Height', type: 'text', path: 'theme.components.panel.maxHeight', defaultValue: 'calc(100vh - 80px)' },
280
284
  { id: 'panel-border-radius', label: 'Border Radius', type: 'select', path: 'theme.components.panel.borderRadius', defaultValue: 'palette.radius.xl', options: [
@@ -590,7 +594,7 @@ const launcherBasicsSectionDef: SectionDef = {
590
594
  { id: 'launch-enabled', label: 'Enabled', type: 'toggle', path: 'launcher.enabled', defaultValue: true },
591
595
  { id: 'launch-mount-mode', label: 'Mount Mode', type: 'select', path: 'launcher.mountMode', defaultValue: 'floating', options: [{ value: 'floating', label: 'Floating' }, { value: 'docked', label: 'Docked' }] },
592
596
  { id: 'launch-position', label: 'Position', type: 'select', path: 'launcher.position', defaultValue: 'bottom-right', options: [{ value: 'bottom-right', label: 'Bottom Right' }, { value: 'bottom-left', label: 'Bottom Left' }, { value: 'top-right', label: 'Top Right' }, { value: 'top-left', label: 'Top Left' }] },
593
- { id: 'launch-width', label: 'Width', type: 'text', path: 'launcher.width', defaultValue: 'min(400px, calc(100vw - 24px))' },
597
+ { id: 'launch-width', label: 'Width', type: 'text', path: 'launcher.width', defaultValue: DEFAULT_FLOATING_LAUNCHER_WIDTH },
594
598
  { id: 'launch-auto-expand', label: 'Auto Expand', type: 'toggle', path: 'launcher.autoExpand', defaultValue: false },
595
599
  { id: 'launch-title', label: 'Title', type: 'text', path: 'launcher.title', defaultValue: 'Chat Assistant' },
596
600
  { id: 'launch-subtitle', label: 'Subtitle', type: 'text', path: 'launcher.subtitle', defaultValue: 'Here to help you get answers fast' },
@@ -115,7 +115,7 @@ export const THEME_TOKEN_DOCS = {
115
115
  panel: {
116
116
  description: 'Chat panel container.',
117
117
  properties:
118
- 'width, maxWidth (400px), height (600px), maxHeight, borderRadius, shadow.',
118
+ 'width, maxWidth (440px), height (600px), maxHeight, borderRadius, shadow.',
119
119
  },
120
120
  header: {
121
121
  description: 'Chat panel header.',
package/src/ui.ts CHANGED
@@ -69,7 +69,7 @@ import {
69
69
  import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resize";
70
70
  import { enhanceWithForms } from "./components/forms";
71
71
  import { pluginRegistry } from "./plugins/registry";
72
- import { mergeWithDefaults } from "./defaults";
72
+ import { mergeWithDefaults, DEFAULT_FLOATING_LAUNCHER_WIDTH } from "./defaults";
73
73
  import { createEventBus } from "./utils/events";
74
74
  import {
75
75
  createActionManager,
@@ -1508,7 +1508,7 @@ export const createAgentExperience = (
1508
1508
  if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
1509
1509
  if (!shouldExpandLauncherForArtifacts(config, launcherEnabled)) return;
1510
1510
 
1511
- const base = config.launcher?.width ?? config.launcherWidth ?? "min(400px, calc(100vw - 24px))";
1511
+ const base = config.launcher?.width ?? config.launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
1512
1512
  const expanded =
1513
1513
  config.features?.artifacts?.layout?.expandedPanelWidth ??
1514
1514
  "min(720px, calc(100vw - 24px))";
@@ -1659,7 +1659,7 @@ export const createAgentExperience = (
1659
1659
 
1660
1660
  // Re-apply panel width/maxWidth from initial setup
1661
1661
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
1662
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
1662
+ const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
1663
1663
  if (!sidebarMode && !dockedMode) {
1664
1664
  if (isInlineEmbed && fullHeight) {
1665
1665
  panel.style.width = "100%";
@@ -3609,7 +3609,7 @@ export const createAgentExperience = (
3609
3609
  // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3610
3610
  if (!sidebarMode && !dockedMode) {
3611
3611
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3612
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3612
+ const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
3613
3613
  panel.style.width = width;
3614
3614
  panel.style.maxWidth = width;
3615
3615
  }
@@ -8,6 +8,10 @@ import type {
8
8
  ComponentTokens,
9
9
  SemanticTokens,
10
10
  } from '../types/theme';
11
+ import {
12
+ DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
13
+ DEFAULT_FLOATING_LAUNCHER_WIDTH,
14
+ } from '../defaults';
11
15
 
12
16
  export const DEFAULT_PALETTE = {
13
17
  colors: {
@@ -277,8 +281,8 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
277
281
  shadow: 'palette.shadows.lg',
278
282
  },
279
283
  panel: {
280
- width: 'min(400px, calc(100vw - 24px))',
281
- maxWidth: '400px',
284
+ width: DEFAULT_FLOATING_LAUNCHER_WIDTH,
285
+ maxWidth: DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
282
286
  height: '600px',
283
287
  maxHeight: 'calc(100vh - 80px)',
284
288
  borderRadius: 'palette.radius.xl',