@runtypelabs/persona 3.29.0 → 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 (63) hide show
  1. package/README.md +55 -0
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CxvHw0X6.d.cts → types-B_Qazlm4.d.cts} +6 -0
  5. package/dist/animations/{types-CxvHw0X6.d.ts → types-B_Qazlm4.d.ts} +6 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +6 -6
  9. package/dist/codegen.js +4 -4
  10. package/dist/index.cjs +47 -47
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +127 -7
  13. package/dist/index.d.ts +127 -7
  14. package/dist/index.global.js +60 -60
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +47 -47
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js +2 -2
  19. package/dist/launcher.global.js.map +1 -1
  20. package/dist/plugin-kit.cjs +1 -0
  21. package/dist/plugin-kit.d.cts +98 -0
  22. package/dist/plugin-kit.d.ts +98 -0
  23. package/dist/plugin-kit.js +1 -0
  24. package/dist/smart-dom-reader.d.cts +113 -4
  25. package/dist/smart-dom-reader.d.ts +113 -4
  26. package/dist/theme-editor.cjs +38 -38
  27. package/dist/theme-editor.d.cts +117 -6
  28. package/dist/theme-editor.d.ts +117 -6
  29. package/dist/theme-editor.js +38 -38
  30. package/dist/theme-reference.cjs +1 -1
  31. package/dist/theme-reference.d.cts +2 -0
  32. package/dist/theme-reference.d.ts +2 -0
  33. package/dist/theme-reference.js +1 -1
  34. package/package.json +8 -2
  35. package/src/client.test.ts +40 -0
  36. package/src/client.ts +18 -4
  37. package/src/components/approval-bubble.test.ts +268 -0
  38. package/src/components/approval-bubble.ts +170 -20
  39. package/src/components/launcher.ts +6 -3
  40. package/src/components/tool-bubble.test.ts +39 -0
  41. package/src/components/tool-bubble.ts +4 -0
  42. package/src/generated/runtype-openapi-contract.ts +12 -0
  43. package/src/index-core.ts +1 -0
  44. package/src/plugin-kit.test.ts +230 -0
  45. package/src/plugin-kit.ts +294 -0
  46. package/src/plugins/types.ts +49 -2
  47. package/src/session.test.ts +99 -0
  48. package/src/session.ts +204 -32
  49. package/src/session.webmcp.test.ts +326 -6
  50. package/src/theme-editor/preview-utils.test.ts +10 -0
  51. package/src/theme-editor/preview-utils.ts +29 -1
  52. package/src/theme-editor/sections.test.ts +17 -0
  53. package/src/theme-editor/sections.ts +31 -0
  54. package/src/theme-reference.ts +1 -1
  55. package/src/types/theme.ts +2 -0
  56. package/src/types.ts +59 -2
  57. package/src/ui.approval-plugin.test.ts +204 -0
  58. package/src/ui.ts +149 -16
  59. package/src/utils/theme.test.ts +6 -2
  60. package/src/utils/theme.ts +0 -8
  61. package/src/utils/tokens.ts +6 -1
  62. package/src/webmcp-bridge.test.ts +66 -0
  63. package/src/webmcp-bridge.ts +49 -0
package/src/session.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AgentWidgetEvent,
6
6
  AgentWidgetMessage,
7
7
  AgentWidgetApproval,
8
+ AgentWidgetApprovalDecisionOptions,
8
9
  WebMcpConfirmInfo,
9
10
  AgentExecutionState,
10
11
  ClientSession,
@@ -76,6 +77,20 @@ function buildDispatchErrorContent(
76
77
  return err.message ? `${base}\n\n_Details: ${err.message}_` : base;
77
78
  }
78
79
 
80
+ const buildWebMcpErrorResult = (message: string) => ({
81
+ isError: true,
82
+ content: [{ type: "text" as const, text: message }],
83
+ });
84
+
85
+ const getWebMcpErrorMessage = (
86
+ error: unknown,
87
+ fallback = "WebMCP tool execution failed.",
88
+ ): string => {
89
+ if (error instanceof Error && error.message) return error.message;
90
+ if (typeof error === "string" && error) return error;
91
+ return fallback;
92
+ };
93
+
79
94
  export class AgentWidgetSession {
80
95
  private client: AgentWidgetClient;
81
96
  private messages: AgentWidgetMessage[];
@@ -1182,7 +1197,8 @@ export class AgentWidgetSession {
1182
1197
  */
1183
1198
  public async resolveApproval(
1184
1199
  approval: AgentWidgetApproval,
1185
- decision: 'approved' | 'denied'
1200
+ decision: 'approved' | 'denied',
1201
+ options?: AgentWidgetApprovalDecisionOptions
1186
1202
  ): Promise<void> {
1187
1203
  // 1. Update approval message status immediately for responsive UI
1188
1204
  const approvalMessageId = `approval-${approval.id}`;
@@ -1191,11 +1207,19 @@ export class AgentWidgetSession {
1191
1207
  status: decision,
1192
1208
  resolvedAt: Date.now(),
1193
1209
  };
1210
+ // Anchor the bubble where the agent paused for permission. An approval is a
1211
+ // timeline checkpoint, not a "now" event, so resolving it must preserve the
1212
+ // original message's createdAt/sequence — otherwise sortMessages (which
1213
+ // orders by createdAt first) would re-stamp it to now and float it past any
1214
+ // message created later (e.g. a long-pending approval resolved after more
1215
+ // conversation, or restored/replayed transcripts).
1216
+ const existing = this.messages.find((m) => m.id === approvalMessageId);
1194
1217
  const updatedMessage: AgentWidgetMessage = {
1195
1218
  id: approvalMessageId,
1196
1219
  role: "assistant",
1197
1220
  content: "",
1198
- createdAt: new Date().toISOString(),
1221
+ createdAt: existing?.createdAt ?? new Date().toISOString(),
1222
+ ...(existing?.sequence !== undefined ? { sequence: existing.sequence } : {}),
1199
1223
  streaming: false,
1200
1224
  variant: "approval",
1201
1225
  approval: updatedApproval,
@@ -1224,7 +1248,8 @@ export class AgentWidgetSession {
1224
1248
  agentId: approval.agentId,
1225
1249
  toolName: approval.toolName,
1226
1250
  },
1227
- decision
1251
+ decision,
1252
+ options
1228
1253
  );
1229
1254
  } else {
1230
1255
  response = await this.client.resolveApproval(
@@ -1595,6 +1620,78 @@ export class AgentWidgetSession {
1595
1620
  }
1596
1621
  }
1597
1622
 
1623
+ private resolveWebMcpToolStartedAt(
1624
+ toolMessage: AgentWidgetMessage,
1625
+ ): number {
1626
+ const stored = this.messages.find((m) => m.id === toolMessage.id);
1627
+ const candidates = [
1628
+ stored?.toolCall?.startedAt,
1629
+ toolMessage.toolCall?.startedAt,
1630
+ ];
1631
+ for (const candidate of candidates) {
1632
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
1633
+ return candidate;
1634
+ }
1635
+ }
1636
+ return Date.now();
1637
+ }
1638
+
1639
+ private markWebMcpToolRunning(
1640
+ toolMessage: AgentWidgetMessage,
1641
+ ): number {
1642
+ const startedAt = this.resolveWebMcpToolStartedAt(toolMessage);
1643
+ this.upsertMessage({
1644
+ ...toolMessage,
1645
+ streaming: true,
1646
+ agentMetadata: {
1647
+ ...toolMessage.agentMetadata,
1648
+ awaitingLocalTool: false,
1649
+ },
1650
+ toolCall: toolMessage.toolCall
1651
+ ? {
1652
+ ...toolMessage.toolCall,
1653
+ status: "running",
1654
+ startedAt,
1655
+ completedAt: undefined,
1656
+ duration: undefined,
1657
+ durationMs: undefined,
1658
+ }
1659
+ : toolMessage.toolCall,
1660
+ });
1661
+ return startedAt;
1662
+ }
1663
+
1664
+ private markWebMcpToolComplete(
1665
+ toolMessage: AgentWidgetMessage,
1666
+ result: unknown,
1667
+ startedAt: number,
1668
+ completedAt = Date.now(),
1669
+ ): void {
1670
+ // A teardown such as clearMessages()/hydrateMessages()/new send can remove
1671
+ // the bubble while an aborted WebMCP promise is settling. Never resurrect a
1672
+ // cleared message just to mark the old resolve complete.
1673
+ if (!this.messages.some((message) => message.id === toolMessage.id)) return;
1674
+ this.upsertMessage({
1675
+ ...toolMessage,
1676
+ streaming: false,
1677
+ agentMetadata: {
1678
+ ...toolMessage.agentMetadata,
1679
+ awaitingLocalTool: false,
1680
+ },
1681
+ toolCall: toolMessage.toolCall
1682
+ ? {
1683
+ ...toolMessage.toolCall,
1684
+ status: "complete",
1685
+ result,
1686
+ startedAt,
1687
+ completedAt,
1688
+ duration: undefined,
1689
+ durationMs: Math.max(0, completedAt - startedAt),
1690
+ }
1691
+ : toolMessage.toolCall,
1692
+ });
1693
+ }
1694
+
1598
1695
  /**
1599
1696
  * Resolve TWO OR MORE parallel local-tool awaits sharing one paused
1600
1697
  * executionId with a SINGLE `/resume` (core#3878). Each call is executed
@@ -1613,6 +1710,14 @@ export class AgentWidgetSession {
1613
1710
  executionId: string,
1614
1711
  snapshots: AgentWidgetMessage[],
1615
1712
  ): Promise<void> {
1713
+ type ExecutedWebMcpTool = {
1714
+ dedupeKey: string;
1715
+ resumeKey: string;
1716
+ output: unknown;
1717
+ toolMessage: AgentWidgetMessage;
1718
+ startedAt: number;
1719
+ completedAt: number;
1720
+ };
1616
1721
  const claimedKeys: string[] = [];
1617
1722
  const controllers: AbortController[] = [];
1618
1723
  // Dedicated controller for the shared resume fetch so cancel() can abort it
@@ -1640,14 +1745,10 @@ export class AgentWidgetSession {
1640
1745
  this.webMcpInflightKeys.add(dedupeKey);
1641
1746
  claimedKeys.push(dedupeKey);
1642
1747
 
1643
- // Clear the awaiting flag so the local-tool UI doesn't linger.
1644
- this.upsertMessage({
1645
- ...toolMessage,
1646
- agentMetadata: {
1647
- ...toolMessage.agentMetadata,
1648
- awaitingLocalTool: false,
1649
- },
1650
- });
1748
+ // Clear the awaiting flag and keep the tool bubble running while the
1749
+ // browser-side WebMCP promise is in flight. The initial `step_await`
1750
+ // only means the server paused for a local tool; it is not completion.
1751
+ const startedAt = this.markWebMcpToolRunning(toolMessage);
1651
1752
 
1652
1753
  const controller = new AbortController();
1653
1754
  this.webMcpResolveControllers.add(controller);
@@ -1686,24 +1787,43 @@ export class AgentWidgetSession {
1686
1787
  error instanceof Error ? error : new Error(String(error)),
1687
1788
  );
1688
1789
  }
1790
+ this.markWebMcpToolComplete(
1791
+ toolMessage,
1792
+ buildWebMcpErrorResult(
1793
+ isAbortError
1794
+ ? "Aborted by cancel()"
1795
+ : getWebMcpErrorMessage(error),
1796
+ ),
1797
+ startedAt,
1798
+ );
1689
1799
  // Release the dedupe claim so a re-emit can retry this call.
1690
1800
  this.webMcpInflightKeys.delete(dedupeKey);
1691
1801
  return null;
1692
1802
  }
1693
1803
  }
1694
1804
  if (controller.signal.aborted) {
1805
+ this.markWebMcpToolComplete(
1806
+ toolMessage,
1807
+ buildWebMcpErrorResult("Aborted by cancel()"),
1808
+ startedAt,
1809
+ );
1695
1810
  this.webMcpInflightKeys.delete(dedupeKey);
1696
1811
  return null;
1697
1812
  }
1698
- return { dedupeKey, resumeKey, output };
1813
+ return {
1814
+ dedupeKey,
1815
+ resumeKey,
1816
+ output,
1817
+ toolMessage,
1818
+ startedAt,
1819
+ completedAt: Date.now(),
1820
+ };
1699
1821
  }),
1700
1822
  );
1701
1823
 
1824
+ let ready: ExecutedWebMcpTool[] = [];
1702
1825
  try {
1703
- const ready = executed.filter(
1704
- (r): r is { dedupeKey: string; resumeKey: string; output: unknown } =>
1705
- r !== null,
1706
- );
1826
+ ready = executed.filter((r): r is ExecutedWebMcpTool => r !== null);
1707
1827
  // Everything deduped/aborted/failed — nothing to post.
1708
1828
  if (ready.length === 0) return;
1709
1829
 
@@ -1723,9 +1843,17 @@ export class AgentWidgetSession {
1723
1843
  throw new Error(errorData?.error ?? `Resume failed: ${response.status}`);
1724
1844
  }
1725
1845
  // Server accepted the batch — mark every included call resolved so stale
1726
- // re-emits don't re-execute the page tool.
1846
+ // re-emits don't re-execute the page tool, then complete each bubble.
1847
+ // Do this only after /resume HTTP success; if /resume fails, the server
1848
+ // may still be paused and the retry path must not show a final result.
1727
1849
  for (const r of ready) {
1728
1850
  this.webMcpResolvedKeys.add(r.dedupeKey);
1851
+ this.markWebMcpToolComplete(
1852
+ r.toolMessage,
1853
+ r.output,
1854
+ r.startedAt,
1855
+ r.completedAt,
1856
+ );
1729
1857
  }
1730
1858
  if (response.body) {
1731
1859
  await this.connectStream(response.body, { allowReentry: true });
@@ -1740,6 +1868,14 @@ export class AgentWidgetSession {
1740
1868
  this.callbacks.onError?.(
1741
1869
  error instanceof Error ? error : new Error(String(error)),
1742
1870
  );
1871
+ } else {
1872
+ for (const r of ready) {
1873
+ this.markWebMcpToolComplete(
1874
+ r.toolMessage,
1875
+ buildWebMcpErrorResult("Aborted by cancel()"),
1876
+ r.startedAt,
1877
+ );
1878
+ }
1743
1879
  }
1744
1880
  } finally {
1745
1881
  for (const key of claimedKeys) {
@@ -1842,14 +1978,10 @@ export class AgentWidgetSession {
1842
1978
 
1843
1979
  // Mark resolved on the message so the UI's local-tool sheet (if any
1844
1980
  // generic one ever lands) does not show — this is a fully-automatic
1845
- // tool from the user's perspective, modulo the confirm bubble.
1846
- this.upsertMessage({
1847
- ...toolMessage,
1848
- agentMetadata: {
1849
- ...toolMessage.agentMetadata,
1850
- awaitingLocalTool: false,
1851
- },
1852
- });
1981
+ // tool from the user's perspective, modulo the confirm bubble. Keep the
1982
+ // tool bubble running until the browser-side promise resolves; the
1983
+ // initial step_await was only the server pause, not tool completion.
1984
+ const startedAt = this.markWebMcpToolRunning(toolMessage);
1853
1985
 
1854
1986
  // Per-resolve AbortController, NOT the shared `this.abortController`.
1855
1987
  // A single turn can produce multiple `webmcp:*` step_await messages —
@@ -1877,6 +2009,8 @@ export class AgentWidgetSession {
1877
2009
  signal,
1878
2010
  );
1879
2011
 
2012
+ let phase: "execute" | "resume" = "execute";
2013
+ let completedAt = startedAt;
1880
2014
  try {
1881
2015
  let resumeOutput: unknown;
1882
2016
  if (!execPromise) {
@@ -1891,6 +2025,7 @@ export class AgentWidgetSession {
1891
2025
  } else {
1892
2026
  resumeOutput = await execPromise;
1893
2027
  }
2028
+ completedAt = Date.now();
1894
2029
  // If cancel() fired during execute, the bridge returned an aborted
1895
2030
  // result — don't post it. The server's SSE has been torn down; a
1896
2031
  // /resume now would just produce an orphan dispatch on the server.
@@ -1898,6 +2033,11 @@ export class AgentWidgetSession {
1898
2033
  // the resolve set) so we don't clobber a sibling resolve or a live
1899
2034
  // dispatch's controller here.
1900
2035
  if (signal.aborted) {
2036
+ this.markWebMcpToolComplete(
2037
+ toolMessage,
2038
+ buildWebMcpErrorResult("Aborted by cancel()"),
2039
+ startedAt,
2040
+ );
1901
2041
  return;
1902
2042
  }
1903
2043
  // Mark resolved as soon as the HTTP /resume returns OK — not after the
@@ -1913,9 +2053,16 @@ export class AgentWidgetSession {
1913
2053
  // call (only same-tool PARALLEL calls could collide on the name).
1914
2054
  const resumeKey =
1915
2055
  toolMessage.agentMetadata?.webMcpToolCallId ?? wireToolName;
2056
+ phase = "resume";
1916
2057
  await this.resumeWithToolOutput(executionId, resumeKey, resumeOutput, {
1917
2058
  onHttpOk: () => {
1918
2059
  this.webMcpResolvedKeys.add(dedupeKey);
2060
+ this.markWebMcpToolComplete(
2061
+ toolMessage,
2062
+ resumeOutput,
2063
+ startedAt,
2064
+ completedAt,
2065
+ );
1919
2066
  },
1920
2067
  signal,
1921
2068
  });
@@ -1928,6 +2075,17 @@ export class AgentWidgetSession {
1928
2075
  // Streaming/teardown handled by the shared `finally` (gated on the
1929
2076
  // resolve set) — do NOT null the shared `this.abortController` here; it
1930
2077
  // may belong to a live dispatch or sibling resolve, not to us.
2078
+ if (phase === "execute" || isAbortError || signal.aborted) {
2079
+ this.markWebMcpToolComplete(
2080
+ toolMessage,
2081
+ buildWebMcpErrorResult(
2082
+ isAbortError || signal.aborted
2083
+ ? "Aborted by cancel()"
2084
+ : getWebMcpErrorMessage(error),
2085
+ ),
2086
+ startedAt,
2087
+ );
2088
+ }
1931
2089
  if (!isAbortError) {
1932
2090
  // The bridge normalizes tool errors into result objects, so reaching
1933
2091
  // here means a network failure during `/resume` itself, OR a stream
@@ -2504,9 +2662,11 @@ export class AgentWidgetSession {
2504
2662
  // WebMCP equivalent: once a `webmcp:*` tool has started resolving
2505
2663
  // (inflight) or resolved, a duplicate `step_await` re-emit must not
2506
2664
  // flip `awaitingLocalTool` back to true and resurrect the "waiting on
2507
- // local tool" UI. resolveWebMcpToolCall's dedupe path returns without
2508
- // re-touching the message, so correct the merge here (also avoids a
2509
- // one-frame flash before that microtask runs).
2665
+ // local tool" UI. It also must not overwrite an existing running or
2666
+ // completed toolCall with the fresh running skeleton emitted by client.ts
2667
+ // for every step_await. resolveWebMcpToolCall's dedupe path returns
2668
+ // without re-touching the message, so correct the merge here (also avoids
2669
+ // a one-frame flash before that microtask runs).
2510
2670
  const reTcName = withSequence.toolCall?.name;
2511
2671
  const reExecId = withSequence.agentMetadata?.executionId;
2512
2672
  const reTcId = withSequence.toolCall?.id;
@@ -2518,14 +2678,26 @@ export class AgentWidgetSession {
2518
2678
  withSequence.agentMetadata?.awaitingLocalTool === true
2519
2679
  ) {
2520
2680
  const reKey = `${reExecId}:${reTcId}`;
2521
- if (
2522
- this.webMcpInflightKeys.has(reKey) ||
2523
- this.webMcpResolvedKeys.has(reKey)
2524
- ) {
2681
+ const isInflight = this.webMcpInflightKeys.has(reKey);
2682
+ const isResolved = this.webMcpResolvedKeys.has(reKey);
2683
+ const existingToolName = existing.toolCall?.name;
2684
+ const hasCompletedTool =
2685
+ existing.agentMetadata?.executionId === reExecId &&
2686
+ existing.toolCall?.id === reTcId &&
2687
+ existingToolName !== undefined &&
2688
+ isWebMcpToolName(existingToolName) &&
2689
+ existing.toolCall?.status === "complete";
2690
+ if (isInflight || isResolved || hasCompletedTool) {
2525
2691
  merged.agentMetadata = {
2526
2692
  ...(merged.agentMetadata ?? {}),
2527
2693
  awaitingLocalTool: false,
2528
2694
  };
2695
+ // Preserve the in-flight/completed tool state. For in-flight calls,
2696
+ // this keeps the original `startedAt`; for completed calls, it keeps
2697
+ // the measured duration/result even when the call completed with a
2698
+ // local/browser error and therefore was not promoted to resolved.
2699
+ merged.toolCall = existing.toolCall;
2700
+ merged.streaming = existing.streaming;
2529
2701
  }
2530
2702
  }
2531
2703
  return merged;