@runtypelabs/persona 4.23.2 → 4.25.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +278 -37
- package/dist/approval-ui.d.ts +278 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +273 -25
- package/dist/artifacts-ui.d.ts +273 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +273 -25
- package/dist/context-mentions.d.ts +273 -25
- package/dist/event-stream-view.d.cts +273 -25
- package/dist/event-stream-view.d.ts +273 -25
- package/dist/forms-ui.d.cts +319 -27
- package/dist/forms-ui.d.ts +319 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -39
- package/dist/index.d.ts +344 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +273 -25
- package/dist/session-reconnect.d.ts +273 -25
- package/dist/smart-dom-reader.d.cts +273 -25
- package/dist/smart-dom-reader.d.ts +273 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +280 -25
- package/dist/theme-editor-preview.d.ts +280 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +293 -27
- package/dist/theme-editor.d.ts +293 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime-7DA7MJH4.js +1 -0
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +33 -0
- package/dist/voice-runtime.d.ts +33 -0
- package/dist/voice-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +4 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +59 -7
- package/src/session.voice.test.ts +72 -1
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +207 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +422 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/voice/runtype-voice-provider.ts +238 -27
- package/src/voice/voice.test.ts +272 -2
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
- package/dist/voice-runtime-EFR3R5ES.js +0 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { appendToolDetails } from "./tool-details";
|
|
2
|
+
import { applyActivityRow, activityDisplay, activityVariant } from "./activity-row";
|
|
1
3
|
import { createElement, createNode } from "../utils/dom";
|
|
2
4
|
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
5
|
+
import { DEFAULT_TOOL_CALL_DISPLAY } from "../defaults";
|
|
3
6
|
import { formatUnknownValue, describeToolTitle, resolveToolHeaderText, computeToolElapsed, parseFormattedTemplate } from "../utils/formatting";
|
|
4
7
|
import { appendCharSpans } from "../utils/tool-loading-animation";
|
|
5
8
|
import {
|
|
@@ -11,19 +14,15 @@ import {
|
|
|
11
14
|
updateExpandableBubbleUI,
|
|
12
15
|
} from "./expandable-bubble";
|
|
13
16
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Default the toggle chevron to the tool-call title color so it stays
|
|
18
|
-
// readable on whatever surface the title does. The title falls back to
|
|
19
|
-
// `.persona-text-persona-primary` (var(--persona-primary)) when no
|
|
20
|
-
// `headerTextColor` is set, so mirror that here instead of `currentColor`.
|
|
17
|
+
// Rows inherit their muted label color; cards retain the legacy primary fallback.
|
|
18
|
+
// Explicit toggle and header colors win in both variants.
|
|
21
19
|
const toolChevronColor = (
|
|
22
|
-
toolCallConfig: NonNullable<AgentWidgetConfig["toolCall"]
|
|
20
|
+
toolCallConfig: NonNullable<AgentWidgetConfig["toolCall"]>,
|
|
21
|
+
activityRow = false
|
|
23
22
|
): string =>
|
|
24
23
|
toolCallConfig.toggleTextColor ||
|
|
25
24
|
toolCallConfig.headerTextColor ||
|
|
26
|
-
"var(--persona-primary, #171717)";
|
|
25
|
+
(activityRow ? "currentColor" : "var(--persona-primary, #171717)");
|
|
27
26
|
|
|
28
27
|
const getToolPreviewText = (message: AgentWidgetMessage, maxLines: number): string => {
|
|
29
28
|
const tool = message.toolCall;
|
|
@@ -72,11 +71,14 @@ const getToolSummaryText = (
|
|
|
72
71
|
): { summary: string; previewText: string; isActive: boolean } => {
|
|
73
72
|
const tool = message.toolCall;
|
|
74
73
|
const toolDisplayConfig = config?.features?.toolCallDisplay;
|
|
75
|
-
const collapsedMode = toolDisplayConfig?.collapsedMode ??
|
|
74
|
+
const collapsedMode = toolDisplayConfig?.collapsedMode ?? DEFAULT_TOOL_CALL_DISPLAY.collapsedMode;
|
|
76
75
|
const previewText = tool?.success === false
|
|
77
76
|
? tool.error || "Tool failed"
|
|
78
|
-
: getToolPreviewText(
|
|
79
|
-
|
|
77
|
+
: getToolPreviewText(
|
|
78
|
+
message,
|
|
79
|
+
toolDisplayConfig?.previewMaxLines ?? DEFAULT_TOOL_CALL_DISPLAY.previewMaxLines
|
|
80
|
+
);
|
|
81
|
+
const defaultSummary = tool ? (activityVariant(config, "tool") === "row" ? `${tool.status === "complete" ? "Used" : "Using"} ${tool.name?.trim() || "tool"}` : describeToolTitle(tool)) : "";
|
|
80
82
|
|
|
81
83
|
if (!tool) {
|
|
82
84
|
return { summary: defaultSummary, previewText, isActive: false };
|
|
@@ -103,15 +105,25 @@ const getToolSummaryText = (
|
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
// Helper function to update tool bubble UI after expansion state changes
|
|
106
|
-
export const updateToolBubbleUI = (
|
|
108
|
+
export const updateToolBubbleUI = (
|
|
109
|
+
messageId: string,
|
|
110
|
+
bubble: HTMLElement,
|
|
111
|
+
expansionState: Set<string>,
|
|
112
|
+
config?: AgentWidgetConfig
|
|
113
|
+
): void => {
|
|
107
114
|
updateExpandableBubbleUI(messageId, bubble, {
|
|
108
|
-
stateSet:
|
|
115
|
+
stateSet: expansionState,
|
|
109
116
|
previewKind: "tool",
|
|
110
|
-
iconColor: toolChevronColor(config?.toolCall ?? {}),
|
|
117
|
+
iconColor: toolChevronColor(config?.toolCall ?? {}, bubble.classList.contains("persona-activity-row")),
|
|
111
118
|
});
|
|
112
119
|
};
|
|
113
120
|
|
|
114
|
-
export const createToolBubble = (
|
|
121
|
+
export const createToolBubble = (
|
|
122
|
+
message: AgentWidgetMessage,
|
|
123
|
+
config?: AgentWidgetConfig,
|
|
124
|
+
expansionState = new Set<string>()
|
|
125
|
+
): HTMLElement => {
|
|
126
|
+
config = activityDisplay(config, "tool");
|
|
115
127
|
const tool = message.toolCall;
|
|
116
128
|
const toolCallConfig = config?.toolCall ?? {};
|
|
117
129
|
|
|
@@ -133,15 +145,15 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
133
145
|
bubble.style.boxShadow =
|
|
134
146
|
toolCallConfig.shadow !== undefined
|
|
135
147
|
? (toolCallConfig.shadow.trim() === "" ? "none" : toolCallConfig.shadow)
|
|
136
|
-
: "var(--persona-tool-bubble-shadow
|
|
148
|
+
: "var(--persona-tool-bubble-shadow)";
|
|
137
149
|
|
|
138
150
|
if (!tool) {
|
|
139
|
-
return bubble;
|
|
151
|
+
return applyActivityRow(bubble, message, config, "tool");
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
const toolDisplayConfig = config?.features?.toolCallDisplay ?? {};
|
|
143
|
-
const expandable = toolDisplayConfig.expandable
|
|
144
|
-
const expanded = expandable &&
|
|
155
|
+
const expandable = toolDisplayConfig.expandable ?? DEFAULT_TOOL_CALL_DISPLAY.expandable;
|
|
156
|
+
const expanded = expandable && expansionState.has(message.id);
|
|
145
157
|
const { summary, previewText, isActive } = getToolSummaryText(message, config);
|
|
146
158
|
|
|
147
159
|
const header = createExpandableHeader({ expandable, expanded, bubbleType: "tool" });
|
|
@@ -181,7 +193,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
181
193
|
toolCall: tool,
|
|
182
194
|
defaultSummary: summary,
|
|
183
195
|
previewText,
|
|
184
|
-
collapsedMode: toolDisplayConfig.collapsedMode ??
|
|
196
|
+
collapsedMode: toolDisplayConfig.collapsedMode ?? DEFAULT_TOOL_CALL_DISPLAY.collapsedMode,
|
|
185
197
|
isActive,
|
|
186
198
|
config: config ?? {},
|
|
187
199
|
elapsed: computeToolElapsed(tool),
|
|
@@ -198,7 +210,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
// Apply loading animation when tool is active and no custom HTMLElement was provided
|
|
201
|
-
const loadingAnimation = toolDisplayConfig.loadingAnimation ??
|
|
213
|
+
const loadingAnimation = toolDisplayConfig.loadingAnimation ?? DEFAULT_TOOL_CALL_DISPLAY.loadingAnimation;
|
|
202
214
|
const activeTemplate = toolCallConfig.activeTextTemplate;
|
|
203
215
|
const completeTemplate = toolCallConfig.completeTextTemplate;
|
|
204
216
|
const currentTemplate = isActive ? activeTemplate : completeTemplate;
|
|
@@ -280,7 +292,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
|
|
283
|
-
const iconColor = toolChevronColor(toolCallConfig);
|
|
295
|
+
const iconColor = toolChevronColor(toolCallConfig, activityVariant(config, "tool") === "row");
|
|
284
296
|
const toggleIcon = appendHeaderToggle(header, headerContent, {
|
|
285
297
|
expandable,
|
|
286
298
|
expanded,
|
|
@@ -308,7 +320,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
308
320
|
|
|
309
321
|
if (!expandable) {
|
|
310
322
|
bubble.append(header, collapsedPreview);
|
|
311
|
-
return bubble;
|
|
323
|
+
return applyActivityRow(bubble, message, config, "tool");
|
|
312
324
|
}
|
|
313
325
|
|
|
314
326
|
const content = createElement(
|
|
@@ -333,109 +345,114 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
333
345
|
content.style.paddingBottom = toolCallConfig.contentPaddingY;
|
|
334
346
|
}
|
|
335
347
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
348
|
+
if (config.future?.v5Defaults === true) {
|
|
349
|
+
appendToolDetails(content, message, config);
|
|
350
|
+
} else {
|
|
351
|
+
// Add tool name at the top of content
|
|
352
|
+
if (tool.name) {
|
|
353
|
+
const toolName = createElement("div", "persona-text-xs persona-text-persona-muted persona-italic");
|
|
354
|
+
if (toolCallConfig.contentTextColor) {
|
|
355
|
+
toolName.style.color = toolCallConfig.contentTextColor;
|
|
356
|
+
} else if (toolCallConfig.headerTextColor) {
|
|
357
|
+
toolName.style.color = toolCallConfig.headerTextColor;
|
|
358
|
+
}
|
|
359
|
+
toolName.textContent = tool.name;
|
|
360
|
+
content.appendChild(toolName);
|
|
343
361
|
}
|
|
344
|
-
toolName.textContent = tool.name;
|
|
345
|
-
content.appendChild(toolName);
|
|
346
|
-
}
|
|
347
362
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
363
|
+
if (tool.args !== undefined) {
|
|
364
|
+
const argsBlock = createElement("div", "persona-space-y-1");
|
|
365
|
+
const argsLabel = createElement(
|
|
366
|
+
"div",
|
|
367
|
+
"persona-text-xs persona-text-persona-muted"
|
|
368
|
+
);
|
|
369
|
+
if (toolCallConfig.labelTextColor) {
|
|
370
|
+
argsLabel.style.color = toolCallConfig.labelTextColor;
|
|
371
|
+
}
|
|
372
|
+
argsLabel.textContent = "Arguments";
|
|
373
|
+
const argsPre = createElement(
|
|
374
|
+
"pre",
|
|
375
|
+
"persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-px-3 persona-py-2 persona-text-xs"
|
|
376
|
+
);
|
|
377
|
+
// Ensure font size matches header text (0.75rem / 12px)
|
|
378
|
+
argsPre.style.fontSize = "0.75rem";
|
|
379
|
+
argsPre.style.lineHeight = "1rem";
|
|
380
|
+
applyToolCodeBlockColors(argsPre, toolCallConfig);
|
|
381
|
+
argsPre.textContent = formatUnknownValue(tool.args);
|
|
382
|
+
argsBlock.append(argsLabel, argsPre);
|
|
383
|
+
content.appendChild(argsBlock);
|
|
356
384
|
}
|
|
357
|
-
argsLabel.textContent = "Arguments";
|
|
358
|
-
const argsPre = createElement(
|
|
359
|
-
"pre",
|
|
360
|
-
"persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-px-3 persona-py-2 persona-text-xs"
|
|
361
|
-
);
|
|
362
|
-
// Ensure font size matches header text (0.75rem / 12px)
|
|
363
|
-
argsPre.style.fontSize = "0.75rem";
|
|
364
|
-
argsPre.style.lineHeight = "1rem";
|
|
365
|
-
applyToolCodeBlockColors(argsPre, toolCallConfig);
|
|
366
|
-
argsPre.textContent = formatUnknownValue(tool.args);
|
|
367
|
-
argsBlock.append(argsLabel, argsPre);
|
|
368
|
-
content.appendChild(argsBlock);
|
|
369
|
-
}
|
|
370
385
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
386
|
+
if (tool.chunks && tool.chunks.length) {
|
|
387
|
+
const logsBlock = createElement("div", "persona-space-y-1");
|
|
388
|
+
const logsLabel = createElement(
|
|
389
|
+
"div",
|
|
390
|
+
"persona-text-xs persona-text-persona-muted"
|
|
391
|
+
);
|
|
392
|
+
if (toolCallConfig.labelTextColor) {
|
|
393
|
+
logsLabel.style.color = toolCallConfig.labelTextColor;
|
|
394
|
+
}
|
|
395
|
+
logsLabel.textContent = "Activity";
|
|
396
|
+
const logsPre = createElement(
|
|
397
|
+
"pre",
|
|
398
|
+
"persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-px-3 persona-py-2 persona-text-xs"
|
|
399
|
+
);
|
|
400
|
+
// Ensure font size matches header text (0.75rem / 12px)
|
|
401
|
+
logsPre.style.fontSize = "0.75rem";
|
|
402
|
+
logsPre.style.lineHeight = "1rem";
|
|
403
|
+
applyToolCodeBlockColors(logsPre, toolCallConfig);
|
|
404
|
+
logsPre.textContent = tool.chunks.join("");
|
|
405
|
+
logsBlock.append(logsLabel, logsPre);
|
|
406
|
+
content.appendChild(logsBlock);
|
|
379
407
|
}
|
|
380
|
-
logsLabel.textContent = "Activity";
|
|
381
|
-
const logsPre = createElement(
|
|
382
|
-
"pre",
|
|
383
|
-
"persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-px-3 persona-py-2 persona-text-xs"
|
|
384
|
-
);
|
|
385
|
-
// Ensure font size matches header text (0.75rem / 12px)
|
|
386
|
-
logsPre.style.fontSize = "0.75rem";
|
|
387
|
-
logsPre.style.lineHeight = "1rem";
|
|
388
|
-
applyToolCodeBlockColors(logsPre, toolCallConfig);
|
|
389
|
-
logsPre.textContent = tool.chunks.join("");
|
|
390
|
-
logsBlock.append(logsLabel, logsPre);
|
|
391
|
-
content.appendChild(logsBlock);
|
|
392
|
-
}
|
|
393
408
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
409
|
+
if (tool.success === false) {
|
|
410
|
+
content.appendChild(createNode("div", {
|
|
411
|
+
className: "persona-text-sm persona-whitespace-pre-wrap",
|
|
412
|
+
text: tool.error || "Tool failed",
|
|
413
|
+
attrs: { "data-persona-tool-error": "" },
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
401
416
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
if (tool.status === "complete" && tool.result !== undefined) {
|
|
418
|
+
const resultBlock = createElement("div", "persona-space-y-1");
|
|
419
|
+
const resultLabel = createElement(
|
|
420
|
+
"div",
|
|
421
|
+
"persona-text-xs persona-text-persona-muted"
|
|
422
|
+
);
|
|
423
|
+
if (toolCallConfig.labelTextColor) {
|
|
424
|
+
resultLabel.style.color = toolCallConfig.labelTextColor;
|
|
425
|
+
}
|
|
426
|
+
resultLabel.textContent = "Result";
|
|
427
|
+
const resultPre = createElement(
|
|
428
|
+
"pre",
|
|
429
|
+
"persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-px-3 persona-py-2 persona-text-xs"
|
|
430
|
+
);
|
|
431
|
+
// Ensure font size matches header text (0.75rem / 12px)
|
|
432
|
+
resultPre.style.fontSize = "0.75rem";
|
|
433
|
+
resultPre.style.lineHeight = "1rem";
|
|
434
|
+
applyToolCodeBlockColors(resultPre, toolCallConfig);
|
|
435
|
+
resultPre.textContent = formatUnknownValue(tool.result);
|
|
436
|
+
resultBlock.append(resultLabel, resultPre);
|
|
437
|
+
content.appendChild(resultBlock);
|
|
410
438
|
}
|
|
411
|
-
resultLabel.textContent = "Result";
|
|
412
|
-
const resultPre = createElement(
|
|
413
|
-
"pre",
|
|
414
|
-
"persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-px-3 persona-py-2 persona-text-xs"
|
|
415
|
-
);
|
|
416
|
-
// Ensure font size matches header text (0.75rem / 12px)
|
|
417
|
-
resultPre.style.fontSize = "0.75rem";
|
|
418
|
-
resultPre.style.lineHeight = "1rem";
|
|
419
|
-
applyToolCodeBlockColors(resultPre, toolCallConfig);
|
|
420
|
-
resultPre.textContent = formatUnknownValue(tool.result);
|
|
421
|
-
resultBlock.append(resultLabel, resultPre);
|
|
422
|
-
content.appendChild(resultBlock);
|
|
423
|
-
}
|
|
424
439
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
440
|
+
if (tool.status === "complete" && typeof tool.duration === "number") {
|
|
441
|
+
const duration = createElement(
|
|
442
|
+
"div",
|
|
443
|
+
"persona-text-xs persona-text-persona-muted"
|
|
444
|
+
);
|
|
445
|
+
if (toolCallConfig.contentTextColor) {
|
|
446
|
+
duration.style.color = toolCallConfig.contentTextColor;
|
|
447
|
+
}
|
|
448
|
+
duration.textContent = `Duration: ${tool.duration}ms`;
|
|
449
|
+
content.appendChild(duration);
|
|
432
450
|
}
|
|
433
|
-
|
|
434
|
-
content.appendChild(duration);
|
|
451
|
+
|
|
435
452
|
}
|
|
436
453
|
|
|
437
454
|
applyExpansionDisplay({ expanded, header, toggleIcon, content, collapsedPreview, iconColor });
|
|
438
455
|
|
|
439
456
|
bubble.append(header, collapsedPreview, content);
|
|
440
|
-
return bubble;
|
|
457
|
+
return applyActivityRow(bubble, message, config, "tool");
|
|
441
458
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createToolBubble } from "./tool-bubble";
|
|
5
|
+
import { copyToolDetail } from "./tool-details";
|
|
6
|
+
import type { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
7
|
+
|
|
8
|
+
const message = (tool: Partial<NonNullable<AgentWidgetMessage["toolCall"]>> = {}): AgentWidgetMessage => ({
|
|
9
|
+
id: "tool", role: "assistant", content: "", createdAt: "2026-01-01", variant: "tool",
|
|
10
|
+
toolCall: { id: "call", name: "Search", status: "complete", ...tool },
|
|
11
|
+
});
|
|
12
|
+
const render = (tool: Partial<NonNullable<AgentWidgetMessage["toolCall"]>>, config: AgentWidgetConfig = {}) =>
|
|
13
|
+
createToolBubble(message(tool), { future: { v5Defaults: true }, ...config }, new Set(["tool"]));
|
|
14
|
+
afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); });
|
|
15
|
+
|
|
16
|
+
describe("V5 tool details", () => {
|
|
17
|
+
it.each([undefined, null, "", " ", {}, [], "{}", "[]", "null"].map(value => [value]))("omits empty request %j", args => {
|
|
18
|
+
const bubble = render({ args, result: "Done" });
|
|
19
|
+
expect(bubble.querySelector('[data-persona-tool-detail="request"]')).toBeNull();
|
|
20
|
+
expect(bubble.querySelector('[data-persona-tool-detail="response"] pre')?.textContent).toBe("Done");
|
|
21
|
+
});
|
|
22
|
+
it.each([0, false, { count: 0 }])("keeps meaningful request %j", args => {
|
|
23
|
+
expect(render({ args }).querySelector('[data-persona-tool-detail="request"]')).not.toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it("shows chunks during streaming and replaces them with the final result", () => {
|
|
26
|
+
const running = render({ status: "running", chunks: ["Found ", "one"], result: "stale" });
|
|
27
|
+
expect(running.querySelector("pre")?.textContent).toBe("Found one");
|
|
28
|
+
const done = render({ chunks: ["Found one"], result: { count: 1 } });
|
|
29
|
+
expect(done.querySelectorAll('[data-persona-tool-detail="response"]')).toHaveLength(1);
|
|
30
|
+
expect(done.querySelector("pre")?.textContent).toBe('{\n "count": 1\n}');
|
|
31
|
+
expect(done.querySelector(".persona-tool-detail-code")).not.toBeNull();
|
|
32
|
+
});
|
|
33
|
+
it("keeps errors and escapes output, with plain prose typography", () => {
|
|
34
|
+
const bubble = render({ result: '<img src=x onerror="alert(1)">', success: false, error: "Denied" });
|
|
35
|
+
expect(bubble.querySelector("img")).toBeNull();
|
|
36
|
+
expect(bubble.querySelector(".persona-tool-detail-code")).toBeNull();
|
|
37
|
+
expect(bubble.querySelector("[data-persona-tool-error]")?.textContent).toBe("Denied");
|
|
38
|
+
});
|
|
39
|
+
it("preserves V4 sections and explicit V5 styles", () => {
|
|
40
|
+
const legacy = createToolBubble(message({ args: {}, chunks: ["progress"], result: "done" }));
|
|
41
|
+
expect(legacy.textContent).toContain("Arguments");
|
|
42
|
+
expect(legacy.textContent).toContain("Activity");
|
|
43
|
+
expect(legacy.textContent).toContain("Result");
|
|
44
|
+
expect(legacy.querySelector(".persona-tool-detail")).toBeNull();
|
|
45
|
+
const custom = render({ result: "done" }, { toolCall: {
|
|
46
|
+
codeBlockBackgroundColor: "red", codeBlockTextColor: "blue", codeBlockBorderColor: "green", labelTextColor: "purple",
|
|
47
|
+
} });
|
|
48
|
+
expect(custom.querySelector<HTMLElement>(".persona-tool-detail")?.style.backgroundColor).toBe("red");
|
|
49
|
+
expect(custom.querySelector<HTMLElement>("pre")?.style.color).toBe("blue");
|
|
50
|
+
expect(custom.querySelector<HTMLElement>(".persona-tool-detail-label")?.style.color).toBe("purple");
|
|
51
|
+
});
|
|
52
|
+
it("uses a flat surface instead of inheriting markdown code boxes", () => {
|
|
53
|
+
const style = document.createElement("style");
|
|
54
|
+
style.textContent = readFileSync("src/styles/widget.css", "utf8");
|
|
55
|
+
document.head.appendChild(style);
|
|
56
|
+
const bubble = render({ result: "Plain response" });
|
|
57
|
+
document.body.appendChild(bubble);
|
|
58
|
+
try {
|
|
59
|
+
const pre = bubble.querySelector("pre")!;
|
|
60
|
+
expect(getComputedStyle(pre).padding).toBe("0px");
|
|
61
|
+
expect(["transparent", "rgba(0, 0, 0, 0)"]).toContain(getComputedStyle(pre).backgroundColor);
|
|
62
|
+
expect(getComputedStyle(pre).fontSize).toBe("13px");
|
|
63
|
+
} finally { bubble.remove(); style.remove(); }
|
|
64
|
+
});
|
|
65
|
+
it("copies all output, shows a check, and restores the copy icon", async () => {
|
|
66
|
+
vi.useFakeTimers();
|
|
67
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
68
|
+
vi.stubGlobal("navigator", { clipboard: { writeText } });
|
|
69
|
+
const text = "Long output\n".repeat(50);
|
|
70
|
+
const button = render({ result: text }).querySelector<HTMLButtonElement>(".persona-tool-detail-copy")!;
|
|
71
|
+
await copyToolDetail(button);
|
|
72
|
+
expect(writeText).toHaveBeenCalledWith(text);
|
|
73
|
+
expect(button.dataset.copied).toBe("true");
|
|
74
|
+
expect(button.getAttribute("aria-label")).toBe("Copied");
|
|
75
|
+
vi.advanceTimersByTime(1800);
|
|
76
|
+
expect(button.dataset.copied).toBeUndefined();
|
|
77
|
+
expect(button.getAttribute("aria-label")).toBe("Copy response");
|
|
78
|
+
});
|
|
79
|
+
it("handles unavailable clipboard without claiming success", async () => {
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
vi.stubGlobal("navigator", {});
|
|
82
|
+
const button = render({ result: "done" }).querySelector<HTMLButtonElement>("button[data-persona-copy-tool-detail]")!;
|
|
83
|
+
await copyToolDetail(button);
|
|
84
|
+
expect(button.getAttribute("aria-label")).toBe("Copy failed — try again");
|
|
85
|
+
expect(button.dataset.copied).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
2
|
+
import { createNode } from "../utils/dom";
|
|
3
|
+
import { formatUnknownValue } from "../utils/formatting";
|
|
4
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
5
|
+
|
|
6
|
+
const isEmptyRequest = (value: unknown): boolean => {
|
|
7
|
+
if (value == null) return true;
|
|
8
|
+
if (typeof value === "string") {
|
|
9
|
+
if (!value.trim()) return true;
|
|
10
|
+
try { return isEmptyRequest(JSON.parse(value)); } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
return typeof value === "object" && Object.keys(value).length === 0;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const isStructured = (value: unknown): boolean => {
|
|
16
|
+
if (value !== null && typeof value === "object") return true;
|
|
17
|
+
if (typeof value !== "string") return false;
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = JSON.parse(value);
|
|
20
|
+
return parsed !== null && typeof parsed === "object";
|
|
21
|
+
} catch { return false; }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Plain text only: tool output is untrusted and must never become HTML. */
|
|
25
|
+
export function appendToolDetails(content: HTMLElement, message: AgentWidgetMessage, config: AgentWidgetConfig): void {
|
|
26
|
+
const tool = message.toolCall;
|
|
27
|
+
if (!tool) return;
|
|
28
|
+
const styles = config.toolCall ?? {};
|
|
29
|
+
const section = (label: "Request" | "Response", value: unknown) => {
|
|
30
|
+
const block = createNode("section", {
|
|
31
|
+
className: "persona-tool-detail",
|
|
32
|
+
attrs: { "data-persona-tool-detail": label.toLowerCase() },
|
|
33
|
+
style: {
|
|
34
|
+
backgroundColor: styles.codeBlockBackgroundColor,
|
|
35
|
+
borderColor: styles.codeBlockBorderColor,
|
|
36
|
+
borderWidth: styles.codeBlockBorderColor ? "1px" : undefined,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const title = createNode("div", {
|
|
40
|
+
className: "persona-tool-detail-label",
|
|
41
|
+
text: label,
|
|
42
|
+
style: { color: styles.labelTextColor },
|
|
43
|
+
});
|
|
44
|
+
const text = formatUnknownValue(value);
|
|
45
|
+
const pre = createNode("pre", {
|
|
46
|
+
className: `persona-tool-detail-value${isStructured(value) ? " persona-tool-detail-code" : ""}`,
|
|
47
|
+
text,
|
|
48
|
+
attrs: text.length > 420 || text.split("\n").length > 7
|
|
49
|
+
? { tabindex: "0", "aria-label": `${label} content` } : {},
|
|
50
|
+
style: { color: styles.codeBlockTextColor ?? styles.contentTextColor },
|
|
51
|
+
});
|
|
52
|
+
const copy = createNode("button", {
|
|
53
|
+
className: "persona-tool-detail-copy",
|
|
54
|
+
attrs: { type: "button", "data-persona-copy-tool-detail": "", "aria-label": `Copy ${label.toLowerCase()}`, title: `Copy ${label.toLowerCase()}` },
|
|
55
|
+
});
|
|
56
|
+
const icon = renderLucideIcon("copy", 14, "currentColor", 1.7);
|
|
57
|
+
if (icon) copy.appendChild(icon);
|
|
58
|
+
block.append(title, pre, copy);
|
|
59
|
+
content.appendChild(block);
|
|
60
|
+
};
|
|
61
|
+
if (!isEmptyRequest(tool.args)) section("Request", tool.args);
|
|
62
|
+
const response = tool.status === "complete" && tool.result !== undefined
|
|
63
|
+
? tool.result : tool.chunks?.join("");
|
|
64
|
+
if (response !== undefined && (typeof response !== "string" || response.trim())) section("Response", response);
|
|
65
|
+
if (tool.success === false) {
|
|
66
|
+
content.appendChild(createNode("div", {
|
|
67
|
+
className: "persona-text-sm persona-whitespace-pre-wrap",
|
|
68
|
+
text: tool.error || "Tool failed",
|
|
69
|
+
attrs: { "data-persona-tool-error": "", role: "status" },
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const copyResets = new WeakMap<HTMLButtonElement, ReturnType<typeof setTimeout>>();
|
|
75
|
+
|
|
76
|
+
/** Delegated by the transcript so copying works after streaming DOM morphs. */
|
|
77
|
+
export async function copyToolDetail(button: HTMLButtonElement): Promise<void> {
|
|
78
|
+
const block = button.closest("[data-persona-tool-detail]");
|
|
79
|
+
const value = block?.querySelector("pre");
|
|
80
|
+
if (!value) return;
|
|
81
|
+
const label = `Copy ${block?.getAttribute("data-persona-tool-detail")}`;
|
|
82
|
+
clearTimeout(copyResets.get(button));
|
|
83
|
+
try {
|
|
84
|
+
await navigator.clipboard.writeText(value.textContent ?? "");
|
|
85
|
+
button.dataset.copied = "true";
|
|
86
|
+
button.setAttribute("aria-label", "Copied");
|
|
87
|
+
button.title = "Copied";
|
|
88
|
+
const icon = renderLucideIcon("check", 14, "currentColor", 2);
|
|
89
|
+
if (icon) button.replaceChildren(icon);
|
|
90
|
+
} catch {
|
|
91
|
+
button.setAttribute("aria-label", "Copy failed — try again");
|
|
92
|
+
button.title = "Copy failed — try again";
|
|
93
|
+
}
|
|
94
|
+
copyResets.set(button, setTimeout(() => {
|
|
95
|
+
delete button.dataset.copied;
|
|
96
|
+
button.setAttribute("aria-label", label);
|
|
97
|
+
button.title = label;
|
|
98
|
+
const icon = renderLucideIcon("copy", 14, "currentColor", 1.7);
|
|
99
|
+
if (icon) button.replaceChildren(icon);
|
|
100
|
+
copyResets.delete(button);
|
|
101
|
+
}, 1800));
|
|
102
|
+
}
|
|
@@ -151,6 +151,14 @@ describe("composer lift animation", () => {
|
|
|
151
151
|
footer.remove();
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
it("accepts the centered layout's 320ms transition", () => {
|
|
155
|
+
const footer = makeFooter();
|
|
156
|
+
const animate = stubAnimate(footer);
|
|
157
|
+
animateComposerLiftChange(footer, 240, "drop", 320);
|
|
158
|
+
expect(animate.mock.calls[0]![1]).toMatchObject({ duration: 320 });
|
|
159
|
+
footer.remove();
|
|
160
|
+
});
|
|
161
|
+
|
|
154
162
|
it("reverses the travel for the rise", () => {
|
|
155
163
|
const footer = makeFooter();
|
|
156
164
|
const animate = stubAnimate(footer);
|
|
@@ -177,6 +177,7 @@ export const applyWelcomeConfig = (
|
|
|
177
177
|
applyWelcomeIcon(elements.iconHolder, resolved.icon);
|
|
178
178
|
elements.host.setAttribute("data-persona-welcome-variant", resolved.variant);
|
|
179
179
|
elements.host.setAttribute("data-persona-welcome-dismiss", resolved.dismiss);
|
|
180
|
+
elements.host.setAttribute("data-persona-welcome-layout", resolved.layout ?? "top");
|
|
180
181
|
elements.host.setAttribute(
|
|
181
182
|
"data-persona-welcome-anchor",
|
|
182
183
|
resolved.anchor ?? "bottom"
|
|
@@ -422,7 +423,8 @@ export const animateWelcomeOut = (
|
|
|
422
423
|
export const animateComposerLiftChange = (
|
|
423
424
|
footer: HTMLElement,
|
|
424
425
|
distance: number,
|
|
425
|
-
direction: "drop" | "rise"
|
|
426
|
+
direction: "drop" | "rise",
|
|
427
|
+
duration = 260
|
|
426
428
|
): Animation | null => {
|
|
427
429
|
if (
|
|
428
430
|
distance <= 1 ||
|
|
@@ -437,6 +439,6 @@ export const animateComposerLiftChange = (
|
|
|
437
439
|
// risk pinning a stale transform across a later re-show.
|
|
438
440
|
return footer.animate(
|
|
439
441
|
[{ transform: `translateY(${offset}px)` }, { transform: "none" }],
|
|
440
|
-
{ duration
|
|
442
|
+
{ duration, easing: "cubic-bezier(0.2, 0, 0, 1)", fill: "none" }
|
|
441
443
|
);
|
|
442
444
|
};
|