@runtypelabs/persona 4.32.0 → 4.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/activity-ui.cjs +1 -1
  2. package/dist/activity-ui.js +1 -1
  3. package/dist/{chunk-BrptyKSq.js → chunk-B3l7TOOv.js} +1 -1
  4. package/dist/{chunk-CkE90FtA.js → chunk-B4ecQzeK.js} +1 -1
  5. package/dist/{chunk-CzZBmTLd.js → chunk-B9Zu-wrk.js} +1 -1
  6. package/dist/chunk-BNE40cEG.js +1 -0
  7. package/dist/{chunk-BWCo3rCm.js → chunk-B_H0l3u2.js} +1 -1
  8. package/dist/{chunk-MdlQetfC.js → chunk-BbXc58OA.js} +1 -1
  9. package/dist/{chunk-DE0cSmku.js → chunk-BpjtKnGI.js} +1 -1
  10. package/dist/{chunk-CHAwCqpk.js → chunk-CdpVP6LZ.js} +1 -1
  11. package/dist/{chunk-FEzOcZof.js → chunk-CjkechxR.js} +1 -1
  12. package/dist/{chunk-DZg9bFK6.js → chunk-D0tcIZJM.js} +1 -1
  13. package/dist/{chunk-Cb7qy0c7.js → chunk-DNdeIFJ6.js} +1 -1
  14. package/dist/{chunk-VaT6xry9.js → chunk-DTGvDx8o.js} +1 -1
  15. package/dist/{chunk-ezlpsYGk.js → chunk-DVZ8ri1z.js} +1 -1
  16. package/dist/{chunk-DhedQnek.js → chunk-DgZZqy14.js} +1 -1
  17. package/dist/{chunk-C2IbR39s.js → chunk-UWMfWW2a.js} +1 -1
  18. package/dist/client-dispatch.js +1 -1
  19. package/dist/client-history.js +1 -1
  20. package/dist/codegen.cjs +1 -1
  21. package/dist/codegen.js +1 -1
  22. package/dist/index.cjs +4 -4
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.global.js +1 -1
  25. package/dist/index.global.js.map +1 -1
  26. package/dist/index.js +4 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/install.global.js +1 -1
  29. package/dist/install.global.js.map +1 -1
  30. package/dist/launcher.global.js +2 -2
  31. package/dist/launcher.global.js.map +1 -1
  32. package/dist/theme-editor-preview.cjs +4 -4
  33. package/dist/theme-editor-preview.js +4 -4
  34. package/dist/theme-editor.cjs +1 -1
  35. package/dist/theme-editor.js +1 -1
  36. package/dist/theme-reference.cjs +1 -1
  37. package/dist/theme-reference.js +1 -1
  38. package/dist/voice-runtime.cjs +1 -1
  39. package/dist/voice-runtime.js +1 -1
  40. package/dist/widget.css +1 -1
  41. package/package.json +1 -1
  42. package/src/index-global.ts +2 -1
  43. package/src/install.test.ts +6 -5
  44. package/src/install.ts +2 -2
  45. package/src/launcher-global.ts +2 -1
  46. package/src/session.test.ts +41 -0
  47. package/src/session.ts +14 -7
  48. package/src/styles/widget.css +12 -0
  49. package/src/utils/brand.test.ts +50 -0
  50. package/src/utils/brand.ts +6 -2
  51. package/src/utils/cdn-release.test.ts +20 -0
  52. package/src/utils/cdn-release.ts +14 -0
  53. package/src/utils/tokens.ts +2 -1
  54. package/src/utils/tool-activity-summary.test.ts +8 -0
  55. package/src/utils/tool-activity-summary.ts +2 -1
  56. package/dist/chunk-DDAwu89-.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.32.0",
3
+ "version": "4.32.2",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -37,12 +37,13 @@ export type { StreamAnimationPlugin, StreamAnimationContext } from "./types";
37
37
  // ---------------------------------------------------------------------------
38
38
 
39
39
  import { setWebMcpPolyfillLoader } from "./webmcp-bridge";
40
+ import { pinCdnRelease } from "./utils/cdn-release";
40
41
 
41
42
  // Capture at module-evaluation time: `document.currentScript` is null once
42
43
  // execution leaves the script's initial synchronous run.
43
44
  const widgetScriptSrc: string | null =
44
45
  typeof document !== "undefined"
45
- ? ((document.currentScript as HTMLScriptElement | null)?.src ?? null)
46
+ ? pinCdnRelease((document.currentScript as HTMLScriptElement | null)?.src ?? "") || null
46
47
  : null;
47
48
 
48
49
  /**
@@ -10,6 +10,7 @@
10
10
  // `vi.runAllTimersAsync()` to drive it forward deterministically.
11
11
 
12
12
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { version } from "../package.json";
13
14
 
14
15
  const PERSONA_EVENTS = [
15
16
  "persona:script-load",
@@ -464,11 +465,11 @@ describe("install.ts: CDN base derivation", () => {
464
465
  return { cssHref: css?.href ?? null, scripts };
465
466
  }
466
467
 
467
- it("derives asset URLs from the installer's own script directory (first-party CDN)", async () => {
468
+ it("pins first-party latest installer assets to the embedded release", async () => {
468
469
  setCurrentScript("https://cdn.runtype.com/persona/latest/install.global.js");
469
470
  const { cssHref, scripts } = await installedUrls({});
470
- expect(cssHref).toBe("https://cdn.runtype.com/persona/latest/widget.css");
471
- expect(scripts).toContain("https://cdn.runtype.com/persona/latest/launcher.global.js");
471
+ expect(cssHref).toBe(`https://cdn.runtype.com/persona/${version}/widget.css`);
472
+ expect(scripts).toContain(`https://cdn.runtype.com/persona/${version}/launcher.global.js`);
472
473
  });
473
474
 
474
475
  it("keeps npm-CDN siblings on the installer's own pinned range (no @latest skew)", async () => {
@@ -478,13 +479,13 @@ describe("install.ts: CDN base derivation", () => {
478
479
  expect(scripts).toContain("https://cdn.jsdelivr.net/npm/@runtypelabs/persona@4/dist/launcher.global.js");
479
480
  });
480
481
 
481
- it("the full bundle URL follows the installer directory on the eager path", async () => {
482
+ it("pins the full bundle on the eager first-party latest path", async () => {
482
483
  setCurrentScript("https://cdn.runtype.com/persona/latest/install.global.js");
483
484
  markCssLoaded();
484
485
  await install({ config: { apiUrl: "/api", launcher: { enabled: false } } });
485
486
  await flush();
486
487
  const scripts = Array.from(document.head.querySelectorAll("script")).map((s) => s.src);
487
- expect(scripts).toContain("https://cdn.runtype.com/persona/latest/index.global.js");
488
+ expect(scripts).toContain(`https://cdn.runtype.com/persona/${version}/index.global.js`);
488
489
  });
489
490
 
490
491
  it("falls back to jsDelivr@latest when currentScript is unavailable", async () => {
package/src/install.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * if configuration is provided via window.siteAgentConfig
5
5
  */
6
6
 
7
- export {};
7
+ import { pinCdnRelease } from "./utils/cdn-release";
8
8
 
9
9
  import type { BrandConfig } from "./types/theme";
10
10
  import { delayInstallLoading } from "./utils/install-loading";
@@ -201,7 +201,7 @@ declare global {
201
201
  const getInstallerBase = (): string | null => {
202
202
  if (!installerSrc) return null;
203
203
  try {
204
- const url = new URL(installerSrc, document.baseURI);
204
+ const url = new URL(pinCdnRelease(installerSrc), document.baseURI);
205
205
  if (url.protocol !== "http:" && url.protocol !== "https:") return null;
206
206
  return url.origin + url.pathname.slice(0, url.pathname.lastIndexOf("/"));
207
207
  } catch {
@@ -24,6 +24,7 @@ import { createLauncherSurface } from "./components/launcher";
24
24
  import { applyLauncherThemeVariables } from "./utils/theme";
25
25
  import { DEFAULT_LAUNCHER_CONFIG, DEFAULT_LAUNCHER_V5 } from "./defaults";
26
26
  import { setIconsExtraLoader } from "./icons-extra-loader";
27
+ import { pinCdnRelease } from "./utils/cdn-release";
27
28
  import type { AgentWidgetConfig } from "./types";
28
29
 
29
30
  // ---------------------------------------------------------------------------
@@ -33,7 +34,7 @@ import type { AgentWidgetConfig } from "./types";
33
34
  // ---------------------------------------------------------------------------
34
35
  const launcherScriptSrc: string | null =
35
36
  typeof document !== "undefined"
36
- ? ((document.currentScript as HTMLScriptElement | null)?.src ?? null)
37
+ ? pinCdnRelease((document.currentScript as HTMLScriptElement | null)?.src ?? "") || null
37
38
  : null;
38
39
 
39
40
  setIconsExtraLoader(() => {
@@ -1070,6 +1070,47 @@ describe('AgentWidgetSession - dispatch error fallback', () => {
1070
1070
  expect(session.getStatus()).toBe('idle');
1071
1071
  });
1072
1072
 
1073
+ it('does not blame the backend for an internal stream TypeError after a successful response', async () => {
1074
+ const response = new Response('data: {}\n\n', { headers: { 'Content-Type': 'text/event-stream' } });
1075
+ const fault = new TypeError('t.streamGeneration is not a function');
1076
+ vi.spyOn(response.body!, 'getReader').mockImplementation(() => { throw fault; });
1077
+ global.fetch = vi.fn().mockResolvedValue(response);
1078
+ const errors: Error[] = [];
1079
+ const session = new AgentWidgetSession({ apiUrl: 'http://example.invalid/chat' }, {
1080
+ onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {},
1081
+ onError: error => errors.push(error),
1082
+ });
1083
+ await session.sendMessage('Hello');
1084
+ const content = lastAssistantMessage(session)?.content;
1085
+ expect(content).toContain('the chat encountered an unexpected error');
1086
+ expect(content).toContain('t.streamGeneration is not a function');
1087
+ expect(content).not.toContain('backend');
1088
+ expect(content).not.toContain("couldn't reach");
1089
+ expect(errors).toContain(fault);
1090
+ expect(session.isStreaming()).toBe(false);
1091
+ });
1092
+
1093
+ it.each(['Failed to fetch', 'Load failed', 'NetworkError when attempting to fetch resource.', 'fetch failed'])(
1094
+ 'recognizes the browser fetch TypeError: %s', async (message) => {
1095
+ failFetchWith(new TypeError(message));
1096
+ const session = new AgentWidgetSession({ apiUrl: 'http://example.invalid/chat' }, {
1097
+ onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {},
1098
+ });
1099
+ await session.sendMessage('Hello');
1100
+ expect(lastAssistantMessage(session)?.content).toContain("I couldn't reach the assistant");
1101
+ },
1102
+ );
1103
+
1104
+ it('describes a server rejection without claiming the server was unreachable', async () => {
1105
+ global.fetch = vi.fn().mockResolvedValue(new Response('unavailable', { status: 503 }));
1106
+ const session = new AgentWidgetSession({ apiUrl: 'http://example.invalid/chat' }, {
1107
+ onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {},
1108
+ });
1109
+ await session.sendMessage('Hello');
1110
+ expect(lastAssistantMessage(session)?.content).toContain("chat service couldn't complete the request");
1111
+ expect(lastAssistantMessage(session)?.content).toContain('_Details: Chat backend request failed: 503_');
1112
+ });
1113
+
1073
1114
  it('uses a static errorMessage override verbatim', async () => {
1074
1115
  failFetchWith(new Error('boom'));
1075
1116
  const session = new AgentWidgetSession(
package/src/session.ts CHANGED
@@ -265,10 +265,9 @@ export class SessionHistoryError extends Error {
265
265
  }
266
266
 
267
267
  /**
268
- * Build the user-facing detail shown when a dispatch fails. This fires on real network/server errors
269
- * (connection refused, CORS, 4xx/5xx, malformed stream): not just an
270
- * un-wired proxy, so the copy stays honest about the failure and surfaces the
271
- * underlying reason to help with debugging.
268
+ * Dispatch can fail in transport, stream parsing, lazy modules or application
269
+ * callbacks. Only recognized fetch failures should suggest connectivity;
270
+ * unknown/internal failures must not send users to debug a healthy backend.
272
271
  *
273
272
  * Callers can override the copy via `config.errorMessage` (a static string or
274
273
  * a function of the error). An override that returns an empty string yields ""
@@ -283,9 +282,17 @@ function buildDispatchErrorContent(
283
282
  if (typeof override === "string") return override;
284
283
  if (typeof override === "function") return override(err);
285
284
 
286
- const base =
287
- "Sorry: I couldn't reach the assistant. The chat service didn't respond. Please check that your proxy or backend is running and reachable, then try again.";
288
- return err.message ? `${base}\n\n_Details: ${err.message}_` : base;
285
+ // Fetch rejects with TypeError too, so the error class cannot distinguish
286
+ // an offline browser from an internal fault such as a missing function.
287
+ const detail = err.message.trim();
288
+ const networkFailure = /^(Failed to fetch|fetch failed|Load failed|NetworkError when attempting to fetch resource\.?)$/i.test(detail);
289
+ const backendFailure = /^(Chat backend|Agent execution) request failed: \d{3}\b/.test(detail);
290
+ const base = networkFailure
291
+ ? "Sorry: I couldn't reach the assistant. Please check your connection and try again."
292
+ : backendFailure
293
+ ? "Sorry: the chat service couldn't complete the request. Please try again."
294
+ : "Sorry: the chat encountered an unexpected error. Please reload the page and try again.";
295
+ return detail ? `${base}\n\n_Details: ${detail}_` : base;
289
296
  }
290
297
 
291
298
  /**
@@ -6993,6 +6993,18 @@
6993
6993
  .persona-turn-activity-body .persona-tool-bubble.persona-activity-row {
6994
6994
  --persona-activity-height: var(--persona-components-toolBubble-rowHeight, var(--persona-components-activity-rowHeight, 28px));
6995
6995
  }
6996
+ [data-persona-defaults="v5"] [data-persona-tool-group-stack] .persona-tool-bubble.persona-activity-row {
6997
+ --persona-activity-height: var(--persona-components-toolBubble-rowHeight, var(--persona-components-activity-rowHeight, 24px));
6998
+ }
6999
+ [data-persona-defaults="v5"] [data-persona-tool-group-stack] .persona-activity-header:not(.persona-theme-padding) {
7000
+ padding-block: 2px;
7001
+ line-height: max(18px, calc(var(--persona-activity-label-size) * 1.35));
7002
+ }
7003
+ @media (any-pointer: coarse) {
7004
+ [data-persona-defaults="v5"] [data-persona-tool-group-stack] .persona-tool-bubble.persona-activity-row {
7005
+ --persona-activity-height: var(--persona-components-toolBubble-rowHeight, var(--persona-components-activity-rowHeight, 44px));
7006
+ }
7007
+ }
6996
7008
  .persona-turn-activity-body .persona-reasoning-bubble.persona-activity-row {
6997
7009
  --persona-activity-height: var(--persona-components-reasoningBubble-rowHeight, var(--persona-components-activity-rowHeight, 28px));
6998
7010
  }
@@ -73,6 +73,56 @@ describe('brand derivation', () => {
73
73
  expect(theme.components?.panel?.modes?.floating?.border).toContain('var(--persona-border)');
74
74
  expect(theme.components?.panel?.shadow).toBe(theme.palette?.shadows?.xl);
75
75
  });
76
+ describe.each(['light', 'dark'] as const)('%s message presentation', (colorScheme) => {
77
+ const elevations = [
78
+ ['flat', 'none'],
79
+ ['subtle', '0 2px 8px rgba(0, 0, 0, 0.08)'],
80
+ ['raised', '0 12px 32px rgba(0, 0, 0, 0.18)'],
81
+ ] as const;
82
+ it.each(elevations)('keeps V5 prose plain with %s elevation at every density', (elevation, shadow) => {
83
+ for (const density of ['compact', 'comfortable', 'spacious'] as const) {
84
+ const config = { colorScheme, future: { v5Defaults: true }, brand: { elevation, density } };
85
+ const vars = getCssVariables(getActiveTheme(config));
86
+ expect(vars['--persona-message-assistant-shadow']).toBe('none');
87
+ expect(vars['--persona-message-assistant-border']).toBe('transparent');
88
+ expect(vars['--persona-message-assistant-padding']).toBe('0px');
89
+ // These existing surfaces still follow elevation; this is not a global
90
+ // rule that every default of `none` must opt out of brand styling.
91
+ expect(vars['--persona-message-user-shadow']).toBe(shadow);
92
+ expect(getActiveTheme(config).components.introCard?.shadow).toBe(shadow);
93
+ const v4 = getCssVariables(getActiveTheme({ ...config, future: { v5Defaults: false } }));
94
+ expect(v4['--persona-message-assistant-shadow']).toBe(shadow);
95
+ expect(v4['--persona-message-assistant-border']).not.toBe('transparent');
96
+ }
97
+ });
98
+ it.each(elevations)('lets custom padded bubbles follow %s elevation', (elevation, shadow) => {
99
+ for (const [density, padding] of [['compact', '8px'], ['comfortable', '12px'], ['spacious', '16px']] as const) {
100
+ const theme = { components: { message: { assistant: {
101
+ background: 'semantic.colors.container', padding, borderRadius: '12px',
102
+ border: 'semantic.colors.border', borderWidth: '1px', shadow: 'palette.shadows.sm',
103
+ } } } };
104
+ // Shared overrides and dark-only overrides both remain authoritative.
105
+ for (const overrides of colorScheme === 'dark' ? [{ theme }, { darkTheme: theme }] : [{ theme }]) {
106
+ const vars = getCssVariables(getActiveTheme({
107
+ colorScheme, future: { v5Defaults: true }, brand: { elevation, density }, ...overrides,
108
+ }));
109
+ expect(vars['--persona-message-assistant-shadow']).toBe(shadow);
110
+ expect(vars['--persona-message-assistant-padding']).toBe(padding);
111
+ expect(vars['--persona-message-assistant-border']).not.toBe('transparent');
112
+ expect(vars['--persona-message-assistant-border-width']).toBe('1px');
113
+ }
114
+ }
115
+ });
116
+ it('preserves explicit assistant shadows and borders over brand elevation', () => {
117
+ const vars = getCssVariables(getActiveTheme({
118
+ colorScheme, future: { v5Defaults: true }, brand: { elevation: 'raised' },
119
+ theme: { components: { message: { assistant: { shadow: '0 1px 2px red', border: '#123456' } } } },
120
+ ...(colorScheme === 'dark' ? { darkTheme: { components: { message: { assistant: { shadow: 'none', border: '#abcdef' } } } } } : {}),
121
+ }));
122
+ expect(vars['--persona-message-assistant-shadow']).toBe(colorScheme === 'dark' ? 'none' : '0 1px 2px red');
123
+ expect(vars['--persona-message-assistant-border']).toBe(colorScheme === 'dark' ? '#abcdef' : '#123456');
124
+ });
125
+ });
76
126
  it.each([false, true])('keeps neutral-derived surfaces and elevation coherent (%s)', (v5Defaults) => {
77
127
  const theme = getActiveTheme({ future: { v5Defaults }, brand: { neutral: '#161b22', radius: 'none', elevation: 'flat' } });
78
128
  const vars = getCssVariables(theme);
@@ -111,8 +111,12 @@ export const deriveBrandTheme = (brand?: BrandConfig, scheme: 'light' | 'dark' =
111
111
  const shadow = flat ? 'none' : brand.elevation === 'subtle' ? '0 2px 8px rgba(0, 0, 0, 0.08)' : '0 12px 32px rgba(0, 0, 0, 0.18)';
112
112
  const border = flat ? '1px solid var(--persona-border)' : '1px solid color-mix(in srgb, var(--persona-border) 65%, transparent)';
113
113
  theme.palette = { ...theme.palette, shadows: { sm: shadow, md: shadow, lg: shadow, xl: shadow, '2xl': shadow } };
114
- const shadows = (node: Record<string, unknown>): Record<string, unknown> => {
114
+ const shadows = (node: Record<string, unknown>, path = ''): Record<string, unknown> => {
115
115
  const overlay: Record<string, unknown> = {};
116
+ // V5 assistant defaults are unpadded prose, not a surface to elevate.
117
+ // Explicit theme tokens still win; custom bubbles can opt into the brand
118
+ // shadow scale with `palette.shadows.sm` alongside their padding/background.
119
+ if (v5Defaults && path === 'message.assistant') return overlay;
116
120
  for (const [key, value] of Object.entries(node)) {
117
121
  if (key.toLowerCase().endsWith('shadow')) {
118
122
  overlay[key] = shadow;
@@ -120,7 +124,7 @@ export const deriveBrandTheme = (brand?: BrandConfig, scheme: 'light' | 'dark' =
120
124
  if (borderKey in node) overlay[borderKey] = typeof node[borderKey] === 'string' && (node[borderKey] as string).includes('solid') ? border : flat ? 'semantic.colors.border' : 'color-mix(in srgb, var(--persona-border) 65%, transparent)';
121
125
  }
122
126
  else if (value && typeof value === 'object') {
123
- const child = shadows(value as Record<string, unknown>);
127
+ const child = shadows(value as Record<string, unknown>, path ? `${path}.${key}` : key);
124
128
  if (Object.keys(child).length) overlay[key] = child;
125
129
  }
126
130
  }
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { version } from "../../package.json";
3
+ import { pinCdnRelease } from "./cdn-release";
4
+
5
+ describe("pinCdnRelease", () => {
6
+ it.each(["install", "index", "launcher"])("pins the %s entry's dependencies to its embedded release", (entry) => {
7
+ expect(pinCdnRelease(`https://cdn.runtype.com/persona/latest/${entry}.global.js?embed=1`))
8
+ .toBe(`https://cdn.runtype.com/persona/${version}/${entry}.global.js?embed=1`);
9
+ });
10
+
11
+ it.each([
12
+ "https://cdn.runtype.com/persona/4.0.0/index.global.js",
13
+ "https://cdn.example.com/persona/latest/index.global.js",
14
+ "https://cdn.jsdelivr.net/npm/@runtypelabs/persona@4/dist/index.global.js",
15
+ "https://cdn.runtype.com/other/latest/index.global.js",
16
+ "",
17
+ ])("preserves explicit versions and other hosting layouts: %s", (src) => {
18
+ expect(pinCdnRelease(src)).toBe(src);
19
+ });
20
+ });
@@ -0,0 +1,14 @@
1
+ import { version } from "../../package.json";
2
+
3
+ /**
4
+ * A cached /latest/ entry must keep using its own release, even after another
5
+ * release replaces latest. Only rewrite our published CDN layout; custom hosts
6
+ * and explicitly versioned URLs retain their existing sibling resolution.
7
+ * The bundler embeds this package version in each script-tag entry.
8
+ */
9
+ export function pinCdnRelease(src: string): string {
10
+ return src.replace(
11
+ /^https:\/\/cdn\.runtype\.com\/persona\/latest\//,
12
+ `https://cdn.runtype.com/persona/${version}/`,
13
+ );
14
+ }
@@ -603,7 +603,8 @@ export const DEFAULT_COMPONENTS_V5: ComponentTokens = {
603
603
  },
604
604
  launcher: CIRCLE_LAUNCHER_TOKENS,
605
605
  activity: {
606
- rowHeight: "32px", labelSize: "13px", iconSize: "16px", indent: "0px",
606
+ // CSS owns contextual parent, nested, and coarse-pointer row heights.
607
+ labelSize: "13px", iconSize: "16px", indent: "0px",
607
608
  transcriptGap: "0px", responseGap: "12px",
608
609
  groupIndent: "16px", groupGap: "2px", groupPadding: "2px 0",
609
610
  bodyPadding: "4px 0", bodyFontWeight: "400", sectionGap: "8px", bodySurface: "transparent",
@@ -6,6 +6,7 @@ const tool = (name: string, extra: Partial<AgentWidgetToolCall> = {}): AgentWidg
6
6
 
7
7
  describe("semantic tool activity", () => {
8
8
  it.each([
9
+ ["search_docs", "Searched docs"], ["read_file", "Read a file"],
9
10
  ["web_search", "Searched the web"], ["web_search_preview", "Searched the web"], ["WebSearch", "Searched the web"],
10
11
  ["web_fetch", "Read a web page"], ["WebFetch", "Read a web page"],
11
12
  ["file_search", "Searched files"], ["search_files", "Searched files"], ["Glob", "Searched files"],
@@ -22,6 +23,12 @@ describe("semantic tool activity", () => {
22
23
  ["sql_db_query", "Queried the database"], ["search_gmail", "Searched email"],
23
24
  ])("maps exact alias %s", (name, label) => expect(toolActivityLabel(tool(name))).toBe(label));
24
25
 
26
+ it("summarizes documentation searches and file reads without a custom hook", () => {
27
+ expect(summarizeToolActivity([tool("search_docs"), tool("read_file")])).toBe("Searched docs, read files");
28
+ expect(summarizeToolActivity([tool("search_docs", { status: "running" }), tool("read_file", { status: "pending" })])).toBe("Searching docs, reading files");
29
+ expect(toolActivityLabel(tool("read_file", { status: "running" }))).toBe("Reading a file");
30
+ });
31
+
25
32
  it.each(["search_events", "search_nodes", "read_graph", "open_nodes", "read", "search", "git_commit", "my_web_search", "web_search_custom"])("keeps ambiguous %s generic", name => {
26
33
  expect(toolActivityLabel(tool(name))).toMatch(/^Used /);
27
34
  });
@@ -127,6 +134,7 @@ describe("semantic tool activity", () => {
127
134
  });
128
135
 
129
136
  it.each([
137
+ ["search_docs", "search"], ["read_file", "file-text"],
130
138
  ["web_search", "globe"], ["Read", "file-text"], ["edit_file", "pencil"], ["Bash", "terminal"],
131
139
  ["sql_db_query", "database"], ["search_events", "wrench"], ["my_web_search", "wrench"],
132
140
  ])("maps the completed icon for %s without guessing", (name, icon) => expect(toolActivityIcon(tool(name))).toBe(icon));
@@ -3,10 +3,11 @@ import type { AgentWidgetConfig, AgentWidgetToolCall } from "../types";
3
3
  // Exact documented names only. Ambiguous names such as search_events or
4
4
  // search_nodes need provider metadata, which tool messages do not carry.
5
5
  const categories = [
6
+ ["search_docs", "Searching docs", "Searched docs", "search"],
6
7
  ["web_search|web_search_preview|WebSearch", "Searching the web", "Searched the web", "globe"],
7
8
  ["web_fetch|WebFetch", "Reading web pages", "Read web pages", "globe", "single"],
8
9
  ["file_search|search_files|Glob", "Searching files", "Searched files", "search"],
9
- ["read_text_file|read_multiple_files|Read", "Reading files", "Read files", "file-text", "single"],
10
+ ["read_file|read_text_file|read_multiple_files|Read", "Reading files", "Read files", "file-text", "single"],
10
11
  ["write_file|edit_file|apply_patch|Write|Edit|NotebookEdit", "Editing files", "Edited files", "pencil", "single"],
11
12
  ["list_directory|list_directory_with_sizes|directory_tree", "Browsing files", "Browsed files", "folder"],
12
13
  ["Grep", "Searching code", "Searched code", "search"],
@@ -1 +0,0 @@
1
- const e=`4.32.0`;export{e as t};