@agent-native/core 0.99.1 → 0.99.3

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 (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +42 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  5. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  6. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  7. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  8. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  9. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  10. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  11. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  12. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  13. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +12 -38
  16. package/corpus/core/src/audit/config.ts +0 -1
  17. package/corpus/core/src/chat-threads/store.ts +57 -41
  18. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  19. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  20. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  23. package/corpus/core/src/client/index.ts +7 -0
  24. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  25. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  26. package/corpus/core/src/client/use-db-sync.ts +3 -15
  27. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  28. package/corpus/core/src/demo/browser-state.ts +47 -0
  29. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  30. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  31. package/corpus/core/src/server/action-discovery.ts +0 -1
  32. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  33. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  34. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  35. package/corpus/templates/analytics/AGENTS.md +4 -4
  36. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  37. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  38. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  39. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  40. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
  45. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  48. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  49. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  51. package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
  52. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  53. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  54. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  55. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  56. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  57. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  58. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  59. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  60. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  61. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  64. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  65. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  66. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  67. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  68. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  69. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  70. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  71. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  72. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  73. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  74. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  75. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  76. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  77. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  78. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  79. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  80. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  81. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +13 -37
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/audit/config.d.ts.map +1 -1
  86. package/dist/audit/config.js +0 -1
  87. package/dist/audit/config.js.map +1 -1
  88. package/dist/chat-threads/store.d.ts.map +1 -1
  89. package/dist/chat-threads/store.js +47 -33
  90. package/dist/chat-threads/store.js.map +1 -1
  91. package/dist/client/AssistantChat.d.ts.map +1 -1
  92. package/dist/client/AssistantChat.js +1 -1
  93. package/dist/client/AssistantChat.js.map +1 -1
  94. package/dist/client/chat/message-components.d.ts.map +1 -1
  95. package/dist/client/chat/message-components.js +9 -3
  96. package/dist/client/chat/message-components.js.map +1 -1
  97. package/dist/client/chat/tool-call-display.d.ts +3 -1
  98. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  99. package/dist/client/chat/tool-call-display.js +10 -2
  100. package/dist/client/chat/tool-call-display.js.map +1 -1
  101. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  102. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  103. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  104. package/dist/client/extensions/ExtensionViewer.js +2 -59
  105. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  106. package/dist/client/index.d.ts +2 -0
  107. package/dist/client/index.d.ts.map +1 -1
  108. package/dist/client/index.js +2 -0
  109. package/dist/client/index.js.map +1 -1
  110. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  111. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  112. package/dist/client/settings/DemoModeSection.js +5 -52
  113. package/dist/client/settings/DemoModeSection.js.map +1 -1
  114. package/dist/client/settings/SettingsPanel.js +1 -1
  115. package/dist/client/settings/SettingsPanel.js.map +1 -1
  116. package/dist/client/use-db-sync.d.ts.map +1 -1
  117. package/dist/client/use-db-sync.js +3 -7
  118. package/dist/client/use-db-sync.js.map +1 -1
  119. package/dist/client/use-demo-mode-status.d.ts +3 -4
  120. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  121. package/dist/client/use-demo-mode-status.js +8 -23
  122. package/dist/client/use-demo-mode-status.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +2 -2
  126. package/dist/collab/struct-routes.d.ts +1 -1
  127. package/dist/demo/browser-state.d.ts +10 -0
  128. package/dist/demo/browser-state.d.ts.map +1 -0
  129. package/dist/demo/browser-state.js +49 -0
  130. package/dist/demo/browser-state.js.map +1 -0
  131. package/dist/demo/fetch-interceptor.d.ts +2 -7
  132. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  133. package/dist/demo/fetch-interceptor.js +12 -56
  134. package/dist/demo/fetch-interceptor.js.map +1 -1
  135. package/dist/deploy/workspace-deploy.js +2 -2
  136. package/dist/deploy/workspace-deploy.js.map +1 -1
  137. package/dist/notifications/routes.d.ts +2 -2
  138. package/dist/observability/routes.d.ts +5 -5
  139. package/dist/resources/handlers.d.ts +3 -3
  140. package/dist/secrets/routes.d.ts +9 -9
  141. package/dist/server/action-discovery.d.ts.map +1 -1
  142. package/dist/server/action-discovery.js +0 -1
  143. package/dist/server/action-discovery.js.map +1 -1
  144. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  145. package/dist/server/core-routes-plugin.js +0 -27
  146. package/dist/server/core-routes-plugin.js.map +1 -1
  147. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  148. package/docs/content/a2a-protocol.mdx +42 -0
  149. package/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  150. package/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  151. package/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  152. package/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  153. package/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  154. package/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  155. package/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  156. package/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  157. package/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  158. package/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  159. package/package.json +1 -1
  160. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  161. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  162. package/corpus/core/src/demo/config.ts +0 -57
  163. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  164. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  165. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  166. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  167. package/dist/demo/config.d.ts +0 -9
  168. package/dist/demo/config.d.ts.map +0 -1
  169. package/dist/demo/config.js +0 -58
  170. package/dist/demo/config.js.map +0 -1
@@ -11,8 +11,6 @@ import type { EventHandler as H3EventHandler } from "h3";
11
11
  import { isAgentActionStopError } from "../action.js";
12
12
  import { readAppState } from "../application-state/script-helpers.js";
13
13
  import { isReadOnlyShellCommand } from "../coding-tools/index.js";
14
- import { isDemoModeEnabled } from "../demo/config.js";
15
- import { redactDemoData, redactDemoString } from "../demo/redact.js";
16
14
  import { extensionIdFromPathname } from "../extensions/path.js";
17
15
  import { preUploadAttachments } from "../file-upload/pre-upload-attachments.js";
18
16
  import { isMcpActionResult } from "../mcp-client/app-result.js";
@@ -4456,48 +4454,24 @@ export async function runAgentLoop(opts: {
4456
4454
  isError = true;
4457
4455
  }
4458
4456
  mcpApp = mcpResult?.mcpApp;
4459
- // Demo mode: the agent must see the same anonymized data the UI shows, so
4460
- // it can't read out a real name/email on a live screen share. Redact
4461
- // the structured result (not the JSON string) so IDs/dates/URLs stay
4462
- // intact and follow-up tool calls still work. Gated — the expensive
4463
- // walk only runs when demo mode is on.
4464
- let redacted: unknown = rawForAgent;
4465
- const demoMode = await isDemoModeEnabled();
4466
- if (demoMode) {
4467
- mcpApp = undefined;
4468
- if (typeof rawForAgent === "string") {
4469
- try {
4470
- redacted = JSON.stringify(
4471
- redactDemoData(JSON.parse(rawForAgent)),
4472
- null,
4473
- 2,
4474
- );
4475
- } catch {
4476
- redacted = redactDemoString(rawForAgent);
4477
- }
4478
- } else {
4479
- redacted = redactDemoData(rawForAgent);
4480
- }
4481
- }
4457
+ // Demo mode is browser-local presentation state. The agent and MCP
4458
+ // layers always receive the real, access-scoped tool result.
4459
+ let resultForAgent: unknown = rawForAgent;
4482
4460
  // Vision images for the model: MCP tools return standard `image`
4483
4461
  // content parts; first-party actions opt in via the well-known
4484
4462
  // `_agentImages` result field (stripped from the JSON the model
4485
- // reads, even in demo mode). Demo mode drops the images themselves —
4486
- // text redaction can't scrub pixels, and a screenshot may expose
4487
- // original visual data. The images array
4463
+ // reads). The images array
4488
4464
  // never touches the ledger — only the compact text notes appended
4489
4465
  // below are persisted.
4490
4466
  let imageNotes: string[] = [];
4491
4467
  if (mcpResult) {
4492
- if (!demoMode) {
4493
- const mcpImages = extractMcpToolResultImages(mcpResult.raw);
4494
- if (mcpImages.length > 0) toolResultImages = mcpImages;
4495
- }
4468
+ const mcpImages = extractMcpToolResultImages(mcpResult.raw);
4469
+ if (mcpImages.length > 0) toolResultImages = mcpImages;
4496
4470
  } else {
4497
- const extracted = extractAgentImagesFromActionResult(redacted);
4498
- redacted = extracted.value;
4471
+ const extracted = extractAgentImagesFromActionResult(resultForAgent);
4472
+ resultForAgent = extracted.value;
4499
4473
  imageNotes = extracted.notes;
4500
- if (extracted.images.length > 0 && !demoMode) {
4474
+ if (extracted.images.length > 0) {
4501
4475
  toolResultImages = extracted.images;
4502
4476
  }
4503
4477
  }
@@ -4508,9 +4482,9 @@ export async function runAgentLoop(opts: {
4508
4482
  ];
4509
4483
  }
4510
4484
  let resultStr =
4511
- typeof redacted === "string"
4512
- ? redacted
4513
- : JSON.stringify(redacted, null, 2);
4485
+ typeof resultForAgent === "string"
4486
+ ? resultForAgent
4487
+ : JSON.stringify(resultForAgent, null, 2);
4514
4488
  if (resultStr.length > toolMaxResultChars) {
4515
4489
  const truncated = resultStr.slice(0, toolMaxResultChars);
4516
4490
  resultStr = `${truncated}\n\n...[truncated — full result was ${resultStr.length.toLocaleString()} chars; only first ${toolMaxResultChars.toLocaleString()} shown]`;
@@ -27,7 +27,6 @@ const DEFAULT_SKIP_ACTIONS = new Set<string>([
27
27
  "context-report",
28
28
  "context-manifest-get",
29
29
  "change-appearance",
30
- "toggle-demo-mode",
31
30
  ]);
32
31
 
33
32
  /** Name patterns for high-frequency state-sync actions, skipped by default. */
@@ -886,57 +886,71 @@ export async function updateThreadData(
886
886
  messageCount: number,
887
887
  options: UpdateThreadDataOptions = {},
888
888
  ): Promise<void> {
889
- await ensureTable();
889
+ // getThread() ensures the table exists. Keep that bootstrap inside the
890
+ // retry boundary below so a cold serverless process can recover from a
891
+ // transient initialization/read failure too.
890
892
  const client = getDbExec();
891
- const maxAttempts =
892
- options.maxAttempts ?? DEFAULT_THREAD_DATA_UPDATE_ATTEMPTS;
893
+ const maxAttempts = Math.max(
894
+ 1,
895
+ options.maxAttempts ?? DEFAULT_THREAD_DATA_UPDATE_ATTEMPTS,
896
+ );
893
897
  let lastConflict = false;
898
+ let lastError: unknown = null;
894
899
 
895
900
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
896
- const current = await getThread(id);
897
- if (!current) return;
898
-
899
- let nextThreadData = threadData;
900
- let nextMessageCount = messageCount;
901
901
  try {
902
- const merged = mergeThreadDataForClientSave(
903
- parseThreadData(current.threadData),
904
- parseThreadData(threadData),
905
- {
906
- preserveExistingQueuedMessages:
907
- options.preserveExistingQueuedMessages ?? true,
908
- preserveExistingTopLevelKeys:
909
- options.preserveExistingTopLevelKeys ?? true,
910
- },
911
- );
912
- nextThreadData = JSON.stringify(merged);
913
- if (Array.isArray(merged.messages)) {
914
- nextMessageCount = merged.messages.length;
902
+ const current = await getThread(id);
903
+ if (!current) return;
904
+
905
+ let nextThreadData = threadData;
906
+ let nextMessageCount = messageCount;
907
+ try {
908
+ const merged = mergeThreadDataForClientSave(
909
+ parseThreadData(current.threadData),
910
+ parseThreadData(threadData),
911
+ {
912
+ preserveExistingQueuedMessages:
913
+ options.preserveExistingQueuedMessages ?? true,
914
+ preserveExistingTopLevelKeys:
915
+ options.preserveExistingTopLevelKeys ?? true,
916
+ },
917
+ );
918
+ nextThreadData = JSON.stringify(merged);
919
+ if (Array.isArray(merged.messages)) {
920
+ nextMessageCount = merged.messages.length;
921
+ }
922
+ } catch {
923
+ // Keep the caller's serialized value if either JSON blob is malformed.
915
924
  }
916
- } catch {
917
- // Keep the caller's serialized value if either JSON blob is malformed.
918
- }
919
925
 
920
- const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);
921
- const result = await client.execute({
922
- sql: `UPDATE chat_threads SET thread_data = ?, title = ?, preview = ?, message_count = ?, updated_at = ? WHERE id = ? AND updated_at = ?`,
923
- args: [
924
- nextThreadData,
925
- title,
926
- preview,
927
- nextMessageCount,
928
- nextUpdatedAt,
929
- id,
930
- current.updatedAt,
931
- ],
932
- });
926
+ const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);
927
+ const result = await client.execute({
928
+ sql: `UPDATE chat_threads SET thread_data = ?, title = ?, preview = ?, message_count = ?, updated_at = ? WHERE id = ? AND updated_at = ?`,
929
+ args: [
930
+ nextThreadData,
931
+ title,
932
+ preview,
933
+ nextMessageCount,
934
+ nextUpdatedAt,
935
+ id,
936
+ current.updatedAt,
937
+ ],
938
+ });
939
+
940
+ if (result.rowsAffected > 0) {
941
+ emitChatThreadChange(id);
942
+ return;
943
+ }
933
944
 
934
- if (result.rowsAffected > 0) {
935
- emitChatThreadChange(id);
936
- return;
945
+ lastConflict = true;
946
+ } catch (error) {
947
+ // Completion saves happen after a long model/tool turn, when a
948
+ // transient connection or serverless DB failure is especially costly.
949
+ // Retry the whole read/merge/write attempt like a CAS conflict, while
950
+ // preserving the final error if the database remains unavailable.
951
+ lastError = error;
937
952
  }
938
953
 
939
- lastConflict = true;
940
954
  if (attempt < maxAttempts - 1) {
941
955
  await new Promise((resolve) =>
942
956
  setTimeout(
@@ -947,6 +961,8 @@ export async function updateThreadData(
947
961
  }
948
962
  }
949
963
 
964
+ if (lastError) throw lastError;
965
+
950
966
  if (lastConflict) {
951
967
  if (options.ignoreConflicts) return;
952
968
  throw new Error(
@@ -2307,8 +2307,7 @@ const AssistantChatInner = forwardRef<
2307
2307
  const missingApiKey = agentEngineConfigured.missing;
2308
2308
  const isProviderStatusChecking =
2309
2309
  providerStatusChecksEnabled && agentEngineConfigured.state === "unknown";
2310
- const isComposerDisabled =
2311
- missingApiKey || isProviderStatusChecking || composerDisabled;
2310
+ const isComposerDisabled = missingApiKey || composerDisabled;
2312
2311
  const [missingKeySetupOpen, setMissingKeySetupOpen] = useState(false);
2313
2312
  const requestMissingKeySetup = useCallback(() => {
2314
2313
  setMissingKeySetupOpen(true);
@@ -11,8 +11,10 @@ import {
11
11
  BranchPickerPrimitive,
12
12
  ComposerPrimitive,
13
13
  useMessagePartReasoning,
14
+ useMessagePartRuntime,
14
15
  } from "@assistant-ui/react";
15
16
  import type { Attachment } from "@assistant-ui/react";
17
+ import { useAuiState } from "@assistant-ui/store";
16
18
  import {
17
19
  IconX,
18
20
  IconCheck,
@@ -842,8 +844,18 @@ export function shouldShowAssistantMessageFooter({
842
844
 
843
845
  function ReasoningMessagePart() {
844
846
  const part = useMessagePartReasoning();
845
- const chatRunning = React.useContext(ChatRunningContext);
847
+ const partRuntime = useMessagePartRuntime();
848
+ const messageParts = useAuiState((state) => state.message.parts);
846
849
  const isStreaming = part.status?.type === "running";
850
+ const partIndex =
851
+ partRuntime.path.messagePartSelector.type === "index"
852
+ ? partRuntime.path.messagePartSelector.index
853
+ : -1;
854
+ const latestReasoningPartIndex = messageParts.reduce(
855
+ (latestIndex, messagePart, index) =>
856
+ messagePart.type === "reasoning" ? index : latestIndex,
857
+ -1,
858
+ );
847
859
  // Time thinking client-side: record the moment streaming first starts and
848
860
  // the moment it stops so the cell can show "Thought for Xs". Historical
849
861
  // messages that were never observed streaming in this session never get a
@@ -865,8 +877,8 @@ function ReasoningMessagePart() {
865
877
  text={part.text}
866
878
  isStreaming={isStreaming}
867
879
  durationMs={durationMs}
868
- defaultOpen={isStreaming || !chatRunning}
869
- autoCollapse={chatRunning}
880
+ defaultOpen={partIndex === latestReasoningPartIndex}
881
+ collapseWhenReplaced={partIndex < latestReasoningPartIndex}
870
882
  />
871
883
  );
872
884
  }
@@ -897,6 +897,11 @@ export function ReconnectStreamMessage({
897
897
  content: ContentPart[];
898
898
  }) {
899
899
  const chatRunning = React.useContext(ChatRunningContext);
900
+ const latestReasoningPartIndex = content.reduce(
901
+ (latestIndex, part, index) =>
902
+ part.type === "reasoning" ? index : latestIndex,
903
+ -1,
904
+ );
900
905
  const streamingTextPartIndex =
901
906
  content.at(-1)?.type === "text" ? content.length - 1 : -1;
902
907
  const streamingReasoningPartIndex =
@@ -924,7 +929,8 @@ export function ReconnectStreamMessage({
924
929
  key={`reconnect-reasoning-${i}`}
925
930
  text={part.text}
926
931
  isStreaming={chatRunning && i === streamingReasoningPartIndex}
927
- defaultOpen={chatRunning && i === streamingReasoningPartIndex}
932
+ defaultOpen={i === latestReasoningPartIndex}
933
+ collapseWhenReplaced={i < latestReasoningPartIndex}
928
934
  />
929
935
  );
930
936
  }
@@ -969,6 +975,7 @@ export function ReasoningCell({
969
975
  isStreaming = false,
970
976
  defaultOpen,
971
977
  autoCollapse = false,
978
+ collapseWhenReplaced = false,
972
979
  durationMs,
973
980
  }: {
974
981
  text: string;
@@ -976,6 +983,8 @@ export function ReasoningCell({
976
983
  defaultOpen?: boolean;
977
984
  /** Animate closed when a live reasoning segment finishes during a run. */
978
985
  autoCollapse?: boolean;
986
+ /** Animate closed when a newer reasoning segment replaces this one. */
987
+ collapseWhenReplaced?: boolean;
979
988
  /**
980
989
  * Elapsed thinking time in ms, once known. Only meaningful once streaming
981
990
  * has finished — callers that track live timing (see ReasoningMessagePart)
@@ -987,6 +996,7 @@ export function ReasoningCell({
987
996
  const embeddedInWorkSummary = React.useContext(WorkSummaryContentContext);
988
997
  const [open, setOpen] = useState(defaultOpen ?? true);
989
998
  const wasStreamingRef = useRef(isStreaming);
999
+ const wasReplacedRef = useRef(collapseWhenReplaced);
990
1000
  const trimmed = text.trim();
991
1001
 
992
1002
  useEffect(() => {
@@ -996,6 +1006,13 @@ export function ReasoningCell({
996
1006
  wasStreamingRef.current = isStreaming;
997
1007
  }, [autoCollapse, isStreaming]);
998
1008
 
1009
+ useEffect(() => {
1010
+ if (collapseWhenReplaced && !wasReplacedRef.current) {
1011
+ setOpen(false);
1012
+ }
1013
+ wasReplacedRef.current = collapseWhenReplaced;
1014
+ }, [collapseWhenReplaced]);
1015
+
999
1016
  if (!trimmed && !isStreaming) return null;
1000
1017
 
1001
1018
  if (embeddedInWorkSummary) {
@@ -6,7 +6,7 @@ import {
6
6
  } from "@tabler/icons-react";
7
7
  import { useQuery, useQueryClient } from "@tanstack/react-query";
8
8
  import { useEffect, useMemo, useRef, useState } from "react";
9
- import { useNavigate } from "react-router";
9
+ import { Link } from "react-router";
10
10
 
11
11
  import { extensionPath } from "../../extensions/path.js";
12
12
  import { THEME_VAR_NAMES } from "../../extensions/theme.js";
@@ -482,7 +482,6 @@ function EmbeddedToolMenu({
482
482
  const [open, setOpen] = useState(false);
483
483
  const [confirmingDelete, setConfirmingDelete] = useState(false);
484
484
  const queryClient = useQueryClient();
485
- const navigate = useNavigate();
486
485
 
487
486
  const closeMenu = () => {
488
487
  setOpen(false);
@@ -545,17 +544,14 @@ function EmbeddedToolMenu({
545
544
  <PopoverContent align="end" sideOffset={4} className="w-56 p-1">
546
545
  {!confirmingDelete ? (
547
546
  <div className="flex flex-col">
548
- <button
549
- type="button"
550
- onClick={() => {
551
- closeMenu();
552
- navigate(extensionPath(extensionId, toolName));
553
- }}
547
+ <Link
548
+ to={extensionPath(extensionId, toolName)}
549
+ onClick={closeMenu}
554
550
  className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] hover:bg-accent cursor-pointer text-left"
555
551
  >
556
552
  <IconExternalLink className="h-3.5 w-3.5" />
557
553
  <span>{t("extensions.openFullView")}</span>
558
- </button>
554
+ </Link>
559
555
  <button
560
556
  type="button"
561
557
  onClick={removeFromSlot}
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  IconArrowBackUp,
3
3
  IconChevronRight,
4
- IconCode,
5
4
  IconDotsVertical,
6
5
  IconHistory,
7
6
  IconLoader2,
@@ -21,7 +20,6 @@ import { SESSION_REPLAY_IFRAME_ATTRIBUTE } from "../../session-replay-iframe-pro
21
20
  import { sendToAgentChat } from "../agent-chat.js";
22
21
  import { AgentToggleButton } from "../AgentPanel.js";
23
22
  import { agentNativePath, appPath } from "../api-path.js";
24
- import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog.js";
25
23
  import {
26
24
  Popover,
27
25
  PopoverContent,
@@ -366,131 +364,6 @@ function applyCanonicalLink(path: string): () => void {
366
364
  };
367
365
  }
368
366
 
369
- function SourceCodeDialog({
370
- extension,
371
- onSaved,
372
- }: {
373
- extension: Extension;
374
- onSaved?: () => void;
375
- }) {
376
- const queryClient = useQueryClient();
377
- const [open, setOpen] = useState(false);
378
- const [code, setCode] = useState(extension.content ?? "");
379
- const [saving, setSaving] = useState(false);
380
- const [error, setError] = useState<string | null>(null);
381
-
382
- // Sync in when the dialog opens, or when the viewed extension changes
383
- // while the dialog stays mounted (e.g. re-parented to a different id).
384
- useEffect(() => {
385
- if (open) setCode(extension.content ?? "");
386
- }, [open, extension.id]);
387
-
388
- const isDirty = code !== (extension.content ?? "");
389
-
390
- // Block Escape / outside-click from closing while there are unsaved edits.
391
- const handleOpenChange = (next: boolean) => {
392
- if (!next && isDirty) return;
393
- setOpen(next);
394
- if (!next) setError(null);
395
- };
396
-
397
- const handleCancel = () => {
398
- setCode(extension.content ?? "");
399
- setOpen(false);
400
- setError(null);
401
- };
402
-
403
- const handleSave = async () => {
404
- setSaving(true);
405
- setError(null);
406
- try {
407
- const res = await fetch(
408
- agentNativePath(`/_agent-native/extensions/${extension.id}`),
409
- {
410
- method: "PUT",
411
- headers: { "Content-Type": "application/json" },
412
- body: JSON.stringify({ content: code }),
413
- },
414
- );
415
- if (!res.ok) {
416
- const body = await res.json().catch(() => ({}));
417
- throw new Error(body?.error ?? `Save failed (${res.status})`);
418
- }
419
- setOpen(false);
420
- queryClient.setQueryData<Extension>(["extension", extension.id], (old) =>
421
- old ? { ...old, content: code } : old,
422
- );
423
- queryClient.invalidateQueries({
424
- queryKey: ["extension", extension.id],
425
- });
426
- queryClient.invalidateQueries({ queryKey: ["extensions"] });
427
- onSaved?.();
428
- } catch (err: any) {
429
- setError(err?.message ?? "Save failed");
430
- } finally {
431
- setSaving(false);
432
- }
433
- };
434
-
435
- return (
436
- <>
437
- <Tooltip>
438
- <TooltipTrigger asChild>
439
- <button
440
- type="button"
441
- onClick={() => setOpen(true)}
442
- className="inline-flex items-center justify-center rounded-md h-8 w-8 text-muted-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer"
443
- >
444
- <IconCode className="h-4 w-4" />
445
- </button>
446
- </TooltipTrigger>
447
- <TooltipContent>View / edit source</TooltipContent>
448
- </Tooltip>
449
- <Dialog open={open} onOpenChange={handleOpenChange}>
450
- <DialogContent className="flex h-[85vh] w-[90vw] max-w-[900px] flex-col gap-0 overflow-hidden p-0">
451
- <div className="flex shrink-0 items-center border-b border-border px-5 py-3 pr-12">
452
- <DialogTitle className="truncate text-sm font-medium">
453
- {extension.name} — source
454
- </DialogTitle>
455
- </div>
456
- <textarea
457
- className="flex-1 resize-none bg-muted/40 px-5 py-4 font-mono text-xs leading-relaxed text-foreground focus:outline-none"
458
- value={code}
459
- onChange={(e) => setCode(e.target.value)}
460
- spellCheck={false}
461
- />
462
- <div className="flex shrink-0 items-center justify-between border-t border-border px-5 py-3">
463
- {error ? (
464
- <p className="text-xs text-destructive">{error}</p>
465
- ) : (
466
- <span className="text-xs text-muted-foreground">
467
- Alpine.js / HTML &middot; {code.length.toLocaleString()} chars
468
- </span>
469
- )}
470
- <div className="flex items-center gap-2">
471
- <button
472
- type="button"
473
- onClick={handleCancel}
474
- className="inline-flex h-8 cursor-pointer items-center rounded-md border border-input px-3 text-xs hover:bg-accent"
475
- >
476
- Cancel
477
- </button>
478
- <button
479
- type="button"
480
- onClick={handleSave}
481
- disabled={saving}
482
- className="inline-flex h-8 cursor-pointer items-center rounded-md bg-primary px-4 text-xs font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
483
- >
484
- {saving ? "Saving…" : "Save"}
485
- </button>
486
- </div>
487
- </div>
488
- </DialogContent>
489
- </Dialog>
490
- </>
491
- );
492
- }
493
-
494
367
  function EditToolPopover({
495
368
  extension,
496
369
  onOpenChange,
@@ -1342,12 +1215,6 @@ export function ExtensionViewer({ extensionId }: ExtensionViewerProps) {
1342
1215
  onRestored={() => setRefreshKey((k) => k + 1)}
1343
1216
  onOpenChange={onPopoverOpenChange}
1344
1217
  />
1345
- {extension.canEdit && (
1346
- <SourceCodeDialog
1347
- extension={extension}
1348
- onSaved={() => setRefreshKey((k) => k + 1)}
1349
- />
1350
- )}
1351
1218
  <EditToolPopover
1352
1219
  extension={extension}
1353
1220
  onOpenChange={onPopoverOpenChange}
@@ -388,6 +388,13 @@ export {
388
388
  type BuildAgentNativeExtensionHtmlOptions,
389
389
  type CreateHttpAgentNativeExtensionStorageOptions,
390
390
  } from "./extensions/portable-extension.js";
391
+ export { injectSessionReplayIframeBootstrap } from "../extensions/session-replay-iframe.js";
392
+ export {
393
+ SESSION_REPLAY_IFRAME_ATTRIBUTE,
394
+ SESSION_REPLAY_IFRAME_PROBE,
395
+ SESSION_REPLAY_IFRAME_START,
396
+ SESSION_REPLAY_IFRAME_STOP,
397
+ } from "../session-replay-iframe-protocol.js";
391
398
  export {
392
399
  AGENT_NATIVE_HOST_BRIDGE_VERSION,
393
400
  AGENT_NATIVE_HOST_MESSAGE_TYPES,
@@ -1,61 +1,12 @@
1
- /**
2
- * <DemoModeSection /> — toggle that replaces emails and numbers with realistic
3
- * fake data everywhere (UI + what the agent sees) while preserving names,
4
- * free text, labels, IDs, and structure so the app keeps working.
5
- *
6
- * The toggle is stored in application_state under `demo-mode`
7
- * (`{ enabled: boolean }`) and written via `PUT
8
- * /_agent-native/application-state/demo-mode` (optimistic — flip first,
9
- * write in the background). It READS from `/_agent-native/demo/status`
10
- * instead, which ORs the per-user toggle with the `DEMO_MODE` env: a hosted
11
- * demo deployment is forced on and the switch is locked so it can't be
12
- * accidentally turned off on stage.
13
- */
1
+ /** Browser-local presentation toggle. Backend and agent results stay real. */
14
2
 
15
3
  import { IconEyeOff } from "@tabler/icons-react";
16
- import { useEffect, useState } from "react";
17
4
 
18
- import { agentNativePath } from "../api-path.js";
5
+ import { setBrowserDemoModeEnabled } from "../../demo/browser-state.js";
19
6
  import { useDemoModeStatus } from "../use-demo-mode-status.js";
20
7
 
21
- const DEMO_MODE_WRITE_URL = agentNativePath(
22
- "/_agent-native/application-state/demo-mode",
23
- );
24
-
25
8
  export function DemoModeSection() {
26
- const [enabled, setEnabled] = useState<boolean | null>(null);
27
- const { enabled: serverEnabled, forced, isLoading } = useDemoModeStatus();
28
-
29
- // Surface the server value once it arrives (and on subsequent polls), but
30
- // never clobber an in-flight optimistic toggle with a stale read. When
31
- // forced by env, always reflect the locked-on state.
32
- useEffect(() => {
33
- if (forced) {
34
- setEnabled(true);
35
- } else if (!isLoading) {
36
- setEnabled((prev) => (prev === null ? serverEnabled : prev));
37
- }
38
- }, [serverEnabled, forced, isLoading]);
39
-
40
- const toggle = async (next: boolean) => {
41
- if (forced) return;
42
- const previous = enabled;
43
- // Optimistic: flip immediately, write in the background.
44
- setEnabled(next);
45
- try {
46
- const res = await fetch(DEMO_MODE_WRITE_URL, {
47
- method: "PUT",
48
- headers: { "Content-Type": "application/json" },
49
- credentials: "include",
50
- body: JSON.stringify({ enabled: next }),
51
- });
52
- if (!res.ok) {
53
- throw new Error(`HTTP ${res.status}`);
54
- }
55
- } catch {
56
- setEnabled(previous);
57
- }
58
- };
9
+ const { enabled } = useDemoModeStatus();
59
10
 
60
11
  return (
61
12
  <div className="flex items-start justify-between gap-3 rounded-md border border-border bg-accent/30 px-2.5 py-2">
@@ -64,30 +15,23 @@ export function DemoModeSection() {
64
15
  Enable demo mode
65
16
  </div>
66
17
  <p className="text-[10px] text-muted-foreground mt-0.5">
67
- Anonymize emails in the UI and reshape supported dashboard charts for
68
- presentations. Agent-visible emails and numbers are also anonymized.
69
- Names, labels, IDs, and structure stay intact.
18
+ Anonymize displayed emails in this browser and reshape supported
19
+ dashboard charts for presentations. Backend, MCP, and agent results
20
+ stay real and access-scoped.
70
21
  </p>
71
- {forced && (
72
- <p className="text-[10px] text-muted-foreground mt-0.5">
73
- Enforced by the <code>DEMO_MODE</code> environment variable — can't
74
- be turned off here.
75
- </p>
76
- )}
77
22
  </div>
78
23
  <button
79
24
  type="button"
80
25
  role="switch"
81
- aria-checked={!!enabled}
26
+ aria-checked={enabled}
82
27
  aria-label="Enable demo mode"
83
- disabled={enabled === null || forced}
84
- onClick={() => toggle(!enabled)}
28
+ onClick={() => setBrowserDemoModeEnabled(!enabled)}
85
29
  // Theme tokens; streaming agent owns layout.
86
30
  className={`relative inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full transition-colors ${
87
31
  enabled
88
32
  ? "bg-primary"
89
33
  : "bg-muted-foreground/30 hover:bg-muted-foreground/50"
90
- } ${enabled === null || forced ? "opacity-60" : ""}`}
34
+ }`}
91
35
  >
92
36
  <span
93
37
  className={`inline-block h-3 w-3 transform rounded-full bg-background transition-transform ${
@@ -2790,7 +2790,7 @@ function SettingsPanelContent({
2790
2790
  id={settingsSectionDomId("demo-mode")}
2791
2791
  icon={<IconEyeOff size={14} />}
2792
2792
  title="Demo mode"
2793
- subtitle="Replace emails and numbers with realistic fake data everywhere — in the UI and what the agent sees. Names, free text, labels, IDs, and structure are preserved so the app keeps working."
2793
+ subtitle="Replace displayed emails with realistic fake data in this browser and reshape supported charts for presentations. Backend, MCP, and agent results stay real and access-scoped."
2794
2794
  open={openSection === "demo-mode"}
2795
2795
  onToggle={() => toggle("demo-mode")}
2796
2796
  >