@runtypelabs/persona 3.8.2 → 3.9.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/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) {
@@ -1822,7 +1879,19 @@ export class AgentWidgetClient {
1822
1879
  }
1823
1880
  } else if (payloadType === "flow_complete") {
1824
1881
  const finalContent = payload.result?.response;
1825
- if (finalContent !== undefined && finalContent !== null) {
1882
+ if (didSplitByPartId) {
1883
+ // Content was split into multiple assistant messages — the full response
1884
+ // in flow_complete would overwrite the last segment. Just finalize streaming.
1885
+ if (assistantMessage !== null) {
1886
+ const msg: AgentWidgetMessage = assistantMessage;
1887
+ streamParsers.delete(msg.id);
1888
+ rawContentBuffers.delete(msg.id);
1889
+ if (msg.streaming !== false) {
1890
+ msg.streaming = false;
1891
+ emitMessage(msg);
1892
+ }
1893
+ }
1894
+ } else if (finalContent !== undefined && finalContent !== null) {
1826
1895
  const assistant = ensureAssistantMessage();
1827
1896
  // Check if we have raw content buffer that needs final processing
1828
1897
  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,
@@ -883,6 +883,10 @@ export const createAgentExperience = (
883
883
  selectedModelId: composerCfg?.selectedModelId,
884
884
  onModelChange: (modelId: string) => {
885
885
  config.composer = { ...config.composer, selectedModelId: modelId };
886
+ // Sync to agent config so the next request uses the selected model
887
+ if (config.agent) {
888
+ config.agent = { ...config.agent, model: modelId };
889
+ }
886
890
  },
887
891
  onVoiceToggle:
888
892
  config.voiceRecognition?.enabled === true
@@ -929,13 +933,18 @@ export const createAgentExperience = (
929
933
  ensureComposerAttachmentSurface(footer);
930
934
  bindComposerRefsFromFooter(footer);
931
935
 
932
- // Apply contentMaxWidth to composer form and attachment previews if configured
936
+ // Apply contentMaxWidth to composer form, suggestions, and attachment previews if configured
933
937
  const contentMaxWidth = config.layout?.contentMaxWidth;
934
938
  if (contentMaxWidth && composerForm) {
935
939
  composerForm.style.maxWidth = contentMaxWidth;
936
940
  composerForm.style.marginLeft = "auto";
937
941
  composerForm.style.marginRight = "auto";
938
942
  }
943
+ if (contentMaxWidth && suggestions) {
944
+ suggestions.style.maxWidth = contentMaxWidth;
945
+ suggestions.style.marginLeft = "auto";
946
+ suggestions.style.marginRight = "auto";
947
+ }
939
948
  if (contentMaxWidth && attachmentPreviewsContainer) {
940
949
  attachmentPreviewsContainer.style.maxWidth = contentMaxWidth;
941
950
  attachmentPreviewsContainer.style.marginLeft = "auto";
@@ -1499,7 +1508,7 @@ export const createAgentExperience = (
1499
1508
  if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
1500
1509
  if (!shouldExpandLauncherForArtifacts(config, launcherEnabled)) return;
1501
1510
 
1502
- const base = config.launcher?.width ?? config.launcherWidth ?? "min(400px, calc(100vw - 24px))";
1511
+ const base = config.launcher?.width ?? config.launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
1503
1512
  const expanded =
1504
1513
  config.features?.artifacts?.layout?.expandedPanelWidth ??
1505
1514
  "min(720px, calc(100vw - 24px))";
@@ -1650,7 +1659,7 @@ export const createAgentExperience = (
1650
1659
 
1651
1660
  // Re-apply panel width/maxWidth from initial setup
1652
1661
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
1653
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
1662
+ const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
1654
1663
  if (!sidebarMode && !dockedMode) {
1655
1664
  if (isInlineEmbed && fullHeight) {
1656
1665
  panel.style.width = "100%";
@@ -3600,7 +3609,7 @@ export const createAgentExperience = (
3600
3609
  // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
3601
3610
  if (!sidebarMode && !dockedMode) {
3602
3611
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
3603
- const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
3612
+ const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
3604
3613
  panel.style.width = width;
3605
3614
  panel.style.maxWidth = width;
3606
3615
  }
@@ -5021,6 +5030,11 @@ export const createAgentExperience = (
5021
5030
  composerForm.style.marginLeft = "auto";
5022
5031
  composerForm.style.marginRight = "auto";
5023
5032
  }
5033
+ if (suggestions) {
5034
+ suggestions.style.maxWidth = updatedContentMaxWidth;
5035
+ suggestions.style.marginLeft = "auto";
5036
+ suggestions.style.marginRight = "auto";
5037
+ }
5024
5038
  } else {
5025
5039
  messagesWrapper.style.maxWidth = "";
5026
5040
  messagesWrapper.style.marginLeft = "";
@@ -5031,6 +5045,11 @@ export const createAgentExperience = (
5031
5045
  composerForm.style.marginLeft = "";
5032
5046
  composerForm.style.marginRight = "";
5033
5047
  }
5048
+ if (suggestions) {
5049
+ suggestions.style.maxWidth = "";
5050
+ suggestions.style.marginLeft = "";
5051
+ suggestions.style.marginRight = "";
5052
+ }
5034
5053
  }
5035
5054
 
5036
5055
  // Update status indicator visibility and text
@@ -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',