@runtypelabs/persona 4.6.1 → 4.8.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 (120) hide show
  1. package/dist/animations/glyph-cycle.cjs +2 -2
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/glyph-cycle.js +2 -2
  5. package/dist/animations/{types-CSmiKRVa.d.cts → types-BsZtXPKK.d.cts} +43 -3
  6. package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -3
  7. package/dist/animations/wipe.cjs +2 -2
  8. package/dist/animations/wipe.d.cts +1 -1
  9. package/dist/animations/wipe.d.ts +1 -1
  10. package/dist/chunk-5EIIHQLQ.js +1 -0
  11. package/dist/codegen.cjs +12 -12
  12. package/dist/codegen.js +14 -14
  13. package/dist/index.cjs +91 -68
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +918 -174
  16. package/dist/index.d.ts +918 -174
  17. package/dist/index.global.js +81 -57
  18. package/dist/index.global.js.map +1 -1
  19. package/dist/index.js +89 -66
  20. package/dist/index.js.map +1 -1
  21. package/dist/install.global.js +1 -1
  22. package/dist/install.global.js.map +1 -1
  23. package/dist/launcher.global.js +3 -2
  24. package/dist/launcher.global.js.map +1 -1
  25. package/dist/markdown-parsers.js +24 -24
  26. package/dist/plugin-kit.cjs +1 -1
  27. package/dist/plugin-kit.js +1 -1
  28. package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
  29. package/dist/runtype-tts.js +1 -1
  30. package/dist/session-reconnect-JKIJBHS5.js +1 -0
  31. package/dist/smart-dom-reader.cjs +17 -17
  32. package/dist/smart-dom-reader.d.cts +753 -15
  33. package/dist/smart-dom-reader.d.ts +753 -15
  34. package/dist/smart-dom-reader.js +17 -17
  35. package/dist/testing.cjs +3 -3
  36. package/dist/testing.js +3 -3
  37. package/dist/theme-editor-preview.cjs +81 -58
  38. package/dist/theme-editor-preview.d.cts +761 -15
  39. package/dist/theme-editor-preview.d.ts +761 -15
  40. package/dist/theme-editor-preview.js +81 -58
  41. package/dist/theme-editor.cjs +6 -6
  42. package/dist/theme-editor.d.cts +753 -15
  43. package/dist/theme-editor.d.ts +753 -15
  44. package/dist/theme-editor.js +10 -10
  45. package/dist/theme-reference.cjs +1 -1
  46. package/dist/theme-reference.d.cts +74 -0
  47. package/dist/theme-reference.d.ts +74 -0
  48. package/dist/theme-reference.js +1 -1
  49. package/dist/voice-worklet-player.cjs +2 -2
  50. package/dist/voice-worklet-player.js +2 -2
  51. package/dist/webmcp-polyfill.js +2 -2
  52. package/dist/widget.css +1 -1
  53. package/package.json +2 -3
  54. package/src/artifacts-session.test.ts +178 -0
  55. package/src/client.test.ts +500 -1
  56. package/src/client.ts +285 -93
  57. package/src/components/artifact-card.test.ts +333 -0
  58. package/src/components/artifact-card.ts +75 -28
  59. package/src/components/artifact-inline.test.ts +1328 -0
  60. package/src/components/artifact-inline.ts +920 -0
  61. package/src/components/artifact-pane.test.ts +1042 -0
  62. package/src/components/artifact-pane.ts +440 -131
  63. package/src/components/artifact-preview.test.ts +1155 -0
  64. package/src/components/artifact-preview.ts +994 -0
  65. package/src/components/pill-composer-builder.test.ts +6 -2
  66. package/src/components/pill-composer-builder.ts +6 -6
  67. package/src/components/reasoning-bubble.ts +1 -13
  68. package/src/components/registry.ts +38 -3
  69. package/src/components/tool-bubble.ts +1 -13
  70. package/src/defaults.ts +1 -0
  71. package/src/generated/runtype-openapi-contract.ts +55 -3
  72. package/src/index-core.ts +20 -1
  73. package/src/index.ts +8 -0
  74. package/src/markdown-parsers-loader.test.ts +158 -0
  75. package/src/markdown-parsers-loader.ts +74 -9
  76. package/src/runtime/host-layout.test.ts +163 -0
  77. package/src/runtime/host-layout.ts +110 -7
  78. package/src/runtime/init.ts +18 -61
  79. package/src/runtime/persist-state.test.ts +118 -0
  80. package/src/session.ts +76 -22
  81. package/src/styles/widget.css +773 -26
  82. package/src/theme-editor/preview.ts +2 -0
  83. package/src/theme-editor/sections.test.ts +26 -1
  84. package/src/theme-editor/sections.ts +10 -2
  85. package/src/theme-reference.ts +2 -2
  86. package/src/tool-call-display-defaults.test.ts +1 -0
  87. package/src/types/theme.ts +77 -0
  88. package/src/types.ts +516 -17
  89. package/src/ui.artifact-pane-gating.test.ts +636 -0
  90. package/src/ui.component-directive.test.ts +104 -0
  91. package/src/ui.composer-bar.test.ts +60 -2
  92. package/src/ui.detached-panel.test.ts +1049 -0
  93. package/src/ui.scroll-additive.test.ts +1 -1
  94. package/src/ui.streaming-coalescing.test.ts +312 -0
  95. package/src/ui.tool-display.test.ts +51 -0
  96. package/src/ui.ts +933 -214
  97. package/src/utils/artifact-custom-actions.ts +128 -0
  98. package/src/utils/artifact-display.test.ts +42 -0
  99. package/src/utils/artifact-display.ts +84 -0
  100. package/src/utils/artifact-file.test.ts +116 -0
  101. package/src/utils/artifact-file.ts +117 -0
  102. package/src/utils/artifact-gate.test.ts +112 -5
  103. package/src/utils/artifact-gate.ts +39 -14
  104. package/src/utils/artifact-loading-status.ts +55 -0
  105. package/src/utils/artifact-status-label.ts +190 -0
  106. package/src/utils/buttons.ts +7 -1
  107. package/src/utils/code-highlight.test.ts +186 -0
  108. package/src/utils/code-highlight.ts +400 -0
  109. package/src/utils/icons.ts +2 -0
  110. package/src/utils/roving-tablist.test.ts +152 -0
  111. package/src/utils/roving-tablist.ts +111 -0
  112. package/src/utils/spinner.ts +45 -0
  113. package/src/utils/theme.test.ts +48 -0
  114. package/src/utils/theme.ts +7 -0
  115. package/src/utils/tokens.ts +91 -0
  116. package/src/utils/tool-loading-animation.test.ts +32 -0
  117. package/src/utils/tool-loading-animation.ts +24 -0
  118. package/dist/chunk-DFBSCFYN.js +0 -1
  119. package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
  120. package/dist/session-reconnect-U77QFUR7.js +0 -1
package/src/client.ts CHANGED
@@ -17,13 +17,19 @@ import {
17
17
  ClientSession,
18
18
  ClientInitResponse,
19
19
  ClientChatRequest,
20
+ ClientToolDefinition,
20
21
  ClientFeedbackRequest,
21
22
  ClientFeedbackType,
22
23
  PersonaArtifactKind,
24
+ PersonaArtifactFileMeta,
23
25
  ContentPart,
24
26
  WebMcpConfirmHandler
25
27
  } from "./types";
26
28
  import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
29
+ import {
30
+ buildArtifactRefRawContent,
31
+ resolveArtifactDisplayMode
32
+ } from "./utils/artifact-display";
27
33
  import { resolveTarget } from "./utils/target";
28
34
  import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
29
35
  import {
@@ -180,6 +186,14 @@ export class AgentWidgetClient {
180
186
  // (silent re-init / expiry) forces a fresh full send.
181
187
  private lastSentClientToolsFingerprint: string | null = null;
182
188
  private clientToolsFingerprintSessionId: string | null = null;
189
+ // Session under which a non-empty clientTools[] was last committed and not
190
+ // yet confirmed cleared server-side. Distinct from the fingerprint above:
191
+ // an empty-tool chat turn commits a null fingerprint, but OMITTING the
192
+ // fields on chat doesn't clear the tools persisted for a still-paused
193
+ // execution — so a later resume with an empty registry must still send the
194
+ // explicit `clientTools: []` replace. Reset only by an explicit [] replace,
195
+ // a session change, or a conversation reset.
196
+ private sentNonEmptyClientToolsSessionId: string | null = null;
183
197
 
184
198
  // WebMCP: page-discovered tool consumption (see ./webmcp-bridge).
185
199
  // Constructed lazily: null when `config.webmcp?.enabled !== true`.
@@ -438,6 +452,7 @@ export class AgentWidgetClient {
438
452
  public resetClientToolsFingerprint(): void {
439
453
  this.lastSentClientToolsFingerprint = null;
440
454
  this.clientToolsFingerprintSessionId = null;
455
+ this.sentNonEmptyClientToolsSessionId = null;
441
456
  }
442
457
 
443
458
  /**
@@ -614,6 +629,7 @@ export class AgentWidgetClient {
614
629
  * Send a message - handles both proxy and client token modes
615
630
  */
616
631
  public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
632
+ options.signal?.throwIfAborted();
617
633
  if (this.isClientTokenMode()) {
618
634
  return this.dispatchClientToken(options, onEvent);
619
635
  }
@@ -627,11 +643,6 @@ export class AgentWidgetClient {
627
643
  * Client token mode dispatch
628
644
  */
629
645
  private async dispatchClientToken(options: DispatchOptions, onEvent: SSEHandler) {
630
- const controller = new AbortController();
631
- if (options.signal) {
632
- options.signal.addEventListener("abort", () => controller.abort());
633
- }
634
-
635
646
  onEvent({ type: "status", status: "connecting" });
636
647
 
637
648
  try {
@@ -678,71 +689,32 @@ export class AgentWidgetClient {
678
689
  };
679
690
 
680
691
  // Diff-only / send-once WebMCP tool dispatch. `buildPayload()` already
681
- // snapshotted the full set; decide whether to ship it again or just its
682
- // fingerprint. First turn of a session, or a changed set, sends the full
683
- // array; an unchanged set sends only the fingerprint and the server
684
- // reuses its stored copy. The cache is committed only after a successful
685
- // stream start (below), so a 409/failure leaves it untouched.
686
- const fullClientTools = basePayload.clientTools;
687
- const hasClientTools = !!(fullClientTools && fullClientTools.length > 0);
688
- const clientToolsFingerprint = hasClientTools
689
- ? computeClientToolsFingerprint(fullClientTools!)
690
- : undefined;
691
- const sameSession = this.clientToolsFingerprintSessionId === session.sessionId;
692
- const unchanged =
693
- hasClientTools && sameSession && this.lastSentClientToolsFingerprint === clientToolsFingerprint;
694
-
695
- // `forceFull` flips to true after a 409 cache-miss so the single retry
696
- // resends the full list. Capture any error body read inside the loop so
697
- // the `!response.ok` handler below doesn't re-consume the stream.
698
- let forceFull = false;
699
- let errorData: { error?: string; hint?: string } | null = null;
700
- let response: Response;
701
- for (let attempt = 0; ; attempt++) {
702
- const sendFull = hasClientTools && (forceFull || !unchanged);
703
- const chatRequest: ClientChatRequest = {
704
- ...baseChatRequest,
705
- ...(sendFull && fullClientTools ? { clientTools: fullClientTools } : {}),
706
- ...(clientToolsFingerprint ? { clientToolsFingerprint } : {}),
707
- };
708
-
709
- if (this.debug) {
710
- // eslint-disable-next-line no-console
711
- console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
712
- }
692
+ // snapshotted the full set; `sendWithClientToolsDiff` decides whether to
693
+ // ship it again or just its fingerprint (retrying once on a 409 registry
694
+ // miss). The cache is committed only after a successful stream start
695
+ // (below), so a 409/failure leaves it untouched.
696
+ const { response, commit: commitClientToolsFingerprint } =
697
+ await this.sendWithClientToolsDiff(session.sessionId, basePayload.clientTools, (toolFields) => {
698
+ const chatRequest: ClientChatRequest = { ...baseChatRequest, ...toolFields };
699
+
700
+ if (this.debug) {
701
+ // eslint-disable-next-line no-console
702
+ console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
703
+ }
713
704
 
714
- response = await fetch(this.getClientApiUrl('chat'), {
715
- method: 'POST',
716
- headers: {
717
- 'Content-Type': 'application/json',
718
- 'X-Persona-Version': VERSION,
719
- },
720
- body: JSON.stringify(chatRequest),
721
- signal: controller.signal,
705
+ return fetch(this.getClientApiUrl('chat'), {
706
+ method: 'POST',
707
+ headers: {
708
+ 'Content-Type': 'application/json',
709
+ 'X-Persona-Version': VERSION,
710
+ },
711
+ body: JSON.stringify(chatRequest),
712
+ signal: options.signal,
713
+ });
722
714
  });
723
715
 
724
- // Diff-only cache miss: the server has no stored tool set matching our
725
- // fingerprint. Retry exactly once with the full list. A second miss
726
- // falls through to the normal error handling below (no infinite loop).
727
- if (response.status === 409 && attempt === 0 && hasClientTools) {
728
- const body = (await response.json().catch(() => null)) as
729
- | { error?: string; hint?: string }
730
- | null;
731
- if (body?.error === 'client_tools_resend_required') {
732
- forceFull = true;
733
- // Invalidate so future turns also resend until a clean success
734
- // commits a fresh fingerprint.
735
- this.lastSentClientToolsFingerprint = null;
736
- continue;
737
- }
738
- // Some other 409: keep the parsed body for the handler below.
739
- errorData = body ?? { error: 'Chat request failed' };
740
- }
741
- break;
742
- }
743
-
744
716
  if (!response.ok) {
745
- const data = errorData ?? (await response.json().catch(() => ({ error: 'Chat request failed' })));
717
+ const data = await response.json().catch(() => ({ error: 'Chat request failed' }));
746
718
 
747
719
  if (response.status === 401) {
748
720
  // Session expired
@@ -773,8 +745,7 @@ export class AgentWidgetClient {
773
745
  // Stream is good: the server now holds this tool set under this
774
746
  // fingerprint for the session. Commit the cache so unchanged follow-up
775
747
  // turns can send fingerprint-only.
776
- this.lastSentClientToolsFingerprint = clientToolsFingerprint ?? null;
777
- this.clientToolsFingerprintSessionId = session.sessionId;
748
+ commitClientToolsFingerprint();
778
749
 
779
750
  onEvent({ type: "status", status: "connected" });
780
751
 
@@ -798,11 +769,6 @@ export class AgentWidgetClient {
798
769
  * Proxy mode dispatch (original implementation)
799
770
  */
800
771
  private async dispatchProxy(options: DispatchOptions, onEvent: SSEHandler) {
801
- const controller = new AbortController();
802
- if (options.signal) {
803
- options.signal.addEventListener("abort", () => controller.abort());
804
- }
805
-
806
772
  onEvent({ type: "status", status: "connecting" });
807
773
 
808
774
  const payload = await this.buildPayload(options.messages);
@@ -836,7 +802,7 @@ export class AgentWidgetClient {
836
802
  method: "POST",
837
803
  headers,
838
804
  body: JSON.stringify(payload),
839
- signal: controller.signal
805
+ signal: options.signal
840
806
  },
841
807
  payload
842
808
  );
@@ -850,7 +816,7 @@ export class AgentWidgetClient {
850
816
  method: "POST",
851
817
  headers,
852
818
  body: JSON.stringify(payload),
853
- signal: controller.signal
819
+ signal: options.signal
854
820
  });
855
821
  }
856
822
 
@@ -874,11 +840,6 @@ export class AgentWidgetClient {
874
840
  * Agent mode dispatch
875
841
  */
876
842
  private async dispatchAgent(options: DispatchOptions, onEvent: SSEHandler) {
877
- const controller = new AbortController();
878
- if (options.signal) {
879
- options.signal.addEventListener("abort", () => controller.abort());
880
- }
881
-
882
843
  onEvent({ type: "status", status: "connecting" });
883
844
 
884
845
  const payload = await this.buildAgentPayload(options.messages);
@@ -912,7 +873,7 @@ export class AgentWidgetClient {
912
873
  method: "POST",
913
874
  headers,
914
875
  body: JSON.stringify(payload),
915
- signal: controller.signal
876
+ signal: options.signal
916
877
  },
917
878
  payload as unknown as AgentWidgetRequestPayload
918
879
  );
@@ -926,7 +887,7 @@ export class AgentWidgetClient {
926
887
  method: "POST",
927
888
  headers,
928
889
  body: JSON.stringify(payload),
929
- signal: controller.signal
890
+ signal: options.signal
930
891
  });
931
892
  }
932
893
 
@@ -1007,8 +968,13 @@ export class AgentWidgetClient {
1007
968
  * - **client-token mode**: POST `${apiBase}/v1/client/resume` (the
1008
969
  * session-authenticated sibling of `/v1/client/chat`; runtypelabs/core#3889),
1009
970
  * with the active `sessionId` in the body and no Bearer key: a browser
1010
- * client-token page holds no secret. `clientTools` are already persisted
1011
- * server-side from the dispatch turn, so only `toolOutputs` is re-sent.
971
+ * client-token page holds no secret. The page's tool registry is
972
+ * re-snapshotted and sent alongside `toolOutputs` via the same diff-only
973
+ * `clientTools` / `clientToolsFingerprint` protocol as `/v1/client/chat`
974
+ * (runtypelabs/core#5361), so tools registered by a mid-run page
975
+ * navigation replace the run's dispatch-time set and become callable on
976
+ * the next model turn. Old servers strip the unknown fields and keep the
977
+ * frozen-at-dispatch behavior.
1012
978
  * - **dispatch / proxy mode**: POST `${apiUrl}/resume`: Runtype mounts
1013
979
  * resume as a child of `/v1/dispatch`, so the URL is `${apiUrl}/resume`,
1014
980
  * and proxies follow the same shape (`/api/chat/dispatch/resume`).
@@ -1020,6 +986,113 @@ export class AgentWidgetClient {
1020
986
  * @param toolOutputs - Map keyed by per-call `toolCallId` (core#3878),
1021
987
  * falling back to tool name for legacy servers → the tool's result value.
1022
988
  */
989
+ /**
990
+ * Diff-only / send-once WebMCP clientTools transport, shared by the
991
+ * client-token chat (`/v1/client/chat`) and resume (`/v1/client/resume`)
992
+ * paths — both routes speak the same protocol (runtypelabs/core#5361).
993
+ *
994
+ * Decides, against the shared fingerprint cache, whether this request ships
995
+ * the full `clientTools[]` + fingerprint (first send under this session, or
996
+ * a changed set) or the fingerprint alone (unchanged set; the server reuses
997
+ * its stored copy). Runs `doFetch` with the chosen fields and retries
998
+ * EXACTLY once with the full array on a
999
+ * `409 { error: 'client_tools_resend_required' }` registry miss — the retry
1000
+ * is 409-*triggered*, never 409-*expected*, so servers predating the
1001
+ * protocol (which strip the unknown fields and never 409) work unchanged.
1002
+ * The 409 body is probed on a `clone()` so the original response body stays
1003
+ * readable by the caller's error handling.
1004
+ *
1005
+ * The cache is NOT committed here: callers invoke the returned `commit()`
1006
+ * only after the server has accepted the request (response OK / stream
1007
+ * started), so a failed request can never record a fingerprint the server
1008
+ * never stored. A resend-required miss invalidates the cached fingerprint
1009
+ * immediately so later turns keep resending in full until a clean success
1010
+ * commits a fresh one.
1011
+ *
1012
+ * `emptyMeansReplace` (resume only): when the live snapshot is empty but a
1013
+ * non-empty set was committed under this session and never explicitly
1014
+ * cleared (the paused tool navigated to a page with no tool registry), ship
1015
+ * an explicit `clientTools: []` so the server REPLACES the persisted
1016
+ * dispatch-time set with nothing and clears its stored registry. Chat keeps
1017
+ * its omit-when-empty behavior: on `/chat`, absent fields already mean "no
1018
+ * tools this turn", whereas on `/resume` absence means "keep the frozen
1019
+ * dispatch-time set".
1020
+ */
1021
+ private async sendWithClientToolsDiff(
1022
+ sessionId: string,
1023
+ fullClientTools: ClientToolDefinition[] | undefined,
1024
+ doFetch: (
1025
+ toolFields: Pick<ClientChatRequest, 'clientTools' | 'clientToolsFingerprint'>
1026
+ ) => Promise<Response>,
1027
+ opts?: { emptyMeansReplace?: boolean }
1028
+ ): Promise<{ response: Response; commit: () => void }> {
1029
+ const hasClientTools = !!(fullClientTools && fullClientTools.length > 0);
1030
+ const clientToolsFingerprint = hasClientTools
1031
+ ? computeClientToolsFingerprint(fullClientTools!)
1032
+ : undefined;
1033
+ const sameSession = this.clientToolsFingerprintSessionId === sessionId;
1034
+ const unchanged =
1035
+ hasClientTools && sameSession && this.lastSentClientToolsFingerprint === clientToolsFingerprint;
1036
+ // Keyed on `sentNonEmptyClientToolsSessionId`, NOT the fingerprint: an
1037
+ // interleaved empty-tool chat turn commits a null fingerprint without
1038
+ // clearing the tools persisted for a still-paused execution, so the
1039
+ // fingerprint alone would lose the pending clear. The dedicated flag
1040
+ // survives omitted-empty commits and is reset only once an explicit []
1041
+ // replace is confirmed.
1042
+ const sendEmptyReplace =
1043
+ !hasClientTools &&
1044
+ opts?.emptyMeansReplace === true &&
1045
+ this.sentNonEmptyClientToolsSessionId === sessionId;
1046
+
1047
+ // `forceFull` flips to true after a 409 cache-miss so the single retry
1048
+ // resends the full list.
1049
+ let forceFull = false;
1050
+ let response: Response;
1051
+ for (let attempt = 0; ; attempt++) {
1052
+ const sendFull = hasClientTools && (forceFull || !unchanged);
1053
+ response = await doFetch({
1054
+ ...(sendFull && fullClientTools ? { clientTools: fullClientTools } : {}),
1055
+ ...(sendEmptyReplace ? { clientTools: [] } : {}),
1056
+ ...(clientToolsFingerprint ? { clientToolsFingerprint } : {}),
1057
+ });
1058
+
1059
+ // Diff-only cache miss: the server has no stored tool set matching our
1060
+ // fingerprint. Retry exactly once with the full list. A second miss
1061
+ // falls through to the caller's normal error handling (no infinite loop).
1062
+ if (response.status === 409 && attempt === 0 && hasClientTools) {
1063
+ const body = (await response
1064
+ .clone()
1065
+ .json()
1066
+ .catch(() => null)) as { error?: string } | null;
1067
+ if (body?.error === 'client_tools_resend_required') {
1068
+ forceFull = true;
1069
+ // Invalidate so future turns also resend until a clean success
1070
+ // commits a fresh fingerprint.
1071
+ this.lastSentClientToolsFingerprint = null;
1072
+ continue;
1073
+ }
1074
+ }
1075
+ break;
1076
+ }
1077
+
1078
+ return {
1079
+ response,
1080
+ commit: () => {
1081
+ this.lastSentClientToolsFingerprint = clientToolsFingerprint ?? null;
1082
+ this.clientToolsFingerprintSessionId = sessionId;
1083
+ if (hasClientTools) {
1084
+ this.sentNonEmptyClientToolsSessionId = sessionId;
1085
+ } else if (sendEmptyReplace) {
1086
+ // The explicit [] replaced the persisted set server-side; the
1087
+ // pending clear is done.
1088
+ this.sentNonEmptyClientToolsSessionId = null;
1089
+ }
1090
+ // Omitted-empty commits (chat with zero tools) leave the flag set:
1091
+ // they don't touch tools persisted for a paused execution.
1092
+ },
1093
+ };
1094
+ }
1095
+
1023
1096
  public async resumeFlow(
1024
1097
  executionId: string,
1025
1098
  toolOutputs: Record<string, unknown>,
@@ -1060,6 +1133,47 @@ export class AgentWidgetClient {
1060
1133
  body.sessionId = resumeSessionId;
1061
1134
  }
1062
1135
 
1136
+ if (isClientToken && resumeSessionId) {
1137
+ // Mid-run WebMCP tool refresh (runtypelabs/core#5361): the paused tool
1138
+ // may have navigated the page, so the dispatch-time snapshot the server
1139
+ // persisted can be stale. Re-snapshot the registry — the same built-in +
1140
+ // bridge composition as the payload builders, so fingerprints computed
1141
+ // here and on chat turns describe the same tool space — and ship it via
1142
+ // the shared diff-only protocol. `emptyMeansReplace` sends an explicit
1143
+ // `clientTools: []` when the registry vanished after a non-empty send,
1144
+ // so the server replaces the persisted set instead of keeping it frozen.
1145
+ const fullClientTools = [
1146
+ ...builtInClientToolsForDispatch(this.config),
1147
+ ...((await this.webMcpBridge?.snapshotForDispatch()) ?? []),
1148
+ ];
1149
+ const { response, commit } = await this.sendWithClientToolsDiff(
1150
+ resumeSessionId,
1151
+ fullClientTools,
1152
+ (toolFields) => {
1153
+ const resumeRequest = { ...body, ...toolFields };
1154
+ if (this.debug) {
1155
+ // eslint-disable-next-line no-console
1156
+ console.debug("[AgentWidgetClient] client token resume", resumeRequest);
1157
+ }
1158
+ return fetch(url, {
1159
+ method: 'POST',
1160
+ headers,
1161
+ body: JSON.stringify(resumeRequest),
1162
+ signal: options?.signal,
1163
+ });
1164
+ },
1165
+ { emptyMeansReplace: true }
1166
+ );
1167
+ // The server stores the refreshed registry before running the
1168
+ // continuation pipeline, so an OK response means it holds this set under
1169
+ // this fingerprint. Mirror chat's commit-on-success discipline: a failed
1170
+ // resume must not record a fingerprint the server never stored.
1171
+ if (response.ok) {
1172
+ commit();
1173
+ }
1174
+ return response;
1175
+ }
1176
+
1063
1177
  return fetch(url, {
1064
1178
  method: 'POST',
1065
1179
  headers,
@@ -1592,12 +1706,25 @@ export class AgentWidgetClient {
1592
1706
 
1593
1707
  // Track tool call IDs for artifact emit tools so we can suppress their UI
1594
1708
  const artifactToolCallIds = new Set<string>();
1595
- // Track artifact reference card messages so we can update them on artifact_complete
1709
+ // Track artifact block messages (reference card or inline block) so we can
1710
+ // update them on artifact_complete
1596
1711
  const artifactCardMessages = new Map<string, AgentWidgetMessage>();
1597
1712
  // Track artifact IDs that already have a reference card (from auto-creation or transcript_insert)
1598
1713
  const artifactIdsWithCards = new Set<string>();
1599
- // Accumulate artifact markdown content for embedding in card props on complete
1600
- const artifactContent = new Map<string, { markdown: string; title?: string }>();
1714
+ // Accumulate artifact markdown content (and component props) for embedding
1715
+ // in block props on complete. `props` accumulates across `artifact_update`
1716
+ // events so an inline component block hydrates with its real props after a
1717
+ // refresh (the session artifact registry, which also tracks them live, is
1718
+ // not persisted).
1719
+ const artifactContent = new Map<
1720
+ string,
1721
+ {
1722
+ markdown: string;
1723
+ title?: string;
1724
+ file?: PersonaArtifactFileMeta;
1725
+ props?: Record<string, unknown>;
1726
+ }
1727
+ >();
1601
1728
  const isArtifactEmitToolName = (name: string | undefined): boolean => {
1602
1729
  if (!name) return false;
1603
1730
  const normalized = name.replace(/_+/g, "_").replace(/^_|_$/g, "");
@@ -2996,17 +3123,57 @@ export class AgentWidgetClient {
2996
3123
  const at = payload.artifactType as PersonaArtifactKind;
2997
3124
  const artId = String(payload.id);
2998
3125
  const artTitle = typeof payload.title === "string" ? payload.title : undefined;
3126
+ // Additive `file` metadata: validate shape (object with string path +
3127
+ // string mimeType; optional string language). Drop silently if malformed
3128
+ // so old/new backends and unexpected payloads never break the stream.
3129
+ const rawFile = payload.file;
3130
+ let artFile: PersonaArtifactFileMeta | undefined;
3131
+ if (
3132
+ rawFile &&
3133
+ typeof rawFile === "object" &&
3134
+ !Array.isArray(rawFile) &&
3135
+ typeof rawFile.path === "string" &&
3136
+ typeof rawFile.mimeType === "string"
3137
+ ) {
3138
+ artFile = {
3139
+ path: rawFile.path,
3140
+ mimeType: rawFile.mimeType,
3141
+ ...(typeof rawFile.language === "string" ? { language: rawFile.language } : {}),
3142
+ };
3143
+ }
2999
3144
  onEvent({
3000
3145
  type: "artifact_start",
3001
3146
  id: artId,
3002
3147
  artifactType: at,
3003
3148
  title: artTitle,
3004
- component: typeof payload.component === "string" ? payload.component : undefined
3149
+ component: typeof payload.component === "string" ? payload.component : undefined,
3150
+ ...(artFile ? { file: artFile } : {})
3005
3151
  });
3006
- artifactContent.set(artId, { markdown: "", title: artTitle });
3007
- // Insert inline artifact reference card (skip if already present from transcript_insert)
3152
+ // Seed component props from artifact_start when the payload
3153
+ // carries them; artifact_update events accumulate the rest below.
3154
+ const startProps =
3155
+ payload.props && typeof payload.props === "object" && !Array.isArray(payload.props)
3156
+ ? { ...(payload.props as Record<string, unknown>) }
3157
+ : undefined;
3158
+ artifactContent.set(artId, {
3159
+ markdown: "",
3160
+ title: artTitle,
3161
+ file: artFile,
3162
+ ...(startProps ? { props: startProps } : {})
3163
+ });
3164
+ // Insert the in-thread artifact block (skip if already present from
3165
+ // transcript_insert). The resolved display mode picks the component:
3166
+ // "card"/"panel" inject the reference card; "inline" injects the
3167
+ // inline preview block. Both share the rawContent JSON-component
3168
+ // shape so transcript persistence and hydration work unchanged.
3008
3169
  if (!artifactIdsWithCards.has(artId)) {
3009
3170
  artifactIdsWithCards.add(artId);
3171
+ const displayMode = resolveArtifactDisplayMode(
3172
+ this.config.features?.artifacts,
3173
+ at
3174
+ );
3175
+ const artComponent =
3176
+ typeof payload.component === "string" ? payload.component : undefined;
3010
3177
  const cardMsg: AgentWidgetMessage = {
3011
3178
  id: `artifact-ref-${artId}`,
3012
3179
  role: "assistant",
@@ -3014,9 +3181,13 @@ export class AgentWidgetClient {
3014
3181
  createdAt: new Date().toISOString(),
3015
3182
  streaming: true,
3016
3183
  sequence: nextSequence(),
3017
- rawContent: JSON.stringify({
3018
- component: "PersonaArtifactCard",
3019
- props: { artifactId: artId, title: artTitle, artifactType: at, status: "streaming" },
3184
+ rawContent: buildArtifactRefRawContent(displayMode, {
3185
+ artifactId: artId,
3186
+ title: artTitle,
3187
+ artifactType: at,
3188
+ status: "streaming",
3189
+ ...(artFile ? { file: artFile } : {}),
3190
+ ...(artComponent ? { component: artComponent } : {}),
3020
3191
  }),
3021
3192
  };
3022
3193
  artifactCardMessages.set(artId, cardMsg);
@@ -3043,6 +3214,13 @@ export class AgentWidgetClient {
3043
3214
  props,
3044
3215
  component: typeof payload.component === "string" ? payload.component : undefined
3045
3216
  });
3217
+ // Accumulate for hydration: embedded on artifact_complete so an
3218
+ // inline component block re-renders with its real props after a
3219
+ // refresh.
3220
+ const updateAcc = artifactContent.get(String(payload.id));
3221
+ if (updateAcc) {
3222
+ updateAcc.props = { ...(updateAcc.props ?? {}), ...props };
3223
+ }
3046
3224
  } else if (payloadType === "artifact_complete") {
3047
3225
  const artCompleteId = String(payload.id);
3048
3226
  onEvent({ type: "artifact_complete", id: artCompleteId });
@@ -3059,6 +3237,20 @@ export class AgentWidgetClient {
3059
3237
  if (acc?.markdown) {
3060
3238
  parsed.props.markdown = acc.markdown;
3061
3239
  }
3240
+ // Persist file metadata too so the download path unfences correctly after refresh.
3241
+ if (acc?.file) {
3242
+ parsed.props.file = acc.file;
3243
+ }
3244
+ // Embed accumulated component props so an inline component
3245
+ // block hydrates with its real props after a refresh. Only
3246
+ // the inline block reads them; the card never does.
3247
+ if (
3248
+ parsed.component === "PersonaArtifactInline" &&
3249
+ acc?.props &&
3250
+ Object.keys(acc.props).length > 0
3251
+ ) {
3252
+ parsed.props.componentProps = acc.props;
3253
+ }
3062
3254
  }
3063
3255
  refMsg.rawContent = JSON.stringify(parsed);
3064
3256
  } catch { /* ignore parse errors */ }