@runtypelabs/persona 3.9.2 → 3.10.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.
Files changed (39) hide show
  1. package/dist/index.cjs +45 -42
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +148 -0
  4. package/dist/index.d.ts +148 -0
  5. package/dist/index.global.js +67 -64
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +45 -42
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +959 -214
  10. package/dist/theme-editor.d.cts +157 -3
  11. package/dist/theme-editor.d.ts +157 -3
  12. package/dist/theme-editor.js +955 -214
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +8 -0
  15. package/dist/theme-reference.d.ts +8 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +154 -0
  18. package/package.json +1 -1
  19. package/src/client.test.ts +312 -1
  20. package/src/client.ts +247 -24
  21. package/src/components/messages.ts +1 -1
  22. package/src/components/reasoning-bubble.ts +117 -28
  23. package/src/components/tool-bubble.ts +161 -27
  24. package/src/defaults.ts +12 -0
  25. package/src/styles/widget.css +154 -0
  26. package/src/theme-editor/index.ts +5 -0
  27. package/src/theme-editor/preview-utils.test.ts +58 -0
  28. package/src/theme-editor/preview-utils.ts +220 -4
  29. package/src/theme-editor/sections.test.ts +20 -0
  30. package/src/theme-editor/sections.ts +10 -0
  31. package/src/theme-reference.ts +8 -3
  32. package/src/tool-call-display-defaults.test.ts +23 -0
  33. package/src/types.ts +155 -0
  34. package/src/ui.attachments-drop.test.ts +188 -0
  35. package/src/ui.scroll.test.ts +150 -0
  36. package/src/ui.tool-display.test.ts +204 -0
  37. package/src/ui.ts +275 -7
  38. package/src/utils/message-fingerprint.test.ts +17 -0
  39. package/src/utils/message-fingerprint.ts +13 -1
@@ -6,12 +6,76 @@ import { renderLucideIcon } from "../utils/icons";
6
6
  // Expansion state per widget instance
7
7
  export const toolExpansionState = new Set<string>();
8
8
 
9
+ const appendRenderedValue = (
10
+ container: HTMLElement,
11
+ value: HTMLElement | string | null | undefined
12
+ ): boolean => {
13
+ if (value == null) return false;
14
+ if (typeof value === "string") {
15
+ container.textContent = value;
16
+ return true;
17
+ }
18
+ container.appendChild(value);
19
+ return true;
20
+ };
21
+
22
+ const getToolPreviewText = (message: AgentWidgetMessage, maxLines: number): string => {
23
+ const tool = message.toolCall;
24
+ if (!tool) return "";
25
+
26
+ const chunkText = (tool.chunks ?? []).join("").trim();
27
+ if (chunkText) {
28
+ const lines = chunkText
29
+ .split(/\r?\n/)
30
+ .map((line) => line.trim())
31
+ .filter(Boolean)
32
+ .slice(-maxLines);
33
+ return lines.join("\n");
34
+ }
35
+
36
+ const argsText = formatUnknownValue(tool.args).trim();
37
+ if (!argsText) return "";
38
+
39
+ return argsText
40
+ .split(/\r?\n/)
41
+ .map((line) => line.trim())
42
+ .filter(Boolean)
43
+ .slice(0, maxLines)
44
+ .join("\n");
45
+ };
46
+
47
+ const getToolSummaryText = (
48
+ message: AgentWidgetMessage,
49
+ config?: AgentWidgetConfig
50
+ ): { summary: string; previewText: string; isActive: boolean } => {
51
+ const tool = message.toolCall;
52
+ const toolDisplayConfig = config?.features?.toolCallDisplay;
53
+ const collapsedMode = toolDisplayConfig?.collapsedMode ?? "tool-call";
54
+ const previewText = getToolPreviewText(message, toolDisplayConfig?.previewMaxLines ?? 3);
55
+ const defaultSummary = tool ? describeToolTitle(tool) : "";
56
+
57
+ if (!tool) {
58
+ return { summary: defaultSummary, previewText, isActive: false };
59
+ }
60
+
61
+ const isActive = tool.status !== "complete";
62
+ let summary = defaultSummary;
63
+ if (collapsedMode === "tool-name") {
64
+ summary = tool.name?.trim() || defaultSummary;
65
+ } else if (collapsedMode === "tool-preview" && previewText) {
66
+ summary = previewText;
67
+ }
68
+
69
+ return { summary, previewText, isActive };
70
+ };
71
+
9
72
  // Helper function to update tool bubble UI after expansion state changes
10
73
  export const updateToolBubbleUI = (messageId: string, bubble: HTMLElement, config?: AgentWidgetConfig): void => {
11
74
  const expanded = toolExpansionState.has(messageId);
12
75
  const toolCallConfig = config?.toolCall ?? {};
13
76
  const header = bubble.querySelector('button[data-expand-header="true"]') as HTMLElement;
14
77
  const content = bubble.querySelector('.persona-border-t') as HTMLElement;
78
+ const preview = bubble.querySelector('[data-persona-collapsed-preview="tool"]') as HTMLElement | null;
15
79
 
16
80
  if (!header || !content) return;
17
81
 
@@ -32,6 +96,11 @@ export const updateToolBubbleUI = (messageId: string, bubble: HTMLElement, confi
32
96
  }
33
97
 
34
98
  content.style.display = expanded ? "" : "none";
99
+ if (preview) {
100
+ preview.style.display = expanded
101
+ ? "none"
102
+ : ((preview.textContent || preview.childNodes.length) ? "" : "none");
103
+ }
35
104
  };
36
105
 
37
106
  export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidgetConfig): HTMLElement => {
@@ -78,14 +147,21 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
78
147
  return bubble;
79
148
  }
80
149
 
81
- let expanded = toolExpansionState.has(message.id);
150
+ const toolDisplayConfig = config?.features?.toolCallDisplay ?? {};
151
+ const expandable = toolDisplayConfig.expandable !== false;
152
+ let expanded = expandable && toolExpansionState.has(message.id);
153
+ const { summary, previewText, isActive } = getToolSummaryText(message, config);
82
154
  const header = createElement(
83
155
  "button",
84
- "persona-flex persona-w-full persona-items-center persona-justify-between persona-gap-3 persona-bg-transparent persona-px-4 persona-py-3 persona-text-left persona-cursor-pointer persona-border-none"
156
+ expandable
157
+ ? "persona-flex persona-w-full persona-items-center persona-justify-between persona-gap-3 persona-bg-transparent persona-px-4 persona-py-3 persona-text-left persona-cursor-pointer persona-border-none"
158
+ : "persona-flex persona-w-full persona-items-center persona-justify-between persona-gap-3 persona-bg-transparent persona-px-4 persona-py-3 persona-text-left persona-cursor-default persona-border-none"
85
159
  ) as HTMLButtonElement;
86
160
  header.type = "button";
87
- header.setAttribute("aria-expanded", expanded ? "true" : "false");
88
- header.setAttribute("data-expand-header", "true");
161
+ if (expandable) {
162
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
163
+ header.setAttribute("data-expand-header", "true");
164
+ }
89
165
  header.setAttribute("data-bubble-type", "tool");
90
166
 
91
167
  // Apply header styles
@@ -106,23 +182,78 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
106
182
  if (toolCallConfig.headerTextColor) {
107
183
  title.style.color = toolCallConfig.headerTextColor;
108
184
  }
109
- title.textContent = describeToolTitle(tool);
110
- headerContent.appendChild(title);
111
-
112
- const toggleIcon = createElement("div", "persona-flex persona-items-center");
113
- const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
114
- const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
115
- if (chevronIcon) {
116
- toggleIcon.appendChild(chevronIcon);
185
+ const customSummary = toolCallConfig.renderCollapsedSummary?.({
186
+ message,
187
+ toolCall: tool,
188
+ defaultSummary: summary,
189
+ previewText,
190
+ collapsedMode: toolDisplayConfig.collapsedMode ?? "tool-call",
191
+ isActive,
192
+ config: config ?? {},
193
+ });
194
+ if (typeof customSummary === "string" && customSummary.trim()) {
195
+ title.textContent = customSummary;
196
+ headerContent.appendChild(title);
197
+ } else if (customSummary instanceof HTMLElement) {
198
+ headerContent.appendChild(customSummary);
199
+ } else {
200
+ title.textContent = summary;
201
+ headerContent.appendChild(title);
202
+ }
203
+
204
+ let toggleIcon: HTMLElement | null = null;
205
+ if (expandable) {
206
+ toggleIcon = createElement("div", "persona-flex persona-items-center");
207
+ const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
208
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
209
+ if (chevronIcon) {
210
+ toggleIcon.appendChild(chevronIcon);
211
+ } else {
212
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
213
+ }
214
+
215
+ const headerMeta = createElement("div", "persona-flex persona-items-center persona-gap-2 persona-ml-auto");
216
+ headerMeta.append(toggleIcon);
217
+ header.append(headerContent, headerMeta);
117
218
  } else {
118
- // Fallback to text if icon fails
119
- toggleIcon.textContent = expanded ? "Hide" : "Show";
219
+ header.append(headerContent);
120
220
  }
121
221
 
122
- const headerMeta = createElement("div", "persona-flex persona-items-center persona-gap-2 persona-ml-auto");
123
- headerMeta.append(toggleIcon);
222
+ const collapsedPreview = createElement(
223
+ "div",
224
+ "persona-px-4 persona-py-3 persona-text-xs persona-leading-snug persona-text-persona-muted"
225
+ );
226
+ collapsedPreview.setAttribute("data-persona-collapsed-preview", "tool");
227
+ collapsedPreview.style.display = "none";
228
+ collapsedPreview.style.whiteSpace = "pre-wrap";
124
229
 
125
- header.append(headerContent, headerMeta);
230
+ if (
231
+ !expanded &&
232
+ isActive &&
233
+ toolDisplayConfig.activePreview &&
234
+ previewText
235
+ ) {
236
+ const renderedPreview = toolCallConfig.renderCollapsedPreview?.({
237
+ message,
238
+ toolCall: tool,
239
+ defaultPreview: previewText,
240
+ isActive,
241
+ config: config ?? {},
242
+ });
243
+ if (!appendRenderedValue(collapsedPreview, renderedPreview)) {
244
+ collapsedPreview.textContent = previewText;
245
+ }
246
+ collapsedPreview.style.display = "";
247
+ }
248
+
249
+ if (!expanded && isActive && toolDisplayConfig.activeMinHeight) {
250
+ bubble.style.minHeight = toolDisplayConfig.activeMinHeight;
251
+ }
252
+
253
+ if (!expandable) {
254
+ bubble.append(header, collapsedPreview);
255
+ return bubble;
256
+ }
126
257
 
127
258
  const content = createElement(
128
259
  "div",
@@ -265,22 +396,25 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
265
396
 
266
397
  const applyToolExpansion = () => {
267
398
  header.setAttribute("aria-expanded", expanded ? "true" : "false");
268
- // Update chevron icon
269
- toggleIcon.innerHTML = "";
270
- const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
271
- const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
272
- if (chevronIcon) {
273
- toggleIcon.appendChild(chevronIcon);
274
- } else {
275
- // Fallback to text if icon fails
276
- toggleIcon.textContent = expanded ? "Hide" : "Show";
399
+ if (toggleIcon) {
400
+ toggleIcon.innerHTML = "";
401
+ const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
402
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
403
+ if (chevronIcon) {
404
+ toggleIcon.appendChild(chevronIcon);
405
+ } else {
406
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
407
+ }
277
408
  }
278
409
  content.style.display = expanded ? "" : "none";
410
+ collapsedPreview.style.display = expanded
411
+ ? "none"
412
+ : ((collapsedPreview.textContent || collapsedPreview.childNodes.length) ? "" : "none");
279
413
  };
280
414
 
281
415
  applyToolExpansion();
282
416
 
283
- bubble.append(header, content);
417
+ bubble.append(header, collapsedPreview, content);
284
418
  return bubble;
285
419
  };
286
420
 
package/src/defaults.ts CHANGED
@@ -117,6 +117,18 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
117
117
  iconName: "arrow-down",
118
118
  label: "",
119
119
  },
120
+ toolCallDisplay: {
121
+ collapsedMode: "tool-call",
122
+ activePreview: false,
123
+ grouped: false,
124
+ previewMaxLines: 3,
125
+ expandable: true,
126
+ },
127
+ reasoningDisplay: {
128
+ activePreview: false,
129
+ previewMaxLines: 3,
130
+ expandable: true,
131
+ },
120
132
  },
121
133
  suggestionChips: [
122
134
  "What can you help me with?",
@@ -93,6 +93,35 @@
93
93
  gap: 1.5rem;
94
94
  }
95
95
 
96
+ [data-persona-root] .persona-widget-container .persona-attachment-drop-overlay {
97
+ display: none;
98
+ position: absolute;
99
+ inset: var(--persona-drop-overlay-inset, 0);
100
+ z-index: 50;
101
+ flex-direction: column;
102
+ align-items: center;
103
+ justify-content: center;
104
+ gap: 0.5rem;
105
+ pointer-events: none;
106
+ background: var(--persona-drop-overlay-bg, rgba(59, 130, 246, 0.08));
107
+ -webkit-backdrop-filter: blur(var(--persona-drop-overlay-blur, 8px));
108
+ backdrop-filter: blur(var(--persona-drop-overlay-blur, 8px));
109
+ border: var(--persona-drop-overlay-border, 2px dashed rgba(59, 130, 246, 0.4));
110
+ border-radius: var(--persona-drop-overlay-radius, inherit);
111
+ transition: opacity 0.15s ease;
112
+ }
113
+
114
+ [data-persona-root] .persona-widget-container .persona-attachment-drop-overlay .persona-drop-overlay-label {
115
+ font-size: var(--persona-drop-overlay-label-size, 0.875rem);
116
+ color: var(--persona-drop-overlay-label-color, rgba(59, 130, 246, 0.8));
117
+ font-weight: 500;
118
+ user-select: none;
119
+ }
120
+
121
+ [data-persona-root] .persona-widget-container.persona-attachment-drop-active .persona-attachment-drop-overlay {
122
+ display: flex;
123
+ }
124
+
96
125
  /* Widget CSS Variables - scoped to widget root to avoid polluting global namespace */
97
126
  [data-persona-root] {
98
127
  --persona-radius-sm: 0.125rem;
@@ -935,6 +964,7 @@
935
964
  }
936
965
 
937
966
  .persona-widget-container {
967
+ position: relative;
938
968
  border-radius: var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem));
939
969
  }
940
970
 
@@ -2091,6 +2121,130 @@
2091
2121
  background-color: var(--cw-container, #f8fafc);
2092
2122
  }
2093
2123
 
2124
+ /* Collapsed live preview blocks — match expanded content's 1px border-top with transparent border */
2125
+ [data-persona-root] [data-persona-collapsed-preview] {
2126
+ border-top: 1px solid transparent;
2127
+ }
2128
+
2129
+ /* Grouped tool-call transcript chrome */
2130
+ [data-persona-root] .persona-tool-group {
2131
+ --_rail-x: 0.5rem;
2132
+ --_rail-w: 2px;
2133
+ --_dot-size: 6px;
2134
+ --_branch-len: 0.75rem;
2135
+ --_branch-y: 1.5rem;
2136
+ --_rail-color: var(--cw-border, #d1d5db);
2137
+ gap: 0;
2138
+ }
2139
+
2140
+ /* ---- Summary row ---- */
2141
+ [data-persona-root] .persona-tool-group-summary {
2142
+ position: relative;
2143
+ min-height: 1.5rem;
2144
+ padding-left: calc(var(--_rail-x) + var(--_branch-len) + 0.625rem);
2145
+ margin-bottom: 0.625rem;
2146
+ display: flex;
2147
+ align-items: center;
2148
+ }
2149
+
2150
+ /* horizontal branch from rail to summary text */
2151
+ [data-persona-root] .persona-tool-group-summary::before {
2152
+ content: "";
2153
+ position: absolute;
2154
+ left: var(--_rail-x);
2155
+ top: 50%;
2156
+ width: var(--_branch-len);
2157
+ height: var(--_rail-w);
2158
+ background: var(--_rail-color);
2159
+ transform: translateY(-50%);
2160
+ }
2161
+
2162
+ /* dot at the rail end of the summary branch */
2163
+ [data-persona-root] .persona-tool-group-summary::after {
2164
+ content: "";
2165
+ position: absolute;
2166
+ left: var(--_rail-x);
2167
+ top: 50%;
2168
+ width: var(--_dot-size);
2169
+ height: var(--_dot-size);
2170
+ border-radius: 999px;
2171
+ border: var(--_rail-w) solid var(--_rail-color);
2172
+ background: var(--cw-surface, #fff);
2173
+ transform: translate(-50%, -50%);
2174
+ }
2175
+
2176
+ /* ---- Stack (children column) ---- */
2177
+ [data-persona-root] .persona-tool-group-stack {
2178
+ position: relative;
2179
+ margin-left: var(--_rail-x);
2180
+ padding-left: calc(var(--_branch-len) + 0.125rem);
2181
+ gap: 0.625rem;
2182
+ }
2183
+
2184
+ /* vertical rail */
2185
+ [data-persona-root] .persona-tool-group-stack::before {
2186
+ content: "";
2187
+ position: absolute;
2188
+ left: 0;
2189
+ top: calc(-0.625rem - (var(--_dot-size) / 2));
2190
+ bottom: calc(100% - var(--_branch-y));
2191
+ width: var(--_rail-w);
2192
+ background: var(--_rail-color);
2193
+ transform: translateX(-50%);
2194
+ }
2195
+
2196
+ /* extend rail between items when there are multiple */
2197
+ [data-persona-root] .persona-tool-group-stack::after {
2198
+ content: "";
2199
+ position: absolute;
2200
+ left: 0;
2201
+ top: 0;
2202
+ bottom: 0;
2203
+ width: var(--_rail-w);
2204
+ background: var(--_rail-color);
2205
+ transform: translateX(-50%);
2206
+ z-index: 0;
2207
+ pointer-events: none;
2208
+ }
2209
+
2210
+ /* clip the rail at the last item's branch-y */
2211
+ [data-persona-root] .persona-tool-group-item:last-child {
2212
+ position: relative;
2213
+ }
2214
+
2215
+ /* ---- Item branches ---- */
2216
+ [data-persona-root] .persona-tool-group-item {
2217
+ position: relative;
2218
+ z-index: 1;
2219
+ }
2220
+
2221
+ /* horizontal branch from rail to each child bubble */
2222
+ [data-persona-root] .persona-tool-group-item::before {
2223
+ content: "";
2224
+ position: absolute;
2225
+ left: calc(-1 * var(--_branch-len) - 0.125rem);
2226
+ top: var(--_branch-y);
2227
+ width: calc(var(--_branch-len) + 0.125rem);
2228
+ height: var(--_rail-w);
2229
+ background: var(--_rail-color);
2230
+ transform: translateY(-50%);
2231
+ }
2232
+
2233
+ /* dot at the junction of the rail and each branch */
2234
+ [data-persona-root] .persona-tool-group-item::after {
2235
+ content: "";
2236
+ position: absolute;
2237
+ left: calc(-1 * var(--_branch-len) - 0.125rem);
2238
+ top: var(--_branch-y);
2239
+ width: var(--_dot-size);
2240
+ height: var(--_dot-size);
2241
+ border-radius: 999px;
2242
+ border: var(--_rail-w) solid var(--_rail-color);
2243
+ background: var(--cw-surface, #fff);
2244
+ transform: translate(-50%, -50%);
2245
+ z-index: 1;
2246
+ }
2247
+
2094
2248
  /* ============================================
2095
2249
  Approval Bubble Theme Styles
2096
2250
  ============================================ */
@@ -81,12 +81,17 @@ export {
81
81
  buildShellCss,
82
82
  applyShellTheme,
83
83
  buildSrcdoc,
84
+ getPreviewTranscriptPresetLabel,
85
+ createPreviewTranscriptEntry,
86
+ appendPreviewTranscriptEntry,
84
87
  createPreviewMessages,
85
88
  applySceneConfig,
86
89
  buildPreviewConfig,
90
+ buildPreviewConfigWithMessages,
87
91
  } from './preview-utils';
88
92
  export type {
89
93
  PreviewScene,
94
+ PreviewTranscriptEntryPreset,
90
95
  PreviewShellPalette,
91
96
  PreviewConfigOptions,
92
97
  } from './preview-utils';
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ appendPreviewTranscriptEntry,
5
+ buildPreviewConfig,
6
+ createPreviewTranscriptEntry,
7
+ } from "./preview-utils";
8
+
9
+ describe("theme editor preview demo data", () => {
10
+ it("seeds tool call preview messages when advanced tool display modes are enabled", () => {
11
+ const config = buildPreviewConfig({
12
+ scene: "conversation",
13
+ config: {
14
+ features: {
15
+ toolCallDisplay: {
16
+ activePreview: true,
17
+ },
18
+ },
19
+ },
20
+ });
21
+
22
+ expect(config.initialMessages?.some((message) => message.variant === "tool")).toBe(true);
23
+ });
24
+
25
+ it("seeds reasoning preview messages when advanced reasoning display modes are enabled", () => {
26
+ const config = buildPreviewConfig({
27
+ scene: "conversation",
28
+ config: {
29
+ features: {
30
+ reasoningDisplay: {
31
+ activePreview: true,
32
+ },
33
+ },
34
+ },
35
+ });
36
+
37
+ expect(config.initialMessages?.some((message) => message.variant === "reasoning")).toBe(true);
38
+ });
39
+
40
+ it("creates public preview transcript entries for tool and reasoning presets", () => {
41
+ const toolMessage = createPreviewTranscriptEntry("tool-running", 1);
42
+ const reasoningMessage = createPreviewTranscriptEntry("reasoning-streaming", 2);
43
+
44
+ expect(toolMessage.variant).toBe("tool");
45
+ expect(toolMessage.toolCall?.status).toBe("running");
46
+ expect(reasoningMessage.variant).toBe("reasoning");
47
+ expect(reasoningMessage.reasoning?.status).toBe("streaming");
48
+ });
49
+
50
+ it("appends public preview transcript entries to an existing conversation", () => {
51
+ const messages = appendPreviewTranscriptEntry([], "tool-running");
52
+ const updated = appendPreviewTranscriptEntry(messages, "reasoning-complete");
53
+
54
+ expect(updated).toHaveLength(2);
55
+ expect(updated[0].variant).toBe("tool");
56
+ expect(updated[1].variant).toBe("reasoning");
57
+ });
58
+ });