@runtypelabs/persona 3.29.1 → 3.30.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.
Files changed (53) hide show
  1. package/README.md +55 -0
  2. package/dist/codegen.cjs +6 -6
  3. package/dist/codegen.js +4 -4
  4. package/dist/index.cjs +47 -47
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +112 -7
  7. package/dist/index.d.ts +112 -7
  8. package/dist/index.global.js +59 -59
  9. package/dist/index.global.js.map +1 -1
  10. package/dist/index.js +47 -47
  11. package/dist/index.js.map +1 -1
  12. package/dist/launcher.global.js +2 -2
  13. package/dist/launcher.global.js.map +1 -1
  14. package/dist/plugin-kit.cjs +1 -0
  15. package/dist/plugin-kit.d.cts +98 -0
  16. package/dist/plugin-kit.d.ts +98 -0
  17. package/dist/plugin-kit.js +1 -0
  18. package/dist/smart-dom-reader.d.cts +107 -4
  19. package/dist/smart-dom-reader.d.ts +107 -4
  20. package/dist/theme-editor.cjs +38 -38
  21. package/dist/theme-editor.d.cts +111 -6
  22. package/dist/theme-editor.d.ts +111 -6
  23. package/dist/theme-editor.js +38 -38
  24. package/dist/theme-reference.cjs +1 -1
  25. package/dist/theme-reference.d.cts +2 -0
  26. package/dist/theme-reference.d.ts +2 -0
  27. package/dist/theme-reference.js +1 -1
  28. package/package.json +8 -2
  29. package/src/components/approval-bubble.test.ts +268 -0
  30. package/src/components/approval-bubble.ts +170 -20
  31. package/src/components/launcher.ts +6 -3
  32. package/src/components/tool-bubble.test.ts +39 -0
  33. package/src/components/tool-bubble.ts +4 -0
  34. package/src/index-core.ts +1 -0
  35. package/src/plugin-kit.test.ts +230 -0
  36. package/src/plugin-kit.ts +294 -0
  37. package/src/plugins/types.ts +49 -2
  38. package/src/session.test.ts +99 -0
  39. package/src/session.ts +14 -3
  40. package/src/theme-editor/preview-utils.test.ts +10 -0
  41. package/src/theme-editor/preview-utils.ts +29 -1
  42. package/src/theme-editor/sections.test.ts +17 -0
  43. package/src/theme-editor/sections.ts +31 -0
  44. package/src/theme-reference.ts +1 -1
  45. package/src/types/theme.ts +2 -0
  46. package/src/types.ts +59 -2
  47. package/src/ui.approval-plugin.test.ts +204 -0
  48. package/src/ui.ts +149 -16
  49. package/src/utils/theme.test.ts +6 -2
  50. package/src/utils/theme.ts +0 -8
  51. package/src/utils/tokens.ts +6 -1
  52. package/src/webmcp-bridge.test.ts +66 -0
  53. package/src/webmcp-bridge.ts +49 -0
@@ -27,6 +27,7 @@ vi.mock("@mcp-b/webmcp-polyfill", () => ({
27
27
  // Import AFTER vi.mock so the bridge's dynamic import resolves to the mock.
28
28
  import {
29
29
  WebMcpBridge,
30
+ getWebMcpToolDisplayTitle,
30
31
  isWebMcpToolName,
31
32
  stripWebMcpPrefix,
32
33
  computeClientToolsFingerprint,
@@ -39,6 +40,7 @@ type MockTool = {
39
40
  name: string;
40
41
  description?: string;
41
42
  inputSchema?: object;
43
+ title?: string;
42
44
  execute: (args: Record<string, unknown>, client: MockClient) => unknown;
43
45
  };
44
46
 
@@ -47,10 +49,13 @@ const registry: { tools: MockTool[] } = { tools: [] };
47
49
  /** A fake `document.modelContext` exposing the strict consumer surface. */
48
50
  const makeModelContext = () => ({
49
51
  async getTools() {
52
+ // Mirrors the real polyfill's getRegisteredToolInfos(): `title` is always
53
+ // present, "" when the tool didn't declare one; annotations are absent.
50
54
  return registry.tools.map((t) => ({
51
55
  name: t.name,
52
56
  description: t.description ?? `mock ${t.name}`,
53
57
  inputSchema: JSON.stringify(t.inputSchema ?? { type: "object" }),
58
+ title: t.title ?? "",
54
59
  }));
55
60
  },
56
61
  async executeTool(
@@ -199,6 +204,67 @@ describe("WebMcpBridge.snapshotForDispatch", () => {
199
204
  });
200
205
  });
201
206
 
207
+ describe("WebMCP display titles", () => {
208
+ it("records declared titles on snapshot and exposes them via getWebMcpToolDisplayTitle", async () => {
209
+ registry.tools = [
210
+ fakeTool({ name: "add_to_cart", title: "Add to Cart" }),
211
+ fakeTool({ name: "search_products" }),
212
+ ];
213
+ const bridge = new WebMcpBridge({ enabled: true });
214
+ await bridge.snapshotForDispatch();
215
+ expect(getWebMcpToolDisplayTitle("add_to_cart")).toBe("Add to Cart");
216
+ expect(getWebMcpToolDisplayTitle("webmcp:add_to_cart")).toBe("Add to Cart");
217
+ expect(getWebMcpToolDisplayTitle("search_products")).toBeUndefined();
218
+ });
219
+
220
+ it("evicts a stale title when the tool re-registers without one", async () => {
221
+ registry.tools = [fakeTool({ name: "add_to_cart", title: "Add to Cart" })];
222
+ const bridge = new WebMcpBridge({ enabled: true });
223
+ await bridge.snapshotForDispatch();
224
+ expect(getWebMcpToolDisplayTitle("add_to_cart")).toBe("Add to Cart");
225
+
226
+ registry.tools = [fakeTool({ name: "add_to_cart" })];
227
+ await bridge.snapshotForDispatch();
228
+ expect(getWebMcpToolDisplayTitle("add_to_cart")).toBeUndefined();
229
+ });
230
+
231
+ it("evicts the title when the tool is removed from the registry entirely", async () => {
232
+ registry.tools = [
233
+ fakeTool({ name: "add_to_cart", title: "Add to Cart" }),
234
+ fakeTool({ name: "search_products", title: "Search the catalog" }),
235
+ ];
236
+ const bridge = new WebMcpBridge({ enabled: true });
237
+ await bridge.snapshotForDispatch();
238
+ expect(getWebMcpToolDisplayTitle("add_to_cart")).toBe("Add to Cart");
239
+
240
+ registry.tools = [fakeTool({ name: "search_products", title: "Search the catalog" })];
241
+ await bridge.snapshotForDispatch();
242
+ expect(getWebMcpToolDisplayTitle("add_to_cart")).toBeUndefined();
243
+ expect(getWebMcpToolDisplayTitle("search_products")).toBe("Search the catalog");
244
+ });
245
+
246
+ it("passes the declared title to the confirm gate", async () => {
247
+ registry.tools = [fakeTool({ name: "add_to_cart", title: "Add to Cart" })];
248
+ const onConfirm = vi.fn(async () => true);
249
+ const bridge = new WebMcpBridge({ enabled: true, onConfirm });
250
+ const r = await bridge.executeToolCall("webmcp:add_to_cart", {});
251
+ expect(r.isError).toBeFalsy();
252
+ expect(onConfirm).toHaveBeenCalledWith(
253
+ expect.objectContaining({ toolName: "add_to_cart", title: "Add to Cart" })
254
+ );
255
+ });
256
+
257
+ it("omits title from the confirm gate when the tool didn't declare one", async () => {
258
+ registry.tools = [fakeTool({ name: "add_to_cart" })];
259
+ const onConfirm = vi.fn(async () => true);
260
+ const bridge = new WebMcpBridge({ enabled: true, onConfirm });
261
+ await bridge.executeToolCall("webmcp:add_to_cart", {});
262
+ expect(onConfirm).toHaveBeenCalledWith(
263
+ expect.not.objectContaining({ title: expect.anything() })
264
+ );
265
+ });
266
+ });
267
+
202
268
  describe("WebMcpBridge.executeToolCall", () => {
203
269
  it("returns isError when WebMCP is not enabled", async () => {
204
270
  const bridge = new WebMcpBridge({} as AgentWidgetWebMcpConfig);
@@ -66,6 +66,14 @@ interface ModelContextToolInfo {
66
66
  description: string;
67
67
  /** JSON-encoded JSON Schema for the tool's input. */
68
68
  inputSchema?: string;
69
+ /**
70
+ * Display title declared on the tool (`ToolDescriptor.title` in the WebMCP
71
+ * spec). The polyfill returns `""` when the tool didn't declare one. Note:
72
+ * `annotations` (incl. the legacy `annotations.title`) are NOT exposed on
73
+ * this strict consumer surface — top-level `title` is the only display-name
74
+ * channel available to us.
75
+ */
76
+ title?: string;
69
77
  }
70
78
 
71
79
  interface ModelContextCoreLike {
@@ -77,6 +85,43 @@ interface ModelContextCoreLike {
77
85
  ): Promise<string | null>;
78
86
  }
79
87
 
88
+ /**
89
+ * Page-global map of bare tool name → declared display title
90
+ * (`ToolDescriptor.title`). `document.modelContext` is page-global, so a
91
+ * single map shared across widget/bridge instances is semantically correct.
92
+ * Refreshed on every registry read (`snapshotForDispatch` / `executeToolCall`)
93
+ * and consumed by the approval bubble's summary line via
94
+ * `getWebMcpToolDisplayTitle`.
95
+ */
96
+ const webMcpToolDisplayTitles = new Map<string, string>();
97
+
98
+ /**
99
+ * Record declared display titles from a fresh `getTools()` read. The map is
100
+ * rebuilt from scratch — callers always pass the FULL registry snapshot — so
101
+ * a tool that unregistered or dropped its title can't leave a stale label
102
+ * behind. Exported for tests; production callers are the bridge's registry
103
+ * reads.
104
+ */
105
+ export const recordWebMcpToolDisplayTitles = (
106
+ infos: ModelContextToolInfo[],
107
+ ): void => {
108
+ webMcpToolDisplayTitles.clear();
109
+ for (const info of infos) {
110
+ const title = info.title?.trim();
111
+ if (title) webMcpToolDisplayTitles.set(info.name, title);
112
+ }
113
+ };
114
+
115
+ /**
116
+ * Look up the display title a page tool declared via the WebMCP spec's
117
+ * `ToolDescriptor.title`. Accepts wire (`webmcp:add_to_cart`) or bare
118
+ * (`add_to_cart`) names. Returns `undefined` when the tool didn't declare
119
+ * one (callers fall back to humanizing the tool name).
120
+ */
121
+ export const getWebMcpToolDisplayTitle = (
122
+ toolName: string,
123
+ ): string | undefined => webMcpToolDisplayTitles.get(stripWebMcpPrefix(toolName));
124
+
80
125
  const log = {
81
126
  warn(message: string, ...rest: unknown[]): void {
82
127
  if (typeof console !== "undefined" && typeof console.warn === "function") {
@@ -220,6 +265,7 @@ export class WebMcpBridge {
220
265
  log.warn("getTools() threw — shipping an empty WebMCP snapshot.", err);
221
266
  return [];
222
267
  }
268
+ recordWebMcpToolDisplayTitles(infos);
223
269
 
224
270
  const pageOrigin = typeof location !== "undefined" ? location.origin : "";
225
271
 
@@ -295,6 +341,7 @@ export class WebMcpBridge {
295
341
  const message = err instanceof Error ? err.message : String(err);
296
342
  return errorResult(`Failed to read WebMCP registry: ${message}`);
297
343
  }
344
+ recordWebMcpToolDisplayTitles(infos);
298
345
  const info = infos.find((candidate) => candidate.name === bareName);
299
346
 
300
347
  if (!info) {
@@ -323,10 +370,12 @@ export class WebMcpBridge {
323
370
 
324
371
  // Confirm-by-default gate. Every `webmcp:*` call routes through here,
325
372
  // regardless of `annotations.readOnlyHint`.
373
+ const displayTitle = getWebMcpToolDisplayTitle(bareName);
326
374
  const gateInfo: WebMcpConfirmInfo = {
327
375
  toolName: bareName,
328
376
  args,
329
377
  description: info.description,
378
+ ...(displayTitle ? { title: displayTitle } : {}),
330
379
  reason: "gate",
331
380
  };
332
381
  if (!(await this.requestConfirm(gateInfo))) {