@runtypelabs/persona 3.15.1 → 3.16.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
@@ -1057,6 +1057,16 @@ export class AgentWidgetClient {
1057
1057
  let didSplitByPartId = false;
1058
1058
  const reasoningMessages = new Map<string, AgentWidgetMessage>();
1059
1059
  const toolMessages = new Map<string, AgentWidgetMessage>();
1060
+ // Messages produced by steps inside a nested flow executed as a tool.
1061
+ // Keyed by `${parentToolId}::${nestedStepId}::${partId}` so each nested
1062
+ // step (send-stream, prompt) gets its own assistant message, and prompts
1063
+ // with inner tool calls split into one message per text segment — still
1064
+ // attributable to the parent tool call.
1065
+ const nestedStepMessages = new Map<string, AgentWidgetMessage>();
1066
+ // Most-recent partId seen for a given `${toolId}::${stepId}` scope, used
1067
+ // to seal the previous segment when a new partId arrives within the
1068
+ // same nested prompt step.
1069
+ const nestedPartIdByStep = new Map<string, string>();
1060
1070
  const reasoningContext = {
1061
1071
  lastId: null as string | null,
1062
1072
  byStep: new Map<string, string>()
@@ -1066,6 +1076,49 @@ export class AgentWidgetClient {
1066
1076
  byCall: new Map<string, string>()
1067
1077
  };
1068
1078
 
1079
+ // Nested message key. partId defaults to "" so steps without segmentation
1080
+ // (e.g. send-stream) still have a deterministic single key.
1081
+ const getNestedStepKey = (
1082
+ toolId: string,
1083
+ stepId: string,
1084
+ partId: string = ""
1085
+ ) => `${toolId}::${stepId}::${partId}`;
1086
+
1087
+ // Prefix used to sweep every nested message belonging to a single
1088
+ // (toolId, stepId) scope — needed on step_complete to seal any segments
1089
+ // that are still streaming.
1090
+ const getNestedStepPrefix = (toolId: string, stepId: string) =>
1091
+ `${toolId}::${stepId}::`;
1092
+
1093
+ const ensureNestedStepMessage = (
1094
+ toolId: string,
1095
+ stepId: string,
1096
+ partId: string,
1097
+ executionId?: string
1098
+ ): AgentWidgetMessage => {
1099
+ const key = getNestedStepKey(toolId, stepId, partId);
1100
+ const existing = nestedStepMessages.get(key);
1101
+ if (existing) return existing;
1102
+ const idSuffix = partId ? `-${partId}` : "";
1103
+ const message: AgentWidgetMessage = {
1104
+ id: `nested-${toolId}-${stepId}${idSuffix}`,
1105
+ role: "assistant",
1106
+ content: "",
1107
+ createdAt: new Date().toISOString(),
1108
+ streaming: true,
1109
+ sequence: nextSequence(),
1110
+ ...(partId ? { partId } : {}),
1111
+ agentMetadata: {
1112
+ executionId,
1113
+ parentToolId: toolId,
1114
+ parentStepId: stepId,
1115
+ },
1116
+ };
1117
+ nestedStepMessages.set(key, message);
1118
+ emitMessage(message);
1119
+ return message;
1120
+ };
1121
+
1069
1122
  const normalizeKey = (value: unknown): string | null => {
1070
1123
  if (value === null || value === undefined) return null;
1071
1124
  try {
@@ -1669,7 +1722,13 @@ export class AgentWidgetClient {
1669
1722
  toolContext.byCall.delete(callKey);
1670
1723
  }
1671
1724
  } else if (payloadType === "text_start") {
1672
- // Lifecycle event: a new text segment is beginning (emitted at tool boundaries)
1725
+ // Lifecycle event: a new text segment is beginning (emitted at tool boundaries).
1726
+ // When toolContext is present this fired inside a nested flow — it must not
1727
+ // seal or rotate the outer assistant message. Nested prompt segmentation is
1728
+ // handled via nestedStepMessages keyed by (toolId, stepId).
1729
+ if ((payload as any).toolContext?.toolId) {
1730
+ continue;
1731
+ }
1673
1732
  const incomingPartId = payload.partId;
1674
1733
  if (incomingPartId !== undefined && partIdState.current !== null && incomingPartId !== partIdState.current) {
1675
1734
  const prev = assistantMessage as AgentWidgetMessage | null;
@@ -1685,7 +1744,13 @@ export class AgentWidgetClient {
1685
1744
  partIdState.current = incomingPartId;
1686
1745
  }
1687
1746
  } else if (payloadType === "text_end") {
1688
- // Lifecycle event: current text segment ended (tool call about to start)
1747
+ // Lifecycle event: current text segment ended (tool call about to start).
1748
+ // When toolContext is present the boundary belongs to a nested flow — leave
1749
+ // outer assistant state alone so the outer stream is never interrupted by
1750
+ // nested activity.
1751
+ if ((payload as any).toolContext?.toolId) {
1752
+ continue;
1753
+ }
1689
1754
  // Seal the current assistant message so the next segment gets a new one
1690
1755
  const prev = assistantMessage as AgentWidgetMessage | null;
1691
1756
  if (prev) {
@@ -1704,6 +1769,77 @@ export class AgentWidgetClient {
1704
1769
  continue;
1705
1770
  }
1706
1771
 
1772
+ // Nested flow routing: when toolContext is present, this step_delta
1773
+ // originated inside a nested flow executed as a tool. Surface it as
1774
+ // its own assistant message keyed by the nested step id, so authors
1775
+ // who add send-stream / prompt steps inside their flow see them as
1776
+ // real messages in the timeline, in order — rather than merging
1777
+ // into the outer assistant bubble or getting buried in the tool
1778
+ // card. Each nested step id gets its own message; the parent tool
1779
+ // bubble continues to represent the invocation via tool_* events.
1780
+ const nestedToolCtx = (payload as any).toolContext as
1781
+ | { toolId?: string; stepId?: string; executionId?: string }
1782
+ | undefined;
1783
+ if (nestedToolCtx?.toolId) {
1784
+ const nestedStepId = String(
1785
+ payload.id ?? nestedToolCtx.stepId ?? `step-${nextSequence()}`
1786
+ );
1787
+ const incomingPartId =
1788
+ payload.partId !== undefined && payload.partId !== null
1789
+ ? String(payload.partId)
1790
+ : "";
1791
+ const stepScopeKey = `${nestedToolCtx.toolId}::${nestedStepId}`;
1792
+ const prevPartId = nestedPartIdByStep.get(stepScopeKey);
1793
+
1794
+ // If partId changed within this nested step (prompt with inner
1795
+ // tool call emitting a new text segment), seal the previous
1796
+ // segment's message so each segment renders as its own bubble.
1797
+ if (
1798
+ incomingPartId !== "" &&
1799
+ prevPartId !== undefined &&
1800
+ prevPartId !== "" &&
1801
+ prevPartId !== incomingPartId
1802
+ ) {
1803
+ const prev = nestedStepMessages.get(
1804
+ getNestedStepKey(
1805
+ nestedToolCtx.toolId,
1806
+ nestedStepId,
1807
+ prevPartId
1808
+ )
1809
+ );
1810
+ if (prev && prev.streaming !== false) {
1811
+ prev.streaming = false;
1812
+ emitMessage(prev);
1813
+ }
1814
+ }
1815
+ if (incomingPartId !== "") {
1816
+ nestedPartIdByStep.set(stepScopeKey, incomingPartId);
1817
+ }
1818
+
1819
+ const nestedMsg = ensureNestedStepMessage(
1820
+ nestedToolCtx.toolId,
1821
+ nestedStepId,
1822
+ incomingPartId,
1823
+ nestedToolCtx.executionId
1824
+ );
1825
+ const nestedChunk =
1826
+ payload.text ??
1827
+ payload.delta ??
1828
+ payload.content ??
1829
+ payload.chunk ??
1830
+ "";
1831
+ if (nestedChunk) {
1832
+ nestedMsg.content += String(nestedChunk);
1833
+ nestedMsg.streaming = true;
1834
+ emitMessage(nestedMsg);
1835
+ }
1836
+ if (payload.isComplete) {
1837
+ nestedMsg.streaming = false;
1838
+ emitMessage(nestedMsg);
1839
+ }
1840
+ continue;
1841
+ }
1842
+
1707
1843
  // partId-based segmentation: when partId changes, seal current message
1708
1844
  // and start a new one so text and tools render in chronological order
1709
1845
  const incomingPartId = payload.partId;
@@ -1927,6 +2063,37 @@ export class AgentWidgetClient {
1927
2063
  // Skip tool-related completions - they're handled by tool_complete
1928
2064
  continue;
1929
2065
  }
2066
+
2067
+ // Nested flow: seal every segment message produced by this nested
2068
+ // step (a single nested prompt step may have produced multiple
2069
+ // messages, one per partId, when inner tool calls split it). The
2070
+ // outer assistantMessage state is untouched so reconciliation for
2071
+ // the outer flow still works.
2072
+ const nestedCompleteCtx = (payload as any).toolContext as
2073
+ | { toolId?: string; stepId?: string; executionId?: string }
2074
+ | undefined;
2075
+ if (nestedCompleteCtx?.toolId) {
2076
+ const nestedStepId = String(
2077
+ payload.id ?? nestedCompleteCtx.stepId ?? ""
2078
+ );
2079
+ if (nestedStepId) {
2080
+ const prefix = getNestedStepPrefix(
2081
+ nestedCompleteCtx.toolId,
2082
+ nestedStepId
2083
+ );
2084
+ for (const [key, msg] of nestedStepMessages) {
2085
+ if (key.startsWith(prefix) && msg.streaming !== false) {
2086
+ msg.streaming = false;
2087
+ emitMessage(msg);
2088
+ }
2089
+ }
2090
+ nestedPartIdByStep.delete(
2091
+ `${nestedCompleteCtx.toolId}::${nestedStepId}`
2092
+ );
2093
+ }
2094
+ continue;
2095
+ }
2096
+
1930
2097
  if (didSplitByPartId) {
1931
2098
  // Sealed segment(s) — do not create a second bubble from step_complete.
1932
2099
  // Merge authoritative final response into the last sealed segment (fixes async lag).
@@ -159,9 +159,11 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
159
159
  clearChatButton.style.color =
160
160
  clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
161
161
 
162
- // Add icon
162
+ // Add icon. display:block eliminates inline-baseline spacing that can
163
+ // push the icon a fractional pixel off-center inside the button.
163
164
  const iconSvg = renderLucideIcon(clearChatIconName, "20px", "currentColor", 1);
164
165
  if (iconSvg) {
166
+ iconSvg.style.display = "block";
165
167
  clearChatButton.appendChild(iconSvg);
166
168
  }
167
169
 
@@ -276,15 +278,17 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
276
278
  }
277
279
  }
278
280
 
279
- // Create close button wrapper for tooltip positioning
280
- // Only needs ml-auto if clear chat is disabled or top-right positioned
281
+ // Create close button wrapper for tooltip positioning.
282
+ // Mirrors the clear-chat wrapper's inline-flex centering so both
283
+ // header action buttons vertically align identically within the
284
+ // header's flex row.
281
285
  const closeButtonWrapper = createElement(
282
286
  "div",
283
287
  closeButtonPlacement === "top-right"
284
288
  ? "persona-absolute persona-top-4 persona-right-4 persona-z-50"
285
289
  : clearChatEnabled && clearChatPlacement === "inline"
286
- ? ""
287
- : "persona-ml-auto"
290
+ ? "persona-relative persona-inline-flex persona-items-center persona-justify-center"
291
+ : "persona-relative persona-ml-auto persona-inline-flex persona-items-center persona-justify-center"
288
292
  );
289
293
 
290
294
  // Create close button with base classes
@@ -309,9 +313,16 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
309
313
  closeButton.style.color =
310
314
  launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
311
315
 
312
- // Try to render Lucide icon, fallback to text if not provided or fails
313
- const closeIconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 1);
316
+ // Try to render Lucide icon, fallback to text if not provided or fails.
317
+ // The X glyph's paths occupy only the middle 50% of its 24x24 viewBox
318
+ // (from 6,6 to 18,18), while other header icons (e.g. refresh-cw) span
319
+ // ~75% of the viewBox. Rendering X at a larger intrinsic size brings
320
+ // its visible extent into parity with sibling icons in the header.
321
+ // display:block eliminates inline-baseline spacing that can push the
322
+ // icon a fractional pixel off-center inside the button.
323
+ const closeIconSvg = renderLucideIcon(closeButtonIconName, "28px", "currentColor", 1);
314
324
  if (closeIconSvg) {
325
+ closeIconSvg.style.display = "block";
315
326
  closeButton.appendChild(closeIconSvg);
316
327
  } else {
317
328
  closeButton.textContent = closeButtonIconText;
@@ -215,7 +215,9 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
215
215
  launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
216
216
 
217
217
  const closeButtonIconName = launcher.closeButtonIconName ?? "x";
218
- const closeIconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
218
+ // Larger intrinsic size compensates for the X glyph's sparse viewBox
219
+ // (paths only occupy the middle 50%). Matches header-builder.ts.
220
+ const closeIconSvg = renderLucideIcon(closeButtonIconName, "28px", "currentColor", 1);
219
221
  if (closeIconSvg) {
220
222
  closeButton.appendChild(closeIconSvg);
221
223
  } else {
package/src/defaults.ts CHANGED
@@ -43,6 +43,12 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
43
43
  agentIconSize: "40px",
44
44
  headerIconSize: "40px",
45
45
  closeButtonSize: "32px",
46
+ // Zero out browser-default <button> padding so the icon gets the full
47
+ // 32x32 content box, matching clearChat.paddingX/Y below. Without this,
48
+ // UA stylesheets add ~1-2px vertical and ~6px horizontal padding that
49
+ // eats into the border-box width and shrinks the rendered icon.
50
+ closeButtonPaddingX: "0px",
51
+ closeButtonPaddingY: "0px",
46
52
  callToActionIconName: "arrow-up-right",
47
53
  callToActionIconText: "",
48
54
  callToActionIconSize: "32px",
package/src/types.ts CHANGED
@@ -194,6 +194,17 @@ export type AgentMessageMetadata = {
194
194
  iteration?: number;
195
195
  turnId?: string;
196
196
  agentName?: string;
197
+ /**
198
+ * When this message was produced by a step inside a nested flow executed
199
+ * as a tool, identifies the parent tool call id. Enables renderers to
200
+ * visually group or indent nested-flow output under its parent tool.
201
+ */
202
+ parentToolId?: string;
203
+ /**
204
+ * Nested flow step id that produced this message (e.g. a `send-stream`
205
+ * or `prompt` step inside the nested flow). Stable key for that step.
206
+ */
207
+ parentStepId?: string;
197
208
  };
198
209
 
199
210
  export type AgentWidgetRequestMiddlewareContext = {
package/src/ui.ts CHANGED
@@ -4617,9 +4617,11 @@ export const createAgentExperience = (
4617
4617
  const closeButtonIconName = launcher.closeButtonIconName ?? "x";
4618
4618
  const closeButtonIconText = launcher.closeButtonIconText ?? "×";
4619
4619
 
4620
- // Clear existing content and render new icon
4620
+ // Clear existing content and render new icon.
4621
+ // Larger intrinsic size compensates for the X glyph's sparse
4622
+ // viewBox so the close button visually matches sibling icons.
4621
4623
  closeButton.innerHTML = "";
4622
- const iconSvg = renderLucideIcon(closeButtonIconName, "20px", "currentColor", 2);
4624
+ const iconSvg = renderLucideIcon(closeButtonIconName, "28px", "currentColor", 1);
4623
4625
  if (iconSvg) {
4624
4626
  closeButton.appendChild(iconSvg);
4625
4627
  } else {