@runtypelabs/persona 4.21.1 → 4.22.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/{approval-ui-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +73 -2
- package/dist/approval-ui.d.ts +73 -2
- package/dist/approval-ui.js +2 -2
- package/dist/artifacts-ui-F4N6VMKF.js +7 -0
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +73 -2
- package/dist/artifacts-ui.d.ts +73 -2
- package/dist/artifacts-ui.js +1 -1
- package/dist/chunk-NQLDCJAP.js +4 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +73 -2
- package/dist/context-mentions.d.ts +73 -2
- package/dist/event-stream-view-FKWDY3UB.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +73 -2
- package/dist/event-stream-view.d.ts +73 -2
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +73 -2
- package/dist/forms-ui.d.ts +73 -2
- package/dist/index.cjs +63 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -5
- package/dist/index.d.ts +136 -5
- package/dist/index.global.js +48 -48
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +64 -64
- package/dist/index.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 +73 -2
- package/dist/session-reconnect.d.ts +73 -2
- package/dist/smart-dom-reader.d.cts +73 -2
- package/dist/smart-dom-reader.d.ts +73 -2
- package/dist/theme-editor-preview.cjs +64 -64
- package/dist/theme-editor-preview.d.cts +73 -2
- package/dist/theme-editor-preview.d.ts +73 -2
- package/dist/theme-editor-preview.js +45 -45
- package/dist/theme-editor.cjs +4 -4
- package/dist/theme-editor.d.cts +73 -2
- package/dist/theme-editor.d.ts +73 -2
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +65 -1
- package/dist/theme-reference.d.ts +65 -1
- package/dist/theme-reference.js +1 -1
- package/dist/webmcp-polyfill.js +1 -4
- package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +34 -2
- package/dist/webmcp-runtime.d.ts +34 -2
- package/dist/webmcp-runtime.js +2 -2
- package/package.json +2 -2
- package/src/components/artifact-preview.ts +5 -0
- package/src/components/event-stream-badge-theming.test.ts +159 -0
- package/src/components/event-stream-view.ts +50 -20
- package/src/generated/runtype-openapi-contract.ts +8 -0
- package/src/index-core.ts +2 -0
- package/src/runtime/init.registry.test.ts +56 -0
- package/src/runtime/init.ts +17 -0
- package/src/types/theme.ts +41 -1
- package/src/types.ts +38 -1
- package/src/ui.reasoning-display.test.ts +136 -2
- package/src/ui.ts +41 -10
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
- package/src/utils/formatting.ts +22 -0
- package/src/utils/theme.ts +16 -0
- package/src/utils/tokens.ts +59 -0
- package/src/webmcp-bridge.test.ts +132 -23
- package/src/webmcp-bridge.ts +26 -2
- package/src/webmcp-runtime-entry.ts +71 -11
- package/dist/artifacts-ui-J6AKUGTS.js +0 -7
- package/dist/chunk-HBE7LFNT.js +0 -4
- package/dist/event-stream-view-73NWKWVQ.js +0 -1
- package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
|
@@ -32,17 +32,25 @@ function applyCustomClasses(el: HTMLElement, classes?: string): void {
|
|
|
32
32
|
// Constants
|
|
33
33
|
// ============================================================================
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
// Badge families resolve through the theme pipeline: the aliases below are
|
|
36
|
+
// materialized into every theme (light pairs in DEFAULT_COMPONENTS, dark pairs
|
|
37
|
+
// in the built-in dark layer of createDarkTheme), so the chip follows the
|
|
38
|
+
// active color scheme and re-resolves live on scheme toggles. The literal
|
|
39
|
+
// fallbacks are the light pairs, for direct mounts outside a themed root.
|
|
40
|
+
export const DEFAULT_BADGE_COLORS: Record<string, EventStreamBadgeColor> = {
|
|
41
|
+
flow_: { bg: "var(--persona-event-badge-flow-bg, #dcfce7)", text: "var(--persona-event-badge-flow-fg, #15803d)" },
|
|
42
|
+
step_: { bg: "var(--persona-event-badge-step-bg, #f5f5f5)", text: "var(--persona-event-badge-step-fg, #0a0a0a)" },
|
|
43
|
+
reasoning_: { bg: "var(--persona-event-badge-reasoning-bg, #fef9c3)", text: "var(--persona-event-badge-reasoning-fg, #a16207)" },
|
|
44
|
+
tool_: { bg: "var(--persona-event-badge-tool-bg, #f3e8ff)", text: "var(--persona-event-badge-tool-fg, #6b21a8)" },
|
|
45
|
+
agent_: { bg: "var(--persona-event-badge-agent-bg, #ccfbf1)", text: "var(--persona-event-badge-agent-fg, #115e59)" },
|
|
46
|
+
error: { bg: "var(--persona-event-badge-error-bg, #fee2e2)", text: "var(--persona-event-badge-error-fg, #b91c1c)" },
|
|
42
47
|
};
|
|
48
|
+
// The unified error event type is `error`; `execution_error` carries the same
|
|
49
|
+
// severity and would otherwise fall through to the default pair.
|
|
50
|
+
DEFAULT_BADGE_COLORS.execution_error = DEFAULT_BADGE_COLORS.error;
|
|
43
51
|
const DEFAULT_BADGE_COLOR: EventStreamBadgeColor = {
|
|
44
|
-
bg: "var(--persona-
|
|
45
|
-
text: "var(--persona-
|
|
52
|
+
bg: "var(--persona-event-badge-default-bg, #f3f4f6)",
|
|
53
|
+
text: "var(--persona-event-badge-default-fg, #4b5563)",
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
const DEFAULT_DESCRIPTION_FIELDS = [
|
|
@@ -62,20 +70,42 @@ const UPDATE_THROTTLE_MS = 100;
|
|
|
62
70
|
// Helper Functions
|
|
63
71
|
// ============================================================================
|
|
64
72
|
|
|
65
|
-
function
|
|
73
|
+
function matchBadgeColor(
|
|
66
74
|
eventType: string,
|
|
67
|
-
|
|
68
|
-
): EventStreamBadgeColor {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Prefix match (keys ending with "_")
|
|
73
|
-
for (const prefix of Object.keys(allColors)) {
|
|
75
|
+
colors: Record<string, EventStreamBadgeColor>
|
|
76
|
+
): EventStreamBadgeColor | undefined {
|
|
77
|
+
// Exact match first, then prefix match (keys ending with "_").
|
|
78
|
+
if (colors[eventType]) return colors[eventType];
|
|
79
|
+
for (const prefix of Object.keys(colors)) {
|
|
74
80
|
if (prefix.endsWith("_") && eventType.startsWith(prefix)) {
|
|
75
|
-
return
|
|
81
|
+
return colors[prefix];
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
|
-
return
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getBadgeColor(
|
|
88
|
+
eventType: string,
|
|
89
|
+
customColors?: Record<string, EventStreamBadgeColor>
|
|
90
|
+
): EventStreamBadgeColor {
|
|
91
|
+
// Host colors are a strictly higher tier than the defaults: a host prefix
|
|
92
|
+
// (e.g. `execution_`) must not be shadowed by the built-in exact
|
|
93
|
+
// `execution_error` entry, so the maps are never merged flat.
|
|
94
|
+
return (
|
|
95
|
+
(customColors && matchBadgeColor(eventType, customColors)) ??
|
|
96
|
+
matchBadgeColor(eventType, DEFAULT_BADGE_COLORS) ??
|
|
97
|
+
DEFAULT_BADGE_COLOR
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The chip border is its text color at ~31% alpha. The hex-suffix trick only
|
|
102
|
+
// composes on a bare 6-digit hex; every other value (var() refs like the
|
|
103
|
+
// defaults above, rgb(), named colors) needs color-mix or the declaration is
|
|
104
|
+
// invalid and silently dropped.
|
|
105
|
+
export function getBadgeBorderColor(text: string): string {
|
|
106
|
+
return /^#[0-9a-fA-F]{6}$/.test(text)
|
|
107
|
+
? `${text}50`
|
|
108
|
+
: `color-mix(in srgb, ${text} 31%, transparent)`;
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
function formatRelativeTimestamp(ms: number, firstEventMs: number): string {
|
|
@@ -326,7 +356,7 @@ function renderEventRow(
|
|
|
326
356
|
);
|
|
327
357
|
badge.style.backgroundColor = badgeColor.bg;
|
|
328
358
|
badge.style.color = badgeColor.text;
|
|
329
|
-
badge.style.borderColor = badgeColor.text
|
|
359
|
+
badge.style.borderColor = getBadgeBorderColor(badgeColor.text);
|
|
330
360
|
badge.textContent = event.type;
|
|
331
361
|
|
|
332
362
|
// 5. Description (extracted from payload)
|
|
@@ -432,12 +432,14 @@ export type RuntypeStopReasonKind = NonNullable<
|
|
|
432
432
|
>;
|
|
433
433
|
|
|
434
434
|
export type RuntypeClientInitRequest = {
|
|
435
|
+
durableRecovery?: boolean;
|
|
435
436
|
flowId?: string;
|
|
436
437
|
identityProof?: string;
|
|
437
438
|
token: string;
|
|
438
439
|
visitorHistory?: boolean;
|
|
439
440
|
visitorToken?: string;
|
|
440
441
|
} | {
|
|
442
|
+
durableRecovery?: boolean;
|
|
441
443
|
flowId?: string;
|
|
442
444
|
identityProof?: string;
|
|
443
445
|
sessionId: string;
|
|
@@ -446,6 +448,7 @@ export type RuntypeClientInitRequest = {
|
|
|
446
448
|
visitorToken?: string;
|
|
447
449
|
} | {
|
|
448
450
|
conversationId: string;
|
|
451
|
+
durableRecovery?: boolean;
|
|
449
452
|
flowId?: string;
|
|
450
453
|
identityProof?: string;
|
|
451
454
|
token: string;
|
|
@@ -453,6 +456,7 @@ export type RuntypeClientInitRequest = {
|
|
|
453
456
|
visitorToken: string;
|
|
454
457
|
} | {
|
|
455
458
|
conversationId: string;
|
|
459
|
+
durableRecovery?: boolean;
|
|
456
460
|
flowId?: string;
|
|
457
461
|
identityProof: string;
|
|
458
462
|
token: string;
|
|
@@ -471,6 +475,9 @@ export type RuntypeClientInitResponse = {
|
|
|
471
475
|
};
|
|
472
476
|
conversationId: string;
|
|
473
477
|
conversationRevision: string;
|
|
478
|
+
durableRecovery?: {
|
|
479
|
+
enabled: boolean;
|
|
480
|
+
};
|
|
474
481
|
expiresAt: string;
|
|
475
482
|
flow?: {
|
|
476
483
|
description?: string | null;
|
|
@@ -538,6 +545,7 @@ export type RuntypeClientChatRequest = {
|
|
|
538
545
|
export type RuntypeClientChatStreamEvent = RuntypeExecutionStreamEvent;
|
|
539
546
|
|
|
540
547
|
export type RuntypeClientResumeRequest = {
|
|
548
|
+
after?: string;
|
|
541
549
|
assistantMessageId?: string;
|
|
542
550
|
clientTools?: Array<{
|
|
543
551
|
description: string;
|
package/src/index-core.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
initAgentWidget as initAgentWidgetFn,
|
|
3
|
+
getAgentWidgetHandles as getAgentWidgetHandlesFn,
|
|
3
4
|
type AgentWidgetInitHandle
|
|
4
5
|
} from "./runtime/init";
|
|
5
6
|
|
|
@@ -326,6 +327,7 @@ export {
|
|
|
326
327
|
} from "./suggest-replies-tool";
|
|
327
328
|
|
|
328
329
|
export { initAgentWidgetFn as initAgentWidget };
|
|
330
|
+
export { getAgentWidgetHandlesFn as getAgentWidgetHandles };
|
|
329
331
|
export {
|
|
330
332
|
createWidgetHostLayout,
|
|
331
333
|
type WidgetHostLayout,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `getAgentWidgetHandles()` registry: every `initAgentWidget()` mount is
|
|
5
|
+
* listed (oldest first) until its `destroy()`, so companions created after
|
|
6
|
+
* `persona:chat-ready` fired can still adopt a mounted widget.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { getAgentWidgetHandles, initAgentWidget } from "./init";
|
|
12
|
+
|
|
13
|
+
const mountHost = (id: string): HTMLElement => {
|
|
14
|
+
const host = document.createElement("div");
|
|
15
|
+
host.id = id;
|
|
16
|
+
document.body.appendChild(host);
|
|
17
|
+
return host;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
apiUrl: "https://api.example.com/chat",
|
|
22
|
+
launcher: { enabled: false },
|
|
23
|
+
persistState: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("getAgentWidgetHandles", () => {
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
// Sweep any instances a failing assertion left behind.
|
|
29
|
+
for (const handle of getAgentWidgetHandles()) handle.destroy();
|
|
30
|
+
document.body.innerHTML = "";
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("lists mounted handles oldest-first and drops them on destroy", () => {
|
|
34
|
+
expect(getAgentWidgetHandles()).toEqual([]);
|
|
35
|
+
|
|
36
|
+
const first = initAgentWidget({ target: mountHost("reg-a"), config: CONFIG });
|
|
37
|
+
const second = initAgentWidget({ target: mountHost("reg-b"), config: CONFIG });
|
|
38
|
+
|
|
39
|
+
expect(getAgentWidgetHandles()).toEqual([first, second]);
|
|
40
|
+
expect(getAgentWidgetHandles().at(-1)).toBe(second);
|
|
41
|
+
|
|
42
|
+
second.destroy();
|
|
43
|
+
expect(getAgentWidgetHandles()).toEqual([first]);
|
|
44
|
+
|
|
45
|
+
first.destroy();
|
|
46
|
+
expect(getAgentWidgetHandles()).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns a copy, not the live registry", () => {
|
|
50
|
+
const handle = initAgentWidget({ target: mountHost("reg-c"), config: CONFIG });
|
|
51
|
+
const snapshot = getAgentWidgetHandles();
|
|
52
|
+
snapshot.length = 0;
|
|
53
|
+
expect(getAgentWidgetHandles()).toEqual([handle]);
|
|
54
|
+
handle.destroy();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/runtime/init.ts
CHANGED
|
@@ -46,6 +46,19 @@ const mountStyles = (root: ShadowRoot | HTMLElement, ownerDocument: Document) =>
|
|
|
46
46
|
|
|
47
47
|
export type AgentWidgetInitHandle = AgentWidgetController & { host: HTMLElement };
|
|
48
48
|
|
|
49
|
+
// Live handles for every mounted initAgentWidget() instance, oldest first.
|
|
50
|
+
// `persona:chat-ready` is a one-shot broadcast and `windowKey` is opt-in, so
|
|
51
|
+
// code that runs after a widget mounted previously had no way to reach it.
|
|
52
|
+
// This registry closes that gap: late companions (selection toolbars, host
|
|
53
|
+
// integrations) can adopt the most recent mount via
|
|
54
|
+
// `getAgentWidgetHandles().at(-1)`. Handles deregister on destroy().
|
|
55
|
+
const activeHandles: AgentWidgetInitHandle[] = [];
|
|
56
|
+
|
|
57
|
+
/** Handles of all currently mounted widgets, oldest first. */
|
|
58
|
+
export const getAgentWidgetHandles = (): AgentWidgetInitHandle[] => [
|
|
59
|
+
...activeHandles,
|
|
60
|
+
];
|
|
61
|
+
|
|
49
62
|
export const initAgentWidget = (
|
|
50
63
|
options: AgentWidgetInitOptions
|
|
51
64
|
): AgentWidgetInitHandle => {
|
|
@@ -154,6 +167,8 @@ export const initAgentWidget = (
|
|
|
154
167
|
destroy() {
|
|
155
168
|
destroyCurrentController();
|
|
156
169
|
hostLayout.destroy();
|
|
170
|
+
const index = activeHandles.indexOf(handle);
|
|
171
|
+
if (index !== -1) activeHandles.splice(index, 1);
|
|
157
172
|
if (options.windowKey && typeof window !== "undefined") {
|
|
158
173
|
delete (window as any)[options.windowKey];
|
|
159
174
|
}
|
|
@@ -175,6 +190,8 @@ export const initAgentWidget = (
|
|
|
175
190
|
}
|
|
176
191
|
}) as AgentWidgetInitHandle;
|
|
177
192
|
|
|
193
|
+
activeHandles.push(handle);
|
|
194
|
+
|
|
178
195
|
if (options.windowKey && typeof window !== 'undefined') {
|
|
179
196
|
(window as any)[options.windowKey] = handle;
|
|
180
197
|
}
|
package/src/types/theme.ts
CHANGED
|
@@ -26,7 +26,11 @@ export interface ColorPalette {
|
|
|
26
26
|
warning: ColorShade;
|
|
27
27
|
error: ColorShade;
|
|
28
28
|
info: ColorShade;
|
|
29
|
-
|
|
29
|
+
/** Event-stream tool badge family. */
|
|
30
|
+
purple?: ColorShade;
|
|
31
|
+
/** Event-stream agent badge family. */
|
|
32
|
+
teal?: ColorShade;
|
|
33
|
+
[key: string]: ColorShade | undefined;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export interface SpacingScale {
|
|
@@ -628,6 +632,40 @@ export interface ReasoningBubbleTokens {
|
|
|
628
632
|
shadow: string;
|
|
629
633
|
}
|
|
630
634
|
|
|
635
|
+
/** One event-stream badge chip: fill + text pair. The chip border derives
|
|
636
|
+
* from `foreground` at ~31% alpha. */
|
|
637
|
+
export interface EventStreamBadgePairTokens {
|
|
638
|
+
background: TokenReference<'color'>;
|
|
639
|
+
foreground: TokenReference<'color'>;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Event-stream inspector badge chips (`features.eventStream`), one pair per
|
|
644
|
+
* unified event family. Materialized into every theme and emitted as
|
|
645
|
+
* `--persona-event-badge-<family>-{bg,fg}`, with scheme-appropriate defaults:
|
|
646
|
+
* light themes pair a 100-tone fill with a 700-tone text, the built-in dark
|
|
647
|
+
* layer flips to a 900-tone fill with a 300-tone text. `badge.default` colors
|
|
648
|
+
* event types no family matches.
|
|
649
|
+
*/
|
|
650
|
+
export interface EventStreamTokens {
|
|
651
|
+
badge: {
|
|
652
|
+
/** `flow_*` events. */
|
|
653
|
+
flow: EventStreamBadgePairTokens;
|
|
654
|
+
/** `step_*` events. */
|
|
655
|
+
step: EventStreamBadgePairTokens;
|
|
656
|
+
/** `reasoning_*` events. */
|
|
657
|
+
reasoning: EventStreamBadgePairTokens;
|
|
658
|
+
/** `tool_*` events. */
|
|
659
|
+
tool: EventStreamBadgePairTokens;
|
|
660
|
+
/** `agent_*` events. */
|
|
661
|
+
agent: EventStreamBadgePairTokens;
|
|
662
|
+
/** `error` / `execution_error` events. */
|
|
663
|
+
error: EventStreamBadgePairTokens;
|
|
664
|
+
/** Any event type no other family matches. */
|
|
665
|
+
default: EventStreamBadgePairTokens;
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
631
669
|
/** Scrollbar appearance shared by every scroller in the widget. */
|
|
632
670
|
export interface ScrollbarTokens {
|
|
633
671
|
/** Thumb color. @default semantic.colors.border */
|
|
@@ -994,6 +1032,8 @@ export interface ComponentTokens {
|
|
|
994
1032
|
attachment: AttachmentTokens;
|
|
995
1033
|
toolBubble: ToolBubbleTokens;
|
|
996
1034
|
reasoningBubble: ReasoningBubbleTokens;
|
|
1035
|
+
/** Event-stream inspector badge chips. */
|
|
1036
|
+
eventStream?: EventStreamTokens;
|
|
997
1037
|
composer: ComposerChromeTokens;
|
|
998
1038
|
/** Shared motion timing for composer chrome animation. */
|
|
999
1039
|
motion?: MotionTokens;
|
package/src/types.ts
CHANGED
|
@@ -826,6 +826,14 @@ export type ClientToolDefinition = {
|
|
|
826
826
|
readOnlyHint?: boolean;
|
|
827
827
|
untrustedContentHint?: boolean;
|
|
828
828
|
};
|
|
829
|
+
/**
|
|
830
|
+
* Hoisted copy of `annotations.untrustedContentHint`. This is the field the
|
|
831
|
+
* server actually acts on (`InlineClientToolSchema`): it spotlights the
|
|
832
|
+
* tool's output before the model reads it, so instructions embedded in
|
|
833
|
+
* page-provided content (reviews, comments, emails) are not treated as
|
|
834
|
+
* instructions. `annotations` itself is trace/dashboard metadata only.
|
|
835
|
+
*/
|
|
836
|
+
untrustedContentHint?: boolean;
|
|
829
837
|
};
|
|
830
838
|
|
|
831
839
|
/**
|
|
@@ -2237,6 +2245,16 @@ export type AgentWidgetToolCallDisplayFeature = {
|
|
|
2237
2245
|
* @default "stack"
|
|
2238
2246
|
*/
|
|
2239
2247
|
groupedMode?: "stack" | "summary";
|
|
2248
|
+
/**
|
|
2249
|
+
* What happens to a tool call row once it completes.
|
|
2250
|
+
*
|
|
2251
|
+
* - `"kept"` (default): the finished row stays in the transcript.
|
|
2252
|
+
* - `"removed"`: no row is rendered at all, so the transcript gap closes with
|
|
2253
|
+
* it. The tool call stays on the message and in `getMessages()`.
|
|
2254
|
+
*
|
|
2255
|
+
* @default "kept"
|
|
2256
|
+
*/
|
|
2257
|
+
completedVisibility?: AgentWidgetToolCallCompletedVisibility;
|
|
2240
2258
|
/**
|
|
2241
2259
|
* When false, tool call bubbles show only the collapsed summary with no
|
|
2242
2260
|
* expand/collapse toggle. Users see tool awareness without full details.
|
|
@@ -2255,6 +2273,9 @@ export type AgentWidgetToolCallDisplayFeature = {
|
|
|
2255
2273
|
loadingAnimation?: AgentWidgetToolCallLoadingAnimation;
|
|
2256
2274
|
};
|
|
2257
2275
|
|
|
2276
|
+
/** See `features.toolCallDisplay.completedVisibility`. */
|
|
2277
|
+
export type AgentWidgetToolCallCompletedVisibility = "kept" | "removed";
|
|
2278
|
+
|
|
2258
2279
|
export type AgentWidgetReasoningDisplayFeature = {
|
|
2259
2280
|
/**
|
|
2260
2281
|
* When true, active collapsed reasoning rows can render a lightweight preview block.
|
|
@@ -2300,14 +2321,30 @@ export type AgentWidgetReasoningDisplayFeature = {
|
|
|
2300
2321
|
* - `"kept"` (default): the finished row stays in the transcript.
|
|
2301
2322
|
* - `"removed"`: no row is rendered at all, so the transcript gap closes with
|
|
2302
2323
|
* it. The reasoning text stays on the message and in `getMessages()`.
|
|
2324
|
+
* - `"removed-when-short"`: behaves as `"removed"` when the completed trace
|
|
2325
|
+
* is short (see `shortThinkThreshold`), and as `"kept"` otherwise.
|
|
2303
2326
|
*
|
|
2304
2327
|
* @default "kept"
|
|
2305
2328
|
*/
|
|
2306
2329
|
completedVisibility?: AgentWidgetReasoningCompletedVisibility;
|
|
2330
|
+
/**
|
|
2331
|
+
* Thresholds used by `completedVisibility: "removed-when-short"` to decide
|
|
2332
|
+
* whether a completed trace counts as short. Duration is checked first when
|
|
2333
|
+
* the reasoning reports one; the character count is a fallback for traces
|
|
2334
|
+
* with no reported duration.
|
|
2335
|
+
* @default { durationMs: 15000, chars: 1200 }
|
|
2336
|
+
*/
|
|
2337
|
+
shortThinkThreshold?: {
|
|
2338
|
+
durationMs?: number;
|
|
2339
|
+
chars?: number;
|
|
2340
|
+
};
|
|
2307
2341
|
};
|
|
2308
2342
|
|
|
2309
2343
|
/** See `features.reasoningDisplay.completedVisibility`. */
|
|
2310
|
-
export type AgentWidgetReasoningCompletedVisibility =
|
|
2344
|
+
export type AgentWidgetReasoningCompletedVisibility =
|
|
2345
|
+
| "kept"
|
|
2346
|
+
| "removed"
|
|
2347
|
+
| "removed-when-short";
|
|
2311
2348
|
|
|
2312
2349
|
/**
|
|
2313
2350
|
* Reveal animation applied to assistant message text while it is streaming.
|
|
@@ -25,7 +25,8 @@ const injectReasoning = (
|
|
|
25
25
|
controller: ReturnType<typeof createAgentExperience>,
|
|
26
26
|
id: string,
|
|
27
27
|
status: "streaming" | "complete",
|
|
28
|
-
chunks: string[] = ["Weighing the options"]
|
|
28
|
+
chunks: string[] = ["Weighing the options"],
|
|
29
|
+
durationMs?: number
|
|
29
30
|
) => {
|
|
30
31
|
controller.injectTestMessage({
|
|
31
32
|
type: "message",
|
|
@@ -36,7 +37,26 @@ const injectReasoning = (
|
|
|
36
37
|
createdAt: new Date().toISOString(),
|
|
37
38
|
streaming: status !== "complete",
|
|
38
39
|
variant: "reasoning",
|
|
39
|
-
reasoning: { id, status, chunks },
|
|
40
|
+
reasoning: { id, status, chunks, durationMs },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const injectToolCall = (
|
|
46
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
47
|
+
id: string,
|
|
48
|
+
status: "pending" | "running" | "complete"
|
|
49
|
+
) => {
|
|
50
|
+
controller.injectTestMessage({
|
|
51
|
+
type: "message",
|
|
52
|
+
message: {
|
|
53
|
+
id,
|
|
54
|
+
role: "assistant",
|
|
55
|
+
content: "",
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
streaming: status !== "complete",
|
|
58
|
+
variant: "tool",
|
|
59
|
+
toolCall: { id, name: "search", status, chunks: [] },
|
|
40
60
|
},
|
|
41
61
|
});
|
|
42
62
|
};
|
|
@@ -161,4 +181,118 @@ describe("features.reasoningDisplay.completedVisibility", () => {
|
|
|
161
181
|
?.chunks
|
|
162
182
|
).toEqual(["Weighing the options"]);
|
|
163
183
|
});
|
|
184
|
+
|
|
185
|
+
it('"removed-when-short" drops a completed trace with a short reported duration', () => {
|
|
186
|
+
const { mount, controller } = makeController({
|
|
187
|
+
features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
|
|
188
|
+
});
|
|
189
|
+
injectReasoning(controller, "r1", "complete", ["Weighing the options"], 5_000);
|
|
190
|
+
expect(rowOf(mount, "r1")).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('"removed-when-short" keeps a completed trace with a long reported duration', () => {
|
|
194
|
+
const { mount, controller } = makeController({
|
|
195
|
+
features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
|
|
196
|
+
});
|
|
197
|
+
injectReasoning(controller, "r1", "complete", ["Weighing the options"], 20_000);
|
|
198
|
+
expect(rowOf(mount, "r1")).not.toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('"removed-when-short" drops a completed trace with no duration and few chars', () => {
|
|
202
|
+
const { mount, controller } = makeController({
|
|
203
|
+
features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
|
|
204
|
+
});
|
|
205
|
+
injectReasoning(controller, "r1", "complete", ["Short thought"]);
|
|
206
|
+
expect(rowOf(mount, "r1")).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('"removed-when-short" keeps a completed trace with no duration and many chars', () => {
|
|
210
|
+
const { mount, controller } = makeController({
|
|
211
|
+
features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
|
|
212
|
+
});
|
|
213
|
+
injectReasoning(controller, "r1", "complete", ["x".repeat(1_500)]);
|
|
214
|
+
expect(rowOf(mount, "r1")).not.toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('"removed-when-short" honors custom duration and char thresholds', () => {
|
|
218
|
+
const { mount, controller } = makeController({
|
|
219
|
+
features: {
|
|
220
|
+
reasoningDisplay: {
|
|
221
|
+
completedVisibility: "removed-when-short",
|
|
222
|
+
shortThinkThreshold: { durationMs: 1_000, chars: 50 },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
// 5s exceeds the custom 1s threshold, so this long-by-default trace is kept.
|
|
227
|
+
injectReasoning(controller, "r1", "complete", ["Weighing the options"], 5_000);
|
|
228
|
+
expect(rowOf(mount, "r1")).not.toBeNull();
|
|
229
|
+
|
|
230
|
+
// 60 chars exceeds the custom 50-char threshold, so this trace is kept too.
|
|
231
|
+
injectReasoning(controller, "r2", "complete", ["y".repeat(60)]);
|
|
232
|
+
expect(rowOf(mount, "r2")).not.toBeNull();
|
|
233
|
+
|
|
234
|
+
// Under both custom thresholds, so this trace is dropped.
|
|
235
|
+
injectReasoning(controller, "r3", "complete", ["z".repeat(10)], 500);
|
|
236
|
+
expect(rowOf(mount, "r3")).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("features.toolCallDisplay.completedVisibility", () => {
|
|
241
|
+
beforeEach(() => {
|
|
242
|
+
window.localStorage.clear();
|
|
243
|
+
vi.stubGlobal("requestAnimationFrame", (cb: (time: number) => void) => {
|
|
244
|
+
cb(0);
|
|
245
|
+
return 1;
|
|
246
|
+
});
|
|
247
|
+
vi.stubGlobal("cancelAnimationFrame", () => {});
|
|
248
|
+
window.scrollTo = vi.fn();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
controllers.splice(0).forEach((controller) => controller.destroy());
|
|
253
|
+
mounts.splice(0).forEach((mount) => mount.remove());
|
|
254
|
+
document.body.innerHTML = "";
|
|
255
|
+
vi.restoreAllMocks();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("keeps a finished tool call row by default", () => {
|
|
259
|
+
const { mount, controller } = makeController();
|
|
260
|
+
injectToolCall(controller, "t1", "complete");
|
|
261
|
+
expect(rowOf(mount, "t1")).not.toBeNull();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('removes the whole row under "removed", leaving no transcript gap', () => {
|
|
265
|
+
const { mount, controller } = makeController({
|
|
266
|
+
features: { toolCallDisplay: { completedVisibility: "removed" } },
|
|
267
|
+
});
|
|
268
|
+
injectToolCall(controller, "t1", "complete");
|
|
269
|
+
injectText(controller, "a1", "Here is the answer");
|
|
270
|
+
|
|
271
|
+
expect(rowOf(mount, "t1")).toBeNull();
|
|
272
|
+
const rows = mount.querySelectorAll("[data-wrapper-id]");
|
|
273
|
+
expect(Array.from(rows).map((row) => row.getAttribute("data-wrapper-id")))
|
|
274
|
+
.toEqual(["a1"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('shows a running tool call under "removed" and drops it once it completes', () => {
|
|
278
|
+
const { mount, controller } = makeController({
|
|
279
|
+
features: { toolCallDisplay: { completedVisibility: "removed" } },
|
|
280
|
+
});
|
|
281
|
+
injectToolCall(controller, "t1", "running");
|
|
282
|
+
expect(rowOf(mount, "t1")).not.toBeNull();
|
|
283
|
+
|
|
284
|
+
injectToolCall(controller, "t1", "complete");
|
|
285
|
+
expect(rowOf(mount, "t1")).toBeNull();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('keeps the tool call message in the session under "removed"', () => {
|
|
289
|
+
const { controller } = makeController({
|
|
290
|
+
features: { toolCallDisplay: { completedVisibility: "removed" } },
|
|
291
|
+
});
|
|
292
|
+
injectToolCall(controller, "t1", "complete");
|
|
293
|
+
expect(
|
|
294
|
+
controller.getMessages().find((message) => message.id === "t1")?.toolCall
|
|
295
|
+
?.status
|
|
296
|
+
).toEqual("complete");
|
|
297
|
+
});
|
|
164
298
|
});
|
package/src/ui.ts
CHANGED
|
@@ -61,7 +61,8 @@ import {
|
|
|
61
61
|
ComposerQuote,
|
|
62
62
|
ComposerState,
|
|
63
63
|
ComposerStreamingSubmitBehavior,
|
|
64
|
-
ComposerSubmissionSnapshot
|
|
64
|
+
ComposerSubmissionSnapshot,
|
|
65
|
+
AgentWidgetReasoning
|
|
65
66
|
} from "./types";
|
|
66
67
|
import { AttachmentManager, type PendingAttachment } from "./utils/attachment-manager";
|
|
67
68
|
import { createComposerStore } from "./utils/composer-store";
|
|
@@ -282,7 +283,7 @@ import {
|
|
|
282
283
|
latestAgentSuggestions,
|
|
283
284
|
resolveFollowUpsFeature,
|
|
284
285
|
} from "./suggest-replies-tool";
|
|
285
|
-
import { formatElapsedMs } from "./utils/formatting";
|
|
286
|
+
import { formatElapsedMs, isShortReasoning } from "./utils/formatting";
|
|
286
287
|
import { loadApprovalUi, getApprovalUiSync, type ApprovalUiModule } from "./approval-ui-loader";
|
|
287
288
|
import { getWebMcpToolDisplayTitle } from "./webmcp-bridge";
|
|
288
289
|
import type { AgentWidgetPlugin } from "./plugins/types";
|
|
@@ -6297,17 +6298,40 @@ export const createAgentExperience = (
|
|
|
6297
6298
|
/** The live editor, re-grafted into its morphed wrapper after the pass. */
|
|
6298
6299
|
let editHydrate: { messageId: string; element: HTMLElement } | null = null;
|
|
6299
6300
|
|
|
6300
|
-
// `reasoningDisplay.completedVisibility: "removed"
|
|
6301
|
-
//
|
|
6302
|
-
//
|
|
6303
|
-
|
|
6304
|
-
|
|
6301
|
+
// `reasoningDisplay.completedVisibility: "removed"` (or "removed-when-short"
|
|
6302
|
+
// for a short completed trace): no row for the finished trace, so the
|
|
6303
|
+
// transcript gap closes with it. Checked before the row is registered, so
|
|
6304
|
+
// the cached wrapper is pruned too.
|
|
6305
|
+
const reasoningCompletedVisibility =
|
|
6306
|
+
config.features?.reasoningDisplay?.completedVisibility;
|
|
6307
|
+
const dropReasoningRow = (reasoning: AgentWidgetReasoning): boolean => {
|
|
6308
|
+
if (reasoningCompletedVisibility === "removed") return true;
|
|
6309
|
+
if (reasoningCompletedVisibility === "removed-when-short") {
|
|
6310
|
+
return isShortReasoning(
|
|
6311
|
+
reasoning,
|
|
6312
|
+
config.features?.reasoningDisplay?.shortThinkThreshold
|
|
6313
|
+
);
|
|
6314
|
+
}
|
|
6315
|
+
return false;
|
|
6316
|
+
};
|
|
6317
|
+
|
|
6318
|
+
// `toolCallDisplay.completedVisibility: "removed"`: same mechanism as
|
|
6319
|
+
// reasoning removal, no threshold variant.
|
|
6320
|
+
const dropCompletedToolCalls =
|
|
6321
|
+
config.features?.toolCallDisplay?.completedVisibility === "removed";
|
|
6305
6322
|
|
|
6306
6323
|
messages.forEach((message) => {
|
|
6307
6324
|
if (
|
|
6308
|
-
dropCompletedReasoning &&
|
|
6309
6325
|
message.variant === "reasoning" &&
|
|
6310
|
-
message.reasoning?.status === "complete"
|
|
6326
|
+
message.reasoning?.status === "complete" &&
|
|
6327
|
+
dropReasoningRow(message.reasoning)
|
|
6328
|
+
) {
|
|
6329
|
+
return;
|
|
6330
|
+
}
|
|
6331
|
+
if (
|
|
6332
|
+
dropCompletedToolCalls &&
|
|
6333
|
+
message.variant === "tool" &&
|
|
6334
|
+
message.toolCall?.status === "complete"
|
|
6311
6335
|
) {
|
|
6312
6336
|
return;
|
|
6313
6337
|
}
|
|
@@ -6862,6 +6886,12 @@ export const createAgentExperience = (
|
|
|
6862
6886
|
|
|
6863
6887
|
messages.forEach((message) => {
|
|
6864
6888
|
if (message.variant === "tool" && message.toolCall && showToolCalls) {
|
|
6889
|
+
// A tool call hidden by `completedVisibility: "removed"` does not
|
|
6890
|
+
// create a visible break in the transcript, so it should not split
|
|
6891
|
+
// an otherwise contiguous group.
|
|
6892
|
+
if (dropCompletedToolCalls && message.toolCall.status === "complete") {
|
|
6893
|
+
return;
|
|
6894
|
+
}
|
|
6865
6895
|
currentGroup.push(message);
|
|
6866
6896
|
return;
|
|
6867
6897
|
}
|
|
@@ -6871,7 +6901,8 @@ export const createAgentExperience = (
|
|
|
6871
6901
|
if (
|
|
6872
6902
|
message.variant === "reasoning" &&
|
|
6873
6903
|
(!showReasoning ||
|
|
6874
|
-
(
|
|
6904
|
+
(message.reasoning?.status === "complete" &&
|
|
6905
|
+
dropReasoningRow(message.reasoning)))
|
|
6875
6906
|
) {
|
|
6876
6907
|
return;
|
|
6877
6908
|
}
|