@runtypelabs/persona 4.23.1 → 4.24.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-9Vxm-PPz.d.cts → types-Db61aI9J.d.cts} +25 -2
- package/dist/animations/{types-9Vxm-PPz.d.ts → types-Db61aI9J.d.ts} +25 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +25 -2
- package/dist/animations-extra.d.ts +25 -2
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +261 -40
- package/dist/approval-ui.d.ts +261 -40
- 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 +256 -28
- package/dist/artifacts-ui.d.ts +256 -28
- 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 +256 -28
- package/dist/context-mentions.d.ts +256 -28
- package/dist/event-stream-view.d.cts +256 -28
- package/dist/event-stream-view.d.ts +256 -28
- package/dist/forms-ui.d.cts +291 -30
- package/dist/forms-ui.d.ts +291 -30
- package/dist/history-view.d.cts +25 -2
- package/dist/history-view.d.ts +25 -2
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -51
- package/dist/index.d.ts +337 -51
- 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 +256 -28
- package/dist/session-reconnect.d.ts +256 -28
- package/dist/smart-dom-reader.d.cts +256 -28
- package/dist/smart-dom-reader.d.ts +256 -28
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +263 -28
- package/dist/theme-editor-preview.d.ts +263 -28
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +276 -30
- package/dist/theme-editor.d.ts +276 -30
- 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/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.tool-pair-replay.test.ts +325 -0
- package/src/client.ts +154 -61
- 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 +28 -13
- package/src/index-core.ts +2 -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 +61 -9
- 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 +179 -23
- 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 +363 -74
- package/src/ui.v5-defaults.test.ts +144 -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/utils/tool-pair-replay.ts +100 -0
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
|
@@ -115,7 +115,6 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
115
115
|
const composerForm = createNode("form", {
|
|
116
116
|
className: "persona-widget-composer persona-pill-composer",
|
|
117
117
|
attrs: { "data-persona-composer-form": "" },
|
|
118
|
-
style: { outline: "none" },
|
|
119
118
|
});
|
|
120
119
|
|
|
121
120
|
// Three columns of the grid: [paperclip?] · textarea · mic + send.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { applyActivityRow, activityDisplay, activityVariant, activityDuration } from "./activity-row";
|
|
1
2
|
import { createElement } from "../utils/dom";
|
|
2
3
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
4
|
import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
5
|
+
import { DEFAULT_REASONING_DISPLAY } from "../defaults";
|
|
4
6
|
import { describeReasonStatus, computeReasoningElapsed, parseFormattedTemplate } from "../utils/formatting";
|
|
5
7
|
import { appendCharSpans } from "../utils/tool-loading-animation";
|
|
6
8
|
import {
|
|
@@ -12,9 +14,6 @@ import {
|
|
|
12
14
|
updateExpandableBubbleUI,
|
|
13
15
|
} from "./expandable-bubble";
|
|
14
16
|
|
|
15
|
-
// Expansion state per widget instance
|
|
16
|
-
export const reasoningExpansionState = new Set<string>();
|
|
17
|
-
|
|
18
17
|
const getReasoningPreviewText = (message: AgentWidgetMessage, maxLines: number): string => {
|
|
19
18
|
const text = message.reasoning?.chunks.join("").trim() ?? "";
|
|
20
19
|
if (!text) return "";
|
|
@@ -27,32 +26,45 @@ const getReasoningPreviewText = (message: AgentWidgetMessage, maxLines: number):
|
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
// Helper function to update reasoning bubble UI after expansion state changes
|
|
30
|
-
export const updateReasoningBubbleUI = (
|
|
29
|
+
export const updateReasoningBubbleUI = (
|
|
30
|
+
messageId: string,
|
|
31
|
+
bubble: HTMLElement,
|
|
32
|
+
expansionState: Set<string>
|
|
33
|
+
): void => {
|
|
31
34
|
updateExpandableBubbleUI(messageId, bubble, {
|
|
32
|
-
stateSet:
|
|
35
|
+
stateSet: expansionState,
|
|
33
36
|
previewKind: "reasoning",
|
|
34
37
|
iconColor: "currentColor",
|
|
35
38
|
});
|
|
36
39
|
};
|
|
37
40
|
|
|
38
|
-
export const createReasoningBubble = (
|
|
41
|
+
export const createReasoningBubble = (
|
|
42
|
+
message: AgentWidgetMessage,
|
|
43
|
+
config?: AgentWidgetConfig,
|
|
44
|
+
expansionState = new Set<string>()
|
|
45
|
+
): HTMLElement => {
|
|
46
|
+
config = activityDisplay(config, "reasoning");
|
|
39
47
|
const reasoning = message.reasoning;
|
|
40
48
|
const bubble = createExpandableBubbleShell("persona-reasoning-bubble", message.id);
|
|
41
49
|
|
|
42
50
|
if (!reasoning) {
|
|
43
|
-
return bubble;
|
|
51
|
+
return applyActivityRow(bubble, message, config, "reasoning");
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
const reasoningDisplayConfig = config?.features?.reasoningDisplay ?? {};
|
|
47
|
-
const expandable = reasoningDisplayConfig.expandable
|
|
48
|
-
const expanded = expandable &&
|
|
55
|
+
const expandable = reasoningDisplayConfig.expandable ?? DEFAULT_REASONING_DISPLAY.expandable;
|
|
56
|
+
const expanded = expandable && expansionState.has(message.id);
|
|
49
57
|
const isActive = reasoning.status !== "complete";
|
|
50
|
-
const previewText = getReasoningPreviewText(
|
|
58
|
+
const previewText = getReasoningPreviewText(
|
|
59
|
+
message,
|
|
60
|
+
reasoningDisplayConfig.previewMaxLines ?? DEFAULT_REASONING_DISPLAY.previewMaxLines
|
|
61
|
+
);
|
|
51
62
|
const header = createExpandableHeader({ expandable, expanded, bubbleType: "reasoning" });
|
|
52
63
|
|
|
53
64
|
const headerContent = createElement("div", "persona-flex persona-flex-col persona-text-left");
|
|
54
65
|
const title = createElement("span", "persona-text-xs persona-text-persona-primary");
|
|
55
|
-
const
|
|
66
|
+
const row = activityVariant(config, "reasoning") === "row";
|
|
67
|
+
const defaultSummary = row && !isActive ? `Thought for ${activityDuration(reasoning.durationMs ?? (reasoning.completedAt !== undefined && reasoning.startedAt !== undefined ? reasoning.completedAt - reasoning.startedAt : undefined))}` : "Thinking...";
|
|
56
68
|
const reasoningConfig = config?.reasoning ?? {};
|
|
57
69
|
|
|
58
70
|
// Elapsed helpers: defined early so they're available to renderCollapsedSummary
|
|
@@ -88,10 +100,10 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
88
100
|
// Status span: used in the legacy (no-template) path
|
|
89
101
|
const status = createElement("span", "persona-text-xs persona-text-persona-primary");
|
|
90
102
|
status.textContent = describeReasonStatus(reasoning);
|
|
91
|
-
headerContent.appendChild(status);
|
|
103
|
+
if (!row) headerContent.appendChild(status);
|
|
92
104
|
|
|
93
105
|
// Template and animation support
|
|
94
|
-
const loadingAnimation = reasoningDisplayConfig.loadingAnimation ??
|
|
106
|
+
const loadingAnimation = reasoningDisplayConfig.loadingAnimation ?? DEFAULT_REASONING_DISPLAY.loadingAnimation;
|
|
95
107
|
const activeTemplate = reasoningConfig.activeTextTemplate;
|
|
96
108
|
const completeTemplate = reasoningConfig.completeTextTemplate;
|
|
97
109
|
const currentTemplate = isActive ? activeTemplate : completeTemplate;
|
|
@@ -190,12 +202,12 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
190
202
|
}
|
|
191
203
|
|
|
192
204
|
// Legacy: hide title on complete, show status
|
|
193
|
-
if (reasoning.status === "complete") {
|
|
205
|
+
if (reasoning.status === "complete" && !row) {
|
|
194
206
|
title.style.display = "none";
|
|
195
207
|
}
|
|
196
208
|
} else if (!skipCustomElement) {
|
|
197
209
|
// Legacy path: no template, no animation
|
|
198
|
-
if (reasoning.status === "complete") {
|
|
210
|
+
if (reasoning.status === "complete" && !row) {
|
|
199
211
|
title.style.display = "none";
|
|
200
212
|
} else {
|
|
201
213
|
title.style.display = "";
|
|
@@ -246,7 +258,7 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
246
258
|
|
|
247
259
|
if (!expandable) {
|
|
248
260
|
bubble.append(header, collapsedPreview);
|
|
249
|
-
return bubble;
|
|
261
|
+
return applyActivityRow(bubble, message, config, "reasoning");
|
|
250
262
|
}
|
|
251
263
|
|
|
252
264
|
const content = createElement(
|
|
@@ -270,5 +282,5 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
270
282
|
applyExpansionDisplay({ expanded, header, toggleIcon, content, collapsedPreview, iconColor });
|
|
271
283
|
|
|
272
284
|
bubble.append(header, collapsedPreview, content);
|
|
273
|
-
return bubble;
|
|
285
|
+
return applyActivityRow(bubble, message, config, "reasoning");
|
|
274
286
|
};
|
|
@@ -257,6 +257,7 @@ describe("legacy suggestionChipsConfig scoping", () => {
|
|
|
257
257
|
expect(button.style.paddingLeft).toBe("12px");
|
|
258
258
|
expect(button.style.paddingTop).toBe("6px");
|
|
259
259
|
expect(button.style.fontWeight).toBe("500");
|
|
260
|
+
expect(button.style.fontFamily).toBe("");
|
|
260
261
|
});
|
|
261
262
|
|
|
262
263
|
it("does not let chip config override card and list padding tokens", () => {
|
|
@@ -97,19 +97,6 @@ export const normalizeSuggestion = (
|
|
|
97
97
|
};
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
const fontFamilyValue = (
|
|
101
|
-
family: "sans-serif" | "serif" | "mono"
|
|
102
|
-
): string => {
|
|
103
|
-
switch (family) {
|
|
104
|
-
case "serif":
|
|
105
|
-
return 'Georgia, "Times New Roman", Times, serif';
|
|
106
|
-
case "mono":
|
|
107
|
-
return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
|
|
108
|
-
default:
|
|
109
|
-
return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
100
|
// Module-level, keyed on the widget config: one widget owns several suggestion
|
|
114
101
|
// managers, so a per-instance flag hints once per surface instead of once.
|
|
115
102
|
const hintedOverflowVariant = new WeakSet<AgentWidgetConfig>();
|
|
@@ -264,9 +251,6 @@ export const createSuggestionElement = (
|
|
|
264
251
|
// Legacy `suggestionChipsConfig` compat is chip-only: its inline
|
|
265
252
|
// styles would otherwise override the card/list padding tokens.
|
|
266
253
|
if (variant === "chip") {
|
|
267
|
-
if (chipsConfig?.fontFamily) {
|
|
268
|
-
button.style.fontFamily = fontFamilyValue(chipsConfig.fontFamily);
|
|
269
|
-
}
|
|
270
254
|
if (chipsConfig?.fontWeight) {
|
|
271
255
|
button.style.fontWeight = chipsConfig.fontWeight;
|
|
272
256
|
}
|
|
@@ -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
|
+
});
|