@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.
- package/dist/index.cjs +45 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.global.js +67 -64
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +45 -42
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +959 -214
- package/dist/theme-editor.d.cts +157 -3
- package/dist/theme-editor.d.ts +157 -3
- package/dist/theme-editor.js +955 -214
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +8 -0
- package/dist/theme-reference.d.ts +8 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +154 -0
- package/package.json +1 -1
- package/src/client.test.ts +312 -1
- package/src/client.ts +247 -24
- package/src/components/messages.ts +1 -1
- package/src/components/reasoning-bubble.ts +117 -28
- package/src/components/tool-bubble.ts +161 -27
- package/src/defaults.ts +12 -0
- package/src/styles/widget.css +154 -0
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/preview-utils.test.ts +58 -0
- package/src/theme-editor/preview-utils.ts +220 -4
- package/src/theme-editor/sections.test.ts +20 -0
- package/src/theme-editor/sections.ts +10 -0
- package/src/theme-reference.ts +8 -3
- package/src/tool-call-display-defaults.test.ts +23 -0
- package/src/types.ts +155 -0
- package/src/ui.attachments-drop.test.ts +188 -0
- package/src/ui.scroll.test.ts +150 -0
- package/src/ui.tool-display.test.ts +204 -0
- package/src/ui.ts +275 -7
- package/src/utils/message-fingerprint.test.ts +17 -0
- package/src/utils/message-fingerprint.ts +13 -1
package/src/ui.ts
CHANGED
|
@@ -147,6 +147,19 @@ function getClipboardImageFiles(clipboardData: DataTransfer | null): File[] {
|
|
|
147
147
|
return imageFiles;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
function dataTransferHasFiles(
|
|
151
|
+
dataTransfer: DataTransfer | null
|
|
152
|
+
): dataTransfer is DataTransfer {
|
|
153
|
+
if (!dataTransfer) return false;
|
|
154
|
+
const types = dataTransfer.types;
|
|
155
|
+
if (!types) return false;
|
|
156
|
+
// Real browsers return DOMStringList which has .contains(); test polyfills use plain arrays.
|
|
157
|
+
if (typeof (types as unknown as { contains?: unknown }).contains === "function") {
|
|
158
|
+
return (types as unknown as DOMStringList).contains("Files");
|
|
159
|
+
}
|
|
160
|
+
return Array.from(types).includes("Files");
|
|
161
|
+
}
|
|
162
|
+
|
|
150
163
|
// ============================================================================
|
|
151
164
|
// PERSIST STATE HELPERS
|
|
152
165
|
// ============================================================================
|
|
@@ -389,11 +402,43 @@ const buildPostprocessor = (
|
|
|
389
402
|
};
|
|
390
403
|
};
|
|
391
404
|
|
|
405
|
+
function buildDropOverlay(
|
|
406
|
+
dropCfg?: NonNullable<AgentWidgetConfig["attachments"]>["dropOverlay"]
|
|
407
|
+
): HTMLElement {
|
|
408
|
+
const overlay = createElement("div", "persona-attachment-drop-overlay");
|
|
409
|
+
if (dropCfg?.background) overlay.style.setProperty("--persona-drop-overlay-bg", dropCfg.background);
|
|
410
|
+
if (dropCfg?.backdropBlur !== undefined) overlay.style.setProperty("--persona-drop-overlay-blur", dropCfg.backdropBlur);
|
|
411
|
+
if (dropCfg?.border) overlay.style.setProperty("--persona-drop-overlay-border", dropCfg.border);
|
|
412
|
+
if (dropCfg?.borderRadius) overlay.style.setProperty("--persona-drop-overlay-radius", dropCfg.borderRadius);
|
|
413
|
+
if (dropCfg?.inset) overlay.style.setProperty("--persona-drop-overlay-inset", dropCfg.inset);
|
|
414
|
+
if (dropCfg?.labelSize) overlay.style.setProperty("--persona-drop-overlay-label-size", dropCfg.labelSize);
|
|
415
|
+
if (dropCfg?.labelColor) overlay.style.setProperty("--persona-drop-overlay-label-color", dropCfg.labelColor);
|
|
416
|
+
|
|
417
|
+
const iconName = dropCfg?.iconName ?? "upload";
|
|
418
|
+
const iconSize = dropCfg?.iconSize ?? "48px";
|
|
419
|
+
const iconColor = dropCfg?.iconColor ?? "rgba(59, 130, 246, 0.6)";
|
|
420
|
+
const iconStrokeWidth = dropCfg?.iconStrokeWidth ?? 0.5;
|
|
421
|
+
const iconSvg = renderLucideIcon(iconName, iconSize, iconColor, iconStrokeWidth);
|
|
422
|
+
if (iconSvg) overlay.appendChild(iconSvg);
|
|
423
|
+
|
|
424
|
+
if (dropCfg?.label) {
|
|
425
|
+
const labelEl = createElement("span", "persona-drop-overlay-label");
|
|
426
|
+
labelEl.textContent = dropCfg.label;
|
|
427
|
+
overlay.appendChild(labelEl);
|
|
428
|
+
}
|
|
429
|
+
return overlay;
|
|
430
|
+
}
|
|
431
|
+
|
|
392
432
|
export const createAgentExperience = (
|
|
393
433
|
mount: HTMLElement,
|
|
394
434
|
initialConfig?: AgentWidgetConfig,
|
|
395
435
|
runtimeOptions?: { debugTools?: boolean }
|
|
396
436
|
): Controller => {
|
|
437
|
+
if (mount == null) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
"createAgentExperience: mount must be a non-null HTMLElement (e.g. pass document.getElementById(\"my-root\") after the node exists)."
|
|
440
|
+
);
|
|
441
|
+
}
|
|
397
442
|
// Preserve original mount id as data attribute for window event instance scoping
|
|
398
443
|
if (mount.id && !mount.getAttribute("data-persona-instance")) {
|
|
399
444
|
mount.setAttribute("data-persona-instance", mount.id);
|
|
@@ -870,8 +915,21 @@ export const createAgentExperience = (
|
|
|
870
915
|
return composerElements.footer;
|
|
871
916
|
},
|
|
872
917
|
onSubmit: (text: string) => {
|
|
873
|
-
if (session
|
|
874
|
-
|
|
918
|
+
if (!session || session.isStreaming()) return;
|
|
919
|
+
const value = text.trim();
|
|
920
|
+
const hasAttachments = attachmentManager?.hasAttachments() ?? false;
|
|
921
|
+
if (!value && !hasAttachments) return;
|
|
922
|
+
let contentParts: ContentPart[] | undefined;
|
|
923
|
+
if (hasAttachments) {
|
|
924
|
+
contentParts = [];
|
|
925
|
+
contentParts.push(...attachmentManager!.getContentParts());
|
|
926
|
+
if (value) {
|
|
927
|
+
contentParts.push(createTextPart(value));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
session.sendMessage(value, { contentParts });
|
|
931
|
+
if (hasAttachments) {
|
|
932
|
+
attachmentManager!.clearAttachments();
|
|
875
933
|
}
|
|
876
934
|
},
|
|
877
935
|
streaming: false,
|
|
@@ -959,6 +1017,10 @@ export const createAgentExperience = (
|
|
|
959
1017
|
attachmentManager?.handleFileSelect(target.files);
|
|
960
1018
|
target.value = "";
|
|
961
1019
|
});
|
|
1020
|
+
|
|
1021
|
+
const dropCfg = config.attachments.dropOverlay;
|
|
1022
|
+
const overlay = buildDropOverlay(dropCfg);
|
|
1023
|
+
container.appendChild(overlay);
|
|
962
1024
|
}
|
|
963
1025
|
|
|
964
1026
|
// Slot system: allow custom content injection into specific regions
|
|
@@ -1925,6 +1987,7 @@ export const createAgentExperience = (
|
|
|
1925
1987
|
let lastScrollTop = 0;
|
|
1926
1988
|
let scrollRAF: number | null = null;
|
|
1927
1989
|
let isAutoScrolling = false;
|
|
1990
|
+
let hasPendingAutoScroll = false;
|
|
1928
1991
|
|
|
1929
1992
|
const USER_SCROLL_THRESHOLD = 1;
|
|
1930
1993
|
const BOTTOM_THRESHOLD = 8;
|
|
@@ -2041,6 +2104,7 @@ export const createAgentExperience = (
|
|
|
2041
2104
|
cancelAnimationFrame(scrollRAF);
|
|
2042
2105
|
scrollRAF = null;
|
|
2043
2106
|
}
|
|
2107
|
+
hasPendingAutoScroll = false;
|
|
2044
2108
|
cancelSmoothScroll();
|
|
2045
2109
|
};
|
|
2046
2110
|
|
|
@@ -2076,10 +2140,25 @@ export const createAgentExperience = (
|
|
|
2076
2140
|
|
|
2077
2141
|
if (!force && !isStreaming) return;
|
|
2078
2142
|
|
|
2079
|
-
|
|
2143
|
+
// Only cancel the pending schedule rAF — keep the ongoing smooth scroll
|
|
2144
|
+
// animation alive so isAutoScrolling stays true. This prevents scroll
|
|
2145
|
+
// events fired by DOM morphing (between cancel and the next rAF) from
|
|
2146
|
+
// being misinterpreted as user-initiated upward scrolls that would
|
|
2147
|
+
// permanently pause auto-follow during streaming.
|
|
2148
|
+
// smoothScrollToBottom() already calls cancelSmoothScroll() internally
|
|
2149
|
+
// before starting its new animation.
|
|
2150
|
+
if (scrollRAF !== null) {
|
|
2151
|
+
cancelAnimationFrame(scrollRAF);
|
|
2152
|
+
scrollRAF = null;
|
|
2153
|
+
}
|
|
2080
2154
|
|
|
2155
|
+
// Treat the render -> next-rAF window as programmatic scrolling too.
|
|
2156
|
+
// This prevents layout/scroll-anchoring scroll events fired before the
|
|
2157
|
+
// actual smooth scroll starts from being misread as user intent.
|
|
2158
|
+
hasPendingAutoScroll = true;
|
|
2081
2159
|
scrollRAF = requestAnimationFrame(() => {
|
|
2082
2160
|
scrollRAF = null;
|
|
2161
|
+
hasPendingAutoScroll = false;
|
|
2083
2162
|
if (!autoFollow.isFollowing()) return;
|
|
2084
2163
|
smoothScrollToBottom(getScrollableContainer(), force ? 220 : 140);
|
|
2085
2164
|
});
|
|
@@ -2213,6 +2292,18 @@ export const createAgentExperience = (
|
|
|
2213
2292
|
};
|
|
2214
2293
|
|
|
2215
2294
|
const inlineLoadingRenderer = getInlineLoadingIndicatorRenderer();
|
|
2295
|
+
const appendRenderedValue = (
|
|
2296
|
+
containerEl: HTMLElement,
|
|
2297
|
+
value: HTMLElement | string | null | undefined
|
|
2298
|
+
): boolean => {
|
|
2299
|
+
if (value == null) return false;
|
|
2300
|
+
if (typeof value === "string") {
|
|
2301
|
+
containerEl.textContent = value;
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
containerEl.appendChild(value);
|
|
2305
|
+
return true;
|
|
2306
|
+
};
|
|
2216
2307
|
|
|
2217
2308
|
// Track active message IDs for cache pruning
|
|
2218
2309
|
const activeMessageIds = new Set<string>();
|
|
@@ -2255,7 +2346,7 @@ export const createAgentExperience = (
|
|
|
2255
2346
|
if (!showReasoning) return;
|
|
2256
2347
|
bubble = matchingPlugin.renderReasoning({
|
|
2257
2348
|
message,
|
|
2258
|
-
defaultRenderer: () => createReasoningBubble(message),
|
|
2349
|
+
defaultRenderer: () => createReasoningBubble(message, config),
|
|
2259
2350
|
config
|
|
2260
2351
|
});
|
|
2261
2352
|
} else if (message.variant === "tool" && message.toolCall && matchingPlugin.renderToolCall) {
|
|
@@ -2371,7 +2462,7 @@ export const createAgentExperience = (
|
|
|
2371
2462
|
if (!bubble) {
|
|
2372
2463
|
if (message.variant === "reasoning" && message.reasoning) {
|
|
2373
2464
|
if (!showReasoning) return;
|
|
2374
|
-
bubble = createReasoningBubble(message);
|
|
2465
|
+
bubble = createReasoningBubble(message, config);
|
|
2375
2466
|
} else if (message.variant === "tool" && message.toolCall) {
|
|
2376
2467
|
if (!showToolCalls) return;
|
|
2377
2468
|
bubble = createToolBubble(message, config);
|
|
@@ -2428,6 +2519,86 @@ export const createAgentExperience = (
|
|
|
2428
2519
|
tempContainer.appendChild(wrapper);
|
|
2429
2520
|
});
|
|
2430
2521
|
|
|
2522
|
+
if (config.features?.toolCallDisplay?.grouped) {
|
|
2523
|
+
const toolGroups: AgentWidgetMessage[][] = [];
|
|
2524
|
+
let currentGroup: AgentWidgetMessage[] = [];
|
|
2525
|
+
|
|
2526
|
+
messages.forEach((message) => {
|
|
2527
|
+
if (message.variant === "tool" && message.toolCall && showToolCalls) {
|
|
2528
|
+
currentGroup.push(message);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
if (currentGroup.length > 1) {
|
|
2532
|
+
toolGroups.push(currentGroup);
|
|
2533
|
+
}
|
|
2534
|
+
currentGroup = [];
|
|
2535
|
+
});
|
|
2536
|
+
if (currentGroup.length > 1) {
|
|
2537
|
+
toolGroups.push(currentGroup);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
toolGroups.forEach((group, groupIndex) => {
|
|
2541
|
+
const wrappers = group
|
|
2542
|
+
.map((groupMessage) =>
|
|
2543
|
+
Array.from(tempContainer.children).find(
|
|
2544
|
+
(child) =>
|
|
2545
|
+
child instanceof HTMLElement &&
|
|
2546
|
+
child.getAttribute("data-wrapper-id") === groupMessage.id
|
|
2547
|
+
) as HTMLElement | undefined
|
|
2548
|
+
)
|
|
2549
|
+
.filter((wrapper): wrapper is HTMLElement => Boolean(wrapper));
|
|
2550
|
+
|
|
2551
|
+
if (wrappers.length < 2) {
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const groupWrapper = document.createElement("div");
|
|
2556
|
+
groupWrapper.className = "persona-flex";
|
|
2557
|
+
groupWrapper.id = `wrapper-tool-group-${groupIndex}-${group[0].id}`;
|
|
2558
|
+
groupWrapper.setAttribute("data-wrapper-id", `tool-group-${groupIndex}-${group[0].id}`);
|
|
2559
|
+
|
|
2560
|
+
const groupContainer = document.createElement("div");
|
|
2561
|
+
groupContainer.className =
|
|
2562
|
+
"persona-tool-group persona-flex persona-w-full persona-flex-col persona-gap-2";
|
|
2563
|
+
groupContainer.setAttribute("data-persona-tool-group", "true");
|
|
2564
|
+
|
|
2565
|
+
const summary = document.createElement("div");
|
|
2566
|
+
summary.className =
|
|
2567
|
+
"persona-tool-group-summary persona-text-xs persona-text-persona-muted";
|
|
2568
|
+
|
|
2569
|
+
const defaultSummary = `Called ${group.length} tools`;
|
|
2570
|
+
const renderedSummary = config.toolCall?.renderGroupedSummary?.({
|
|
2571
|
+
messages: group,
|
|
2572
|
+
toolCalls: group
|
|
2573
|
+
.map((groupMessage) => groupMessage.toolCall)
|
|
2574
|
+
.filter((toolCall): toolCall is NonNullable<typeof group[number]["toolCall"]> => Boolean(toolCall)),
|
|
2575
|
+
defaultSummary,
|
|
2576
|
+
config,
|
|
2577
|
+
});
|
|
2578
|
+
if (!appendRenderedValue(summary, renderedSummary)) {
|
|
2579
|
+
summary.textContent = defaultSummary;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
const stack = document.createElement("div");
|
|
2583
|
+
stack.className = "persona-tool-group-stack persona-flex persona-flex-col";
|
|
2584
|
+
|
|
2585
|
+
groupContainer.append(summary, stack);
|
|
2586
|
+
groupWrapper.appendChild(groupContainer);
|
|
2587
|
+
wrappers[0].before(groupWrapper);
|
|
2588
|
+
|
|
2589
|
+
wrappers.forEach((wrapper, wrapperIndex) => {
|
|
2590
|
+
const item = document.createElement("div");
|
|
2591
|
+
item.className = "persona-tool-group-item persona-relative";
|
|
2592
|
+
item.setAttribute("data-persona-tool-group-item", "true");
|
|
2593
|
+
if (wrapperIndex < wrappers.length - 1) {
|
|
2594
|
+
item.setAttribute("data-persona-tool-group-connector", "true");
|
|
2595
|
+
}
|
|
2596
|
+
item.appendChild(wrapper);
|
|
2597
|
+
stack.appendChild(item);
|
|
2598
|
+
});
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2431
2602
|
// Remove cache entries for messages that no longer exist
|
|
2432
2603
|
pruneCache(messageCache, activeMessageIds);
|
|
2433
2604
|
|
|
@@ -3684,7 +3855,7 @@ export const createAgentExperience = (
|
|
|
3684
3855
|
lastScrollTop,
|
|
3685
3856
|
nearBottom: isElementNearBottom(body, BOTTOM_THRESHOLD),
|
|
3686
3857
|
userScrollThreshold: USER_SCROLL_THRESHOLD,
|
|
3687
|
-
isAutoScrolling,
|
|
3858
|
+
isAutoScrolling: isAutoScrolling || hasPendingAutoScroll,
|
|
3688
3859
|
pauseOnUpwardScroll: true,
|
|
3689
3860
|
pauseWhenAwayFromBottom: false,
|
|
3690
3861
|
resumeRequiresDownwardScroll: true
|
|
@@ -3823,6 +3994,78 @@ export const createAgentExperience = (
|
|
|
3823
3994
|
textarea?.addEventListener("keydown", handleInputEnter);
|
|
3824
3995
|
textarea?.addEventListener("paste", handleInputPaste);
|
|
3825
3996
|
|
|
3997
|
+
const ATTACHMENT_DROP_ACTIVE_CLASS = "persona-attachment-drop-active";
|
|
3998
|
+
let attachmentFileDragDepth = 0;
|
|
3999
|
+
|
|
4000
|
+
const clearAttachmentDropVisual = () => {
|
|
4001
|
+
attachmentFileDragDepth = 0;
|
|
4002
|
+
container.classList.remove(ATTACHMENT_DROP_ACTIVE_CLASS);
|
|
4003
|
+
};
|
|
4004
|
+
|
|
4005
|
+
const attachmentDropHandlingActive = (): boolean =>
|
|
4006
|
+
config.attachments?.enabled === true && attachmentManager !== null;
|
|
4007
|
+
|
|
4008
|
+
// Visual highlight tracked on `container` (the chat column).
|
|
4009
|
+
const handleAttachmentDragEnterCapture = (e: DragEvent) => {
|
|
4010
|
+
if (!dataTransferHasFiles(e.dataTransfer) || !attachmentDropHandlingActive()) return;
|
|
4011
|
+
attachmentFileDragDepth++;
|
|
4012
|
+
if (attachmentFileDragDepth === 1) {
|
|
4013
|
+
container.classList.add(ATTACHMENT_DROP_ACTIVE_CLASS);
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
|
|
4017
|
+
const handleAttachmentDragLeaveCapture = (e: DragEvent) => {
|
|
4018
|
+
if (!dataTransferHasFiles(e.dataTransfer) || !attachmentDropHandlingActive()) return;
|
|
4019
|
+
attachmentFileDragDepth--;
|
|
4020
|
+
if (attachmentFileDragDepth <= 0) {
|
|
4021
|
+
clearAttachmentDropVisual();
|
|
4022
|
+
}
|
|
4023
|
+
};
|
|
4024
|
+
|
|
4025
|
+
// dragover + drop registered on `mount` so the browser default (open file)
|
|
4026
|
+
// is suppressed across the entire widget surface (artifact pane, gaps, etc.).
|
|
4027
|
+
const handleAttachmentDragOverCapture = (e: DragEvent) => {
|
|
4028
|
+
if (!dataTransferHasFiles(e.dataTransfer) || !attachmentDropHandlingActive()) return;
|
|
4029
|
+
e.preventDefault();
|
|
4030
|
+
e.dataTransfer.dropEffect = "copy";
|
|
4031
|
+
};
|
|
4032
|
+
|
|
4033
|
+
const handleAttachmentDropCapture = (e: DragEvent) => {
|
|
4034
|
+
if (!dataTransferHasFiles(e.dataTransfer) || !attachmentDropHandlingActive()) return;
|
|
4035
|
+
e.preventDefault();
|
|
4036
|
+
e.stopPropagation();
|
|
4037
|
+
clearAttachmentDropVisual();
|
|
4038
|
+
const files = Array.from(e.dataTransfer.files ?? []);
|
|
4039
|
+
if (files.length === 0) return;
|
|
4040
|
+
void attachmentManager!.handleFiles(files);
|
|
4041
|
+
};
|
|
4042
|
+
|
|
4043
|
+
const attachmentDropCapture = true;
|
|
4044
|
+
container.addEventListener("dragenter", handleAttachmentDragEnterCapture, attachmentDropCapture);
|
|
4045
|
+
container.addEventListener("dragleave", handleAttachmentDragLeaveCapture, attachmentDropCapture);
|
|
4046
|
+
mount.addEventListener("dragover", handleAttachmentDragOverCapture, attachmentDropCapture);
|
|
4047
|
+
mount.addEventListener("drop", handleAttachmentDropCapture, attachmentDropCapture);
|
|
4048
|
+
|
|
4049
|
+
// Prevent the browser from navigating to/opening a dropped file anywhere on
|
|
4050
|
+
// the page while this widget instance has attachments enabled. These guards
|
|
4051
|
+
// intentionally skip the `dataTransferHasFiles` check because real OS drags
|
|
4052
|
+
// may expose `dataTransfer.types` as a DOMStringList or restrict access
|
|
4053
|
+
// during certain drag phases. The cost is minimal: we suppress the native
|
|
4054
|
+
// "open file" default for ALL drag-overs while the widget is alive and
|
|
4055
|
+
// attachments are on — text drags into the textarea still work because
|
|
4056
|
+
// element-level handlers are unaffected (we don't stopPropagation here).
|
|
4057
|
+
const ownerDoc = mount.ownerDocument;
|
|
4058
|
+
const handleDocDragOver = (e: DragEvent) => {
|
|
4059
|
+
if (!attachmentDropHandlingActive()) return;
|
|
4060
|
+
e.preventDefault();
|
|
4061
|
+
};
|
|
4062
|
+
const handleDocDrop = (e: DragEvent) => {
|
|
4063
|
+
if (!attachmentDropHandlingActive()) return;
|
|
4064
|
+
e.preventDefault();
|
|
4065
|
+
};
|
|
4066
|
+
ownerDoc.addEventListener("dragover", handleDocDragOver);
|
|
4067
|
+
ownerDoc.addEventListener("drop", handleDocDrop);
|
|
4068
|
+
|
|
3826
4069
|
destroyCallbacks.push(() => {
|
|
3827
4070
|
if (composerForm) {
|
|
3828
4071
|
composerForm.removeEventListener("submit", handleSubmit);
|
|
@@ -3831,6 +4074,16 @@ export const createAgentExperience = (
|
|
|
3831
4074
|
textarea?.removeEventListener("paste", handleInputPaste);
|
|
3832
4075
|
});
|
|
3833
4076
|
|
|
4077
|
+
destroyCallbacks.push(() => {
|
|
4078
|
+
container.removeEventListener("dragenter", handleAttachmentDragEnterCapture, attachmentDropCapture);
|
|
4079
|
+
container.removeEventListener("dragleave", handleAttachmentDragLeaveCapture, attachmentDropCapture);
|
|
4080
|
+
mount.removeEventListener("dragover", handleAttachmentDragOverCapture, attachmentDropCapture);
|
|
4081
|
+
mount.removeEventListener("drop", handleAttachmentDropCapture, attachmentDropCapture);
|
|
4082
|
+
ownerDoc.removeEventListener("dragover", handleDocDragOver);
|
|
4083
|
+
ownerDoc.removeEventListener("drop", handleDocDrop);
|
|
4084
|
+
clearAttachmentDropVisual();
|
|
4085
|
+
});
|
|
4086
|
+
|
|
3834
4087
|
destroyCallbacks.push(() => {
|
|
3835
4088
|
session.cancel();
|
|
3836
4089
|
});
|
|
@@ -3853,6 +4106,10 @@ export const createAgentExperience = (
|
|
|
3853
4106
|
const previousColorScheme = config.colorScheme;
|
|
3854
4107
|
const previousLoadingIndicator = config.loadingIndicator;
|
|
3855
4108
|
const previousIterationDisplay = config.iterationDisplay;
|
|
4109
|
+
const previousShowReasoning = config.features?.showReasoning;
|
|
4110
|
+
const previousShowToolCalls = config.features?.showToolCalls;
|
|
4111
|
+
const previousToolCallDisplay = config.features?.toolCallDisplay;
|
|
4112
|
+
const previousReasoningDisplay = config.features?.reasoningDisplay;
|
|
3856
4113
|
config = { ...config, ...nextConfig };
|
|
3857
4114
|
// applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
|
|
3858
4115
|
applyFullHeightStyles();
|
|
@@ -4093,8 +4350,12 @@ export const createAgentExperience = (
|
|
|
4093
4350
|
|| config.loadingIndicator?.renderIdle !== previousLoadingIndicator?.renderIdle
|
|
4094
4351
|
|| config.loadingIndicator?.showBubble !== previousLoadingIndicator?.showBubble;
|
|
4095
4352
|
const iterationDisplayChanged = config.iterationDisplay !== previousIterationDisplay;
|
|
4353
|
+
const featuresChanged = (config.features?.showReasoning ?? true) !== (previousShowReasoning ?? true)
|
|
4354
|
+
|| (config.features?.showToolCalls ?? true) !== (previousShowToolCalls ?? true)
|
|
4355
|
+
|| JSON.stringify(config.features?.toolCallDisplay) !== JSON.stringify(previousToolCallDisplay)
|
|
4356
|
+
|| JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay);
|
|
4096
4357
|
const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged
|
|
4097
|
-
|| loadingIndicatorChanged || iterationDisplayChanged;
|
|
4358
|
+
|| loadingIndicatorChanged || iterationDisplayChanged || featuresChanged;
|
|
4098
4359
|
if (messagesConfigChanged && session) {
|
|
4099
4360
|
configVersion++;
|
|
4100
4361
|
renderMessagesWithPlugins(messagesWrapper, session.getMessages(), postprocess);
|
|
@@ -4858,6 +5119,11 @@ export const createAgentExperience = (
|
|
|
4858
5119
|
}
|
|
4859
5120
|
});
|
|
4860
5121
|
}
|
|
5122
|
+
|
|
5123
|
+
// Create drop overlay if missing
|
|
5124
|
+
if (!container.querySelector(".persona-attachment-drop-overlay")) {
|
|
5125
|
+
container.appendChild(buildDropOverlay(attachmentsConfig.dropOverlay));
|
|
5126
|
+
}
|
|
4861
5127
|
} else {
|
|
4862
5128
|
// Show existing attachment button and update config
|
|
4863
5129
|
attachmentButtonWrapper.style.display = "";
|
|
@@ -4887,6 +5153,8 @@ export const createAgentExperience = (
|
|
|
4887
5153
|
if (attachmentManager) {
|
|
4888
5154
|
attachmentManager.clearAttachments();
|
|
4889
5155
|
}
|
|
5156
|
+
// Remove drop overlay
|
|
5157
|
+
container.querySelector(".persona-attachment-drop-overlay")?.remove();
|
|
4890
5158
|
}
|
|
4891
5159
|
|
|
4892
5160
|
// Update send button styling
|
|
@@ -90,6 +90,23 @@ describe("computeMessageFingerprint", () => {
|
|
|
90
90
|
expect(fp1).not.toBe(fp2);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it("changes when toolCall chunks change", () => {
|
|
94
|
+
const fp1 = computeMessageFingerprint(
|
|
95
|
+
makeMessage({ toolCall: { status: "running", chunks: ["Loaded tools"] } }),
|
|
96
|
+
0
|
|
97
|
+
);
|
|
98
|
+
const fp2 = computeMessageFingerprint(
|
|
99
|
+
makeMessage({
|
|
100
|
+
toolCall: {
|
|
101
|
+
status: "running",
|
|
102
|
+
chunks: ["Loaded tools", "\nFetched platform documentation"],
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
0
|
|
106
|
+
);
|
|
107
|
+
expect(fp1).not.toBe(fp2);
|
|
108
|
+
});
|
|
109
|
+
|
|
93
110
|
it("changes when reasoning chunks change", () => {
|
|
94
111
|
const fp1 = computeMessageFingerprint(makeMessage({ reasoning: { chunks: ["step 1"] } }), 0);
|
|
95
112
|
const fp2 = computeMessageFingerprint(makeMessage({ reasoning: { chunks: ["step 1", "step 2"] } }), 0);
|
|
@@ -16,7 +16,12 @@ export type FingerprintableMessage = {
|
|
|
16
16
|
rawContent?: string;
|
|
17
17
|
llmContent?: string;
|
|
18
18
|
approval?: { status?: string; [key: string]: unknown };
|
|
19
|
-
toolCall?: {
|
|
19
|
+
toolCall?: {
|
|
20
|
+
status?: string;
|
|
21
|
+
chunks?: string[];
|
|
22
|
+
args?: unknown;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
20
25
|
reasoning?: { chunks?: string[]; status?: string; [key: string]: unknown };
|
|
21
26
|
contentParts?: unknown[];
|
|
22
27
|
};
|
|
@@ -48,6 +53,13 @@ export function computeMessageFingerprint(
|
|
|
48
53
|
message.llmContent?.length ?? 0,
|
|
49
54
|
message.approval?.status ?? "",
|
|
50
55
|
message.toolCall?.status ?? "",
|
|
56
|
+
message.toolCall?.chunks?.length ?? 0,
|
|
57
|
+
message.toolCall?.chunks?.[message.toolCall.chunks.length - 1]?.slice(-32) ?? "",
|
|
58
|
+
typeof message.toolCall?.args === "string"
|
|
59
|
+
? message.toolCall.args.length
|
|
60
|
+
: message.toolCall?.args
|
|
61
|
+
? JSON.stringify(message.toolCall.args).length
|
|
62
|
+
: 0,
|
|
51
63
|
message.reasoning?.chunks?.length ?? 0,
|
|
52
64
|
message.contentParts?.length ?? 0,
|
|
53
65
|
configVersion,
|