@agent-native/core 0.70.0 → 0.70.1

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 (167) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +61 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/cli/skills.ts +2 -2
  10. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  11. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  12. package/corpus/core/src/client/client-surface.ts +41 -0
  13. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  14. package/corpus/core/src/client/feedback-context.ts +4 -0
  15. package/corpus/core/src/client/index.ts +1 -0
  16. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  17. package/corpus/core/src/mcp/build-server.ts +57 -1
  18. package/corpus/core/src/mcp/embed-app.ts +86 -2
  19. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  20. package/corpus/core/src/server/core-routes-plugin.ts +78 -0
  21. package/corpus/core/src/server/onboarding-html.ts +35 -0
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  24. package/corpus/templates/analytics/AGENTS.md +8 -1
  25. package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
  26. package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
  27. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  28. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  29. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
  30. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  32. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  34. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  36. package/corpus/templates/assets/app/global.css +17 -1
  37. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  38. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  39. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  40. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  41. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  42. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  43. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  44. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  50. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  51. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  52. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  53. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  54. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  55. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  56. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  57. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  58. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  59. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  60. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  61. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  62. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  63. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  64. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  65. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  66. package/corpus/templates/clips/desktop/src/styles.css +70 -2
  67. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
  68. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
  69. package/corpus/templates/content/AGENTS.md +20 -3
  70. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  71. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  72. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  73. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  74. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  75. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  76. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  77. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  78. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  79. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  80. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  81. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  82. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  83. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  84. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  85. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  86. package/corpus/templates/content/app/global.css +9 -0
  87. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  88. package/corpus/templates/content/server/db/schema.ts +30 -0
  89. package/corpus/templates/content/server/plugins/db.ts +87 -0
  90. package/corpus/templates/content/shared/api.ts +7 -0
  91. package/corpus/templates/content/shared/properties.ts +109 -0
  92. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  93. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  94. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  95. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  96. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  97. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  98. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  99. package/corpus/templates/forms/server/db/schema.ts +7 -0
  100. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  101. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  102. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  103. package/corpus/templates/forms/shared/types.ts +12 -0
  104. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  105. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  106. package/dist/action.js +87 -0
  107. package/dist/action.js.map +1 -1
  108. package/dist/agent/engine/builder-engine.d.ts +1 -1
  109. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  110. package/dist/agent/model-config.d.ts +2 -2
  111. package/dist/agent/model-config.d.ts.map +1 -1
  112. package/dist/agent/model-config.js +0 -2
  113. package/dist/agent/model-config.js.map +1 -1
  114. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  115. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  116. package/dist/agent/run-loop-with-resume.js +44 -0
  117. package/dist/agent/run-loop-with-resume.js.map +1 -1
  118. package/dist/cli/skills.js +2 -2
  119. package/dist/cli/skills.js.map +1 -1
  120. package/dist/client/AgentPanel.d.ts.map +1 -1
  121. package/dist/client/AgentPanel.js +5 -3
  122. package/dist/client/AgentPanel.js.map +1 -1
  123. package/dist/client/AssistantChat.d.ts +6 -1
  124. package/dist/client/AssistantChat.d.ts.map +1 -1
  125. package/dist/client/AssistantChat.js +2 -2
  126. package/dist/client/AssistantChat.js.map +1 -1
  127. package/dist/client/client-surface.d.ts +17 -0
  128. package/dist/client/client-surface.d.ts.map +1 -0
  129. package/dist/client/client-surface.js +24 -0
  130. package/dist/client/client-surface.js.map +1 -0
  131. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  132. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  133. package/dist/client/composer/TiptapComposer.js +27 -5
  134. package/dist/client/composer/TiptapComposer.js.map +1 -1
  135. package/dist/client/feedback-context.d.ts +3 -0
  136. package/dist/client/feedback-context.d.ts.map +1 -1
  137. package/dist/client/feedback-context.js +2 -0
  138. package/dist/client/feedback-context.js.map +1 -1
  139. package/dist/client/index.d.ts +1 -0
  140. package/dist/client/index.d.ts.map +1 -1
  141. package/dist/client/index.js +1 -0
  142. package/dist/client/index.js.map +1 -1
  143. package/dist/client/sse-event-processor.d.ts.map +1 -1
  144. package/dist/client/sse-event-processor.js +7 -1
  145. package/dist/client/sse-event-processor.js.map +1 -1
  146. package/dist/mcp/build-server.d.ts +21 -0
  147. package/dist/mcp/build-server.d.ts.map +1 -1
  148. package/dist/mcp/build-server.js +43 -1
  149. package/dist/mcp/build-server.js.map +1 -1
  150. package/dist/mcp/embed-app.d.ts.map +1 -1
  151. package/dist/mcp/embed-app.js +86 -2
  152. package/dist/mcp/embed-app.js.map +1 -1
  153. package/dist/mcp/oauth-route.d.ts.map +1 -1
  154. package/dist/mcp/oauth-route.js +95 -12
  155. package/dist/mcp/oauth-route.js.map +1 -1
  156. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  157. package/dist/server/core-routes-plugin.js +63 -0
  158. package/dist/server/core-routes-plugin.js.map +1 -1
  159. package/dist/server/onboarding-html.d.ts.map +1 -1
  160. package/dist/server/onboarding-html.js +35 -0
  161. package/dist/server/onboarding-html.js.map +1 -1
  162. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  163. package/docs/design/durable-agent-runs.md +217 -0
  164. package/package.json +1 -1
  165. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  166. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  167. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The runtime shell the web UI is rendering inside. Used as pass-through
3
+ * feedback metadata so form owners can tell whether a submission came from the
4
+ * Agent Native desktop app (Electron), a Tauri desktop shell (e.g. Clips), or a
5
+ * plain browser — without surfacing it as a visible form field.
6
+ */
7
+ export type ClientSurface = "web" | "electron" | "tauri";
8
+
9
+ interface SurfaceGlobals {
10
+ // Tauri v2 always injects this into the webview; `__TAURI__` only exists when
11
+ // the app opts into `withGlobalTauri`, so check both.
12
+ __TAURI_INTERNALS__?: unknown;
13
+ __TAURI__?: unknown;
14
+ // Exposed by the Agent Native desktop (Electron) preload bridges.
15
+ agentNativeDesktop?: unknown;
16
+ electronAPI?: unknown;
17
+ }
18
+
19
+ /**
20
+ * Best-effort, side-effect-free detection of the current client surface.
21
+ * Returns "web" during SSR and in any plain browser.
22
+ *
23
+ * Electron detection keys off the `AgentNativeDesktop` User-Agent token that the
24
+ * desktop shell appends app-wide (see `app.userAgentFallback`), plus the preload
25
+ * globals, so it stays specific to our app rather than every Electron webview.
26
+ */
27
+ export function getClientSurface(): ClientSurface {
28
+ if (typeof window === "undefined") return "web";
29
+ const w = window as unknown as SurfaceGlobals;
30
+ if (w.__TAURI_INTERNALS__ || w.__TAURI__) return "tauri";
31
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent || "" : "";
32
+ if (
33
+ /AgentNativeDesktop/i.test(ua) ||
34
+ /\bElectron\b/i.test(ua) ||
35
+ w.agentNativeDesktop ||
36
+ w.electronAPI
37
+ ) {
38
+ return "electron";
39
+ }
40
+ return "web";
41
+ }
@@ -417,6 +417,11 @@ export interface TiptapComposerProps {
417
417
  * Used by the Electron desktop app to route through the native IPC handler.
418
418
  */
419
419
  onConnectProvider?: () => void;
420
+ /**
421
+ * Optional secondary model menu (e.g. an image-generation model) rendered as
422
+ * an extra section inside the model picker. Opt-in; omit for chat-only apps.
423
+ */
424
+ imageModelMenu?: ComposerImageModelMenu;
420
425
  /** Stable scope for persisted drafts, usually the active thread or tab id. */
421
426
  draftScope?: string;
422
427
  /** Keyed context nuggets staged for the next submitted prompt. */
@@ -669,6 +674,24 @@ function latestModelsOnly(models: string[]): string[] {
669
674
  });
670
675
  }
671
676
 
677
+ /**
678
+ * Optional secondary model menu for apps that drive a separate generation model
679
+ * alongside the chat LLM (e.g. the Assets app's image-generation model). When
680
+ * provided, the model picker renders an extra collapsible section so the user
681
+ * can see and pick both "what reasons about my request" (the chat model) and
682
+ * "what produces the output" (this model). Opt-in — omit it and nothing changes.
683
+ */
684
+ export interface ComposerImageModelMenu {
685
+ /** Currently-selected model id for this secondary menu. */
686
+ value: string;
687
+ /** Selectable options (stable id + human label). */
688
+ options: Array<{ value: string; label: string }>;
689
+ /** Invoked when the user picks a different option. */
690
+ onChange: (value: string) => void;
691
+ /** Section header. Defaults to "Image model". */
692
+ label?: string;
693
+ }
694
+
672
695
  function ModelSelector({
673
696
  model,
674
697
  effort = "auto",
@@ -677,6 +700,7 @@ function ModelSelector({
677
700
  onEffortChange,
678
701
  providerConnectStatusEnabled = true,
679
702
  onConnectProvider,
703
+ imageModel,
680
704
  }: {
681
705
  model: string;
682
706
  effort?: ReasoningEffort;
@@ -690,6 +714,7 @@ function ModelSelector({
690
714
  onEffortChange?: (effort: ReasoningEffort) => void;
691
715
  providerConnectStatusEnabled?: boolean;
692
716
  onConnectProvider?: () => void;
717
+ imageModel?: ComposerImageModelMenu;
693
718
  }) {
694
719
  const [open, setOpen] = useState(false);
695
720
  const autoModelGroup = engines.find((group) => group.models.includes("auto"));
@@ -744,6 +769,24 @@ function ModelSelector({
744
769
  });
745
770
  }, []);
746
771
 
772
+ // The reasoning effort list is collapsed by default — it's a secondary
773
+ // control most users don't touch, so it stays tucked behind a header that
774
+ // reveals the current effort at a glance. Reset to collapsed on each open.
775
+ const [reasoningExpanded, setReasoningExpanded] = useState(false);
776
+ useEffect(() => {
777
+ if (open) setReasoningExpanded(false);
778
+ }, [open]);
779
+
780
+ // The optional image-model section follows the same collapsed-by-default
781
+ // pattern; the current selection shows next to the header.
782
+ const [imageExpanded, setImageExpanded] = useState(false);
783
+ useEffect(() => {
784
+ if (open) setImageExpanded(false);
785
+ }, [open]);
786
+ const imageModelLabel =
787
+ imageModel?.options.find((option) => option.value === imageModel.value)
788
+ ?.label ?? imageModel?.value;
789
+
747
790
  // When Builder.io isn't connected, surface a one-click connect path —
748
791
  // it unlocks every model family (Claude, OpenAI, Gemini) without the
749
792
  // user having to paste individual API keys.
@@ -835,6 +878,52 @@ function ModelSelector({
835
878
  <div className="my-1 border-t border-border" />
836
879
  </>
837
880
  )}
881
+ {imageModel && imageModel.options.length > 0 && (
882
+ <>
883
+ <div className="flex items-center hover:bg-accent/30">
884
+ <button
885
+ type="button"
886
+ aria-expanded={imageExpanded}
887
+ onClick={() => setImageExpanded((prev) => !prev)}
888
+ className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-left"
889
+ >
890
+ {imageExpanded ? (
891
+ <IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
892
+ ) : (
893
+ <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
894
+ )}
895
+ <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
896
+ {imageModel.label ?? "Image model"}
897
+ </span>
898
+ {!imageExpanded && imageModelLabel && (
899
+ <span className="text-[11px] text-muted-foreground/80 truncate">
900
+ {imageModelLabel}
901
+ </span>
902
+ )}
903
+ </button>
904
+ </div>
905
+ {imageExpanded &&
906
+ imageModel.options.map((option) => (
907
+ <button
908
+ key={option.value}
909
+ type="button"
910
+ onClick={() => {
911
+ imageModel.onChange(option.value);
912
+ setImageExpanded(false);
913
+ }}
914
+ className="flex w-full items-center gap-3 pl-7 pr-3 py-1.5 text-left hover:bg-accent/50"
915
+ >
916
+ <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
917
+ {option.label}
918
+ </span>
919
+ {option.value === imageModel.value && (
920
+ <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
921
+ )}
922
+ </button>
923
+ ))}
924
+ <div className="my-1 border-t border-border" />
925
+ </>
926
+ )}
838
927
  {autoModelGroup && (
839
928
  <button
840
929
  type="button"
@@ -930,24 +1019,44 @@ function ModelSelector({
930
1019
  {effortOptions.length > 0 && (
931
1020
  <>
932
1021
  <div className="my-1 border-t border-border" />
933
- <div className="px-3 py-1.5 text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
934
- Reasoning
935
- </div>
936
- {effortOptions.map((option) => (
1022
+ <div className="flex items-center hover:bg-accent/30">
937
1023
  <button
938
- key={option}
939
1024
  type="button"
940
- onClick={() => onEffortChange?.(option)}
941
- className="flex w-full items-center gap-3 px-3 py-1.5 text-left hover:bg-accent/50"
1025
+ aria-expanded={reasoningExpanded}
1026
+ onClick={() => setReasoningExpanded((prev) => !prev)}
1027
+ className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-left"
942
1028
  >
943
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
944
- {reasoningEffortLabel(option)}
1029
+ {reasoningExpanded ? (
1030
+ <IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
1031
+ ) : (
1032
+ <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
1033
+ )}
1034
+ <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1035
+ Reasoning
945
1036
  </span>
946
- {option === effort && (
947
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1037
+ {!reasoningExpanded && (
1038
+ <span className="text-[11px] text-muted-foreground/80 truncate">
1039
+ {reasoningEffortLabel(effort)}
1040
+ </span>
948
1041
  )}
949
1042
  </button>
950
- ))}
1043
+ </div>
1044
+ {reasoningExpanded &&
1045
+ effortOptions.map((option) => (
1046
+ <button
1047
+ key={option}
1048
+ type="button"
1049
+ onClick={() => onEffortChange?.(option)}
1050
+ className="flex w-full items-center gap-3 pl-7 pr-3 py-1.5 text-left hover:bg-accent/50"
1051
+ >
1052
+ <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1053
+ {reasoningEffortLabel(option)}
1054
+ </span>
1055
+ {option === effort && (
1056
+ <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1057
+ )}
1058
+ </button>
1059
+ ))}
951
1060
  </>
952
1061
  )}
953
1062
  </PopoverContent>
@@ -994,6 +1103,7 @@ export function TiptapComposer({
994
1103
  onEffortChange,
995
1104
  providerConnectStatusEnabled,
996
1105
  onConnectProvider,
1106
+ imageModelMenu,
997
1107
  draftScope,
998
1108
  contextItems = [],
999
1109
  onRemoveContextItem,
@@ -2091,6 +2201,7 @@ export function TiptapComposer({
2091
2201
  onEffortChange={onEffortChange}
2092
2202
  providerConnectStatusEnabled={providerConnectStatusEnabled}
2093
2203
  onConnectProvider={onConnectProvider}
2204
+ imageModel={imageModelMenu}
2094
2205
  />
2095
2206
  )}
2096
2207
  {execMode && onExecModeChange && (
@@ -1,10 +1,13 @@
1
1
  import { getActiveRun } from "./active-run-state.js";
2
+ import { getClientSurface, type ClientSurface } from "./client-surface.js";
2
3
  import { scrubUrl } from "./url-scrub.js";
3
4
 
4
5
  export interface FeedbackClientContext {
5
6
  chatSessionIds: string[];
6
7
  activeRunId?: string;
7
8
  pageUrl?: string;
9
+ /** Runtime shell the feedback was sent from: web, electron, or tauri. */
10
+ clientSurface?: ClientSurface;
8
11
  }
9
12
 
10
13
  export interface FeedbackClientContextOptions {
@@ -81,6 +84,7 @@ export function getFeedbackClientContext(
81
84
  if (activeRun?.runId) context.activeRunId = activeRun.runId;
82
85
  if (typeof window !== "undefined") {
83
86
  context.pageUrl = scrubUrl(window.location.href);
87
+ context.clientSurface = getClientSurface();
84
88
  }
85
89
  return context;
86
90
  }
@@ -609,6 +609,7 @@ export {
609
609
  type StarfieldBackgroundProps,
610
610
  } from "./StarfieldBackground.js";
611
611
  export { FeedbackButton, type FeedbackButtonProps } from "./FeedbackButton.js";
612
+ export { getClientSurface, type ClientSurface } from "./client-surface.js";
612
613
  export {
613
614
  DevDatabaseLink,
614
615
  type DevDatabaseLinkProps,
@@ -233,7 +233,13 @@ function isAutoRecoverableError(ev: SSEEvent, errMsg: string): boolean {
233
233
  // same wall. This used to send the chat into a 32-continuation runaway
234
234
  // (each turn cleared+regenerated visible content) for users hitting a
235
235
  // misbehaving Builder route. Surface the error instead.
236
- code === "builder_gateway_error"
236
+ code === "builder_gateway_error" ||
237
+ // The hosted run exhausted its in-invocation continuation budget without
238
+ // finishing (run-loop-with-resume.ts). It's flagged `recoverable: true` so
239
+ // the recovery banner reads "stopped before finishing", but it must NOT
240
+ // auto-continue: another POST would hit the same ~40s wall and churn. The
241
+ // user retries deliberately (ideally as a single bulk action).
242
+ code === "run_budget_exhausted"
237
243
  ) {
238
244
  return false;
239
245
  }
@@ -184,6 +184,46 @@ export interface MCPRequestMeta {
184
184
  * `config.actions`. Set by `mountMCP` from `verifyAuth`.
185
185
  */
186
186
  fullSurface?: boolean;
187
+ /**
188
+ * Whether this request may receive inline MCP App embeds (the `ui://`
189
+ * resource reference hosts render in an iframe). Resolved once per request by
190
+ * `createMCPServerForRequest` from `isMcpAppsInlineEnabled(identity)` — the
191
+ * deploy-toggleable kill switch. When `false`, no MCP App resource is
192
+ * advertised or referenced and tool results fall back to their deep-link
193
+ * text. Defaults to disabled when unset.
194
+ */
195
+ inlineMcpApps?: boolean;
196
+ }
197
+
198
+ /**
199
+ * Deploy-toggleable kill switch for inline MCP App embeds — the `ui://`
200
+ * resource reference hosts like Codex / Cursor / ChatGPT render in a sandboxed
201
+ * iframe. **Off by default**, so a not-yet-verified inline embed never reaches
202
+ * normal users; flip it on per environment with `AGENT_NATIVE_MCP_APPS_INLINE=1`
203
+ * and a redeploy. While the global switch is off, accounts listed in
204
+ * `AGENT_NATIVE_MCP_APPS_INLINE_ALLOW_EMAILS` (comma/space separated) still get
205
+ * inline embeds, so you can keep verifying a fix in production before enabling
206
+ * it for everyone. Requires no skills/instructions change — when disabled, tool
207
+ * results simply fall back to their deep-link text.
208
+ */
209
+ export function isMcpAppsInlineEnabled(
210
+ identity: MCPCallerIdentity | undefined,
211
+ ): boolean {
212
+ const flag = process.env.AGENT_NATIVE_MCP_APPS_INLINE?.trim().toLowerCase();
213
+ if (flag === "1" || flag === "true" || flag === "yes" || flag === "on") {
214
+ return true;
215
+ }
216
+ const email = identity?.userEmail?.trim().toLowerCase();
217
+ if (email) {
218
+ const allowed = (
219
+ process.env.AGENT_NATIVE_MCP_APPS_INLINE_ALLOW_EMAILS ?? ""
220
+ )
221
+ .split(/[\s,]+/)
222
+ .map((value) => value.trim().toLowerCase())
223
+ .filter(Boolean);
224
+ if (allowed.includes(email)) return true;
225
+ }
226
+ return false;
187
227
  }
188
228
 
189
229
  type McpOAuthScope = (typeof MCP_OAUTH_SCOPES)[number];
@@ -708,7 +748,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
708
748
 
709
749
  // ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
710
750
  // when the shared shell changes in a way that must invalidate host caches.
711
- const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v45";
751
+ const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v46";
712
752
 
713
753
  function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
714
754
  const app = safeUiSegment(config.appId ?? config.name, "agent-native");
@@ -912,6 +952,12 @@ async function resolveMcpAppResource(
912
952
  ): Promise<ResolvedMcpAppResource | null> {
913
953
  const resource = entry.mcpApp?.resource;
914
954
  if (!resource) return null;
955
+ // Inline MCP App embeds are gated behind the deploy-toggleable kill switch
956
+ // (default off). When disabled, advertise no resource and attach no embed
957
+ // reference so hosts fall back to the tool's deep-link text instead of an
958
+ // iframe. This is the single chokepoint for every embed surface: tools/list
959
+ // descriptor meta, tools/call result meta, resources/list, resources/read.
960
+ if (!requestMeta?.inlineMcpApps) return null;
915
961
  const resolvedUri = getMcpAppResourceUri(config, actionName, entry);
916
962
  if (!resolvedUri) return null;
917
963
  const description = resource.description ?? entry.tool.description;
@@ -1192,6 +1238,16 @@ export async function createMCPServerForRequest(
1192
1238
  ? { userEmail: ownerFromEnv, orgDomain: undefined }
1193
1239
  : undefined);
1194
1240
 
1241
+ // Resolve the inline-MCP-App kill switch once per request from the effective
1242
+ // identity + environment, then thread it through `requestMeta` so every
1243
+ // resource/tool handler below honors the same decision. An explicit value on
1244
+ // the incoming meta (tests / embedded callers) wins.
1245
+ requestMeta = {
1246
+ ...(requestMeta ?? {}),
1247
+ inlineMcpApps:
1248
+ requestMeta?.inlineMcpApps ?? isMcpAppsInlineEnabled(effectiveIdentity),
1249
+ };
1250
+
1195
1251
  // The action set the request handlers operate on = base actions + generic
1196
1252
  // cross-app builtins (template wins on name collision). An authenticated
1197
1253
  // real caller (connect-minted token / `mcp install` owner / production —
@@ -67,6 +67,8 @@ export function embedApp(
67
67
  .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 700; color: color-mix(in srgb, CanvasText 72%, Canvas); }
68
68
  .actions { display: flex; align-items: center; gap: 6px; }
69
69
  button { min-height: 28px; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 7px; background: Canvas; color: CanvasText; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; padding: 0 9px; }
70
+ button.primary { min-height: 32px; padding: 0 14px; background: CanvasText; color: Canvas; border-color: CanvasText; }
71
+ button.primary:hover:not(:disabled) { background: color-mix(in srgb, CanvasText 86%, Canvas); }
70
72
  button:disabled { opacity: .55; cursor: default; }
71
73
  .stage { position: relative; min-height: var(--agent-native-viewport-height); }
72
74
  iframe { display: block; width: 100%; height: var(--agent-native-viewport-height); border: 0; background: Canvas; }
@@ -895,9 +897,21 @@ export function embedApp(
895
897
  });
896
898
  if (!response.ok) {
897
899
  if (response.status === 401 && isEmbedStartUrl(src)) {
900
+ reportEmbedError(
901
+ "transplant-auth",
902
+ "Embedded app session expired (HTTP 401).",
903
+ "Opaque-origin sandbox could not authenticate the embed.",
904
+ 401
905
+ );
898
906
  refreshExpiredEmbedSession();
899
907
  return;
900
908
  }
909
+ reportEmbedError(
910
+ "transplant-http",
911
+ "Embedded app returned HTTP " + response.status + ".",
912
+ undefined,
913
+ response.status
914
+ );
901
915
  throw new Error("Embedded app returned HTTP " + response.status + ".");
902
916
  }
903
917
  const html = await response.text();
@@ -998,6 +1012,52 @@ export function embedApp(
998
1012
  }, frameReadyTimeoutMs);
999
1013
  }
1000
1014
 
1015
+ // Best-effort telemetry sink so inline-embed failures are visible in
1016
+ // Sentry (handshake timeout, transplant fetch status/CORS, auth, CSP).
1017
+ // The shell is a sandboxed opaque-origin iframe, so it POSTs to the app
1018
+ // origin's CORS-open /_agent-native/mcp/embed-error route, derived from the
1019
+ // known app URLs. Deduped per stage+message so a retry loop can't spam.
1020
+ const reportedEmbedErrorKeys = new Set();
1021
+ let embedErrorReportCount = 0;
1022
+ function embedReportOrigin() {
1023
+ for (const value of [openStartUrl, openUrl, lastFrameSrc]) {
1024
+ if (typeof value === "string" && value) {
1025
+ try { return new URL(value, window.location.href).origin; } catch {}
1026
+ }
1027
+ }
1028
+ return "";
1029
+ }
1030
+ function reportEmbedError(stage, message, detail, status) {
1031
+ try {
1032
+ const key = String(stage) + "|" + String(message || "");
1033
+ if (reportedEmbedErrorKeys.has(key) || embedErrorReportCount >= 8) return;
1034
+ reportedEmbedErrorKeys.add(key);
1035
+ embedErrorReportCount += 1;
1036
+ const origin = embedReportOrigin();
1037
+ if (!origin) return;
1038
+ let renderMode = "";
1039
+ try { renderMode = renderModeSource().mode || ""; } catch {}
1040
+ const body = JSON.stringify({
1041
+ stage: String(stage || ""),
1042
+ message: String(message || "").slice(0, 500),
1043
+ detail: detail ? String(detail).slice(0, 1200) : undefined,
1044
+ url: openStartUrl || openUrl || lastFrameSrc || "",
1045
+ status: typeof status === "number" ? status : undefined,
1046
+ host: window.location.hostname || "",
1047
+ renderMode: renderMode,
1048
+ bridge: openAiBridge ? "openai" : app ? "native" : "none",
1049
+ userAgent: navigator.userAgent || ""
1050
+ });
1051
+ fetch(origin + "/_agent-native/mcp/embed-error", {
1052
+ method: "POST",
1053
+ credentials: "omit",
1054
+ keepalive: true,
1055
+ headers: { "Content-Type": "application/json" },
1056
+ body: body
1057
+ }).catch(() => {});
1058
+ } catch {}
1059
+ }
1060
+
1001
1061
  function renderFrameFallback() {
1002
1062
  clearFrameReadyTimer();
1003
1063
  clearFrameLoadTimer();
@@ -1005,12 +1065,13 @@ export function embedApp(
1005
1065
  const fallbackCopy = openUrl
1006
1066
  ? "This chat host did not allow the embedded app frame to load inline. You can still open the same app route through the host or use the URL below."
1007
1067
  : "This chat host did not allow the embedded app frame to load inline.";
1068
+ reportEmbedError("frame-fallback", fallbackCopy);
1008
1069
  stage.innerHTML =
1009
1070
  '<div class="fallback">' +
1010
1071
  '<div class="fallback-title">Open this app in its own tab</div>' +
1011
1072
  '<div class="fallback-copy">' + esc(fallbackCopy) + '</div>' +
1012
1073
  '<div class="fallback-actions">' +
1013
- '<button type="button" data-fallback-open>Open app</button>' +
1074
+ '<button type="button" class="primary" data-fallback-open>Open in new tab</button>' +
1014
1075
  '<button type="button" data-fallback-retry>Try inline again</button>' +
1015
1076
  '</div>' +
1016
1077
  (openUrl ? '<a class="fallback-url" href="' + esc(openUrl) + '" target="_blank" rel="noreferrer">' + esc(openUrl) + '</a>' : '') +
@@ -1038,6 +1099,7 @@ export function embedApp(
1038
1099
  const fallbackCopy = openUrl
1039
1100
  ? "The inline MCP app could not load in this chat host. You can open the same app route in a new tab or retry the inline load."
1040
1101
  : "The inline MCP app could not load in this chat host.";
1102
+ reportEmbedError("app-launch-error", message || fallbackCopy, message);
1041
1103
  const copyHtml = message
1042
1104
  ? '<div>' + esc(message) + '</div><div>' + esc(fallbackCopy) + '</div>'
1043
1105
  : esc(fallbackCopy);
@@ -1046,7 +1108,7 @@ export function embedApp(
1046
1108
  '<div class="fallback-title">App did not load</div>' +
1047
1109
  '<div class="fallback-copy">' + copyHtml + '</div>' +
1048
1110
  '<div class="fallback-actions">' +
1049
- '<button type="button" data-fallback-open>Open in new tab</button>' +
1111
+ '<button type="button" class="primary" data-fallback-open>Open in new tab</button>' +
1050
1112
  '<button type="button" data-fallback-retry>Retry</button>' +
1051
1113
  '</div>' +
1052
1114
  (openUrl ? '<a class="fallback-url" href="' + esc(openUrl) + '" target="_blank" rel="noreferrer">' + esc(openUrl) + '</a>' : '') +
@@ -1801,8 +1863,30 @@ export function embedApp(
1801
1863
  }
1802
1864
  } catch (err) {
1803
1865
  console.error("[agent-native] MCP app shell failed", err);
1866
+ reportEmbedError(
1867
+ "bridge-init",
1868
+ err && err.message ? err.message : "Could not initialize app.",
1869
+ err && err.stack ? String(err.stack) : undefined
1870
+ );
1804
1871
  setMessage(err && err.message ? err.message : "Could not initialize app.");
1805
1872
  }
1873
+
1874
+ window.addEventListener("error", (event) => {
1875
+ const err = event && event.error;
1876
+ reportEmbedError(
1877
+ "window-error",
1878
+ (err && err.message) || (event && event.message) || "Uncaught error in MCP app embed.",
1879
+ err && err.stack ? String(err.stack) : undefined
1880
+ );
1881
+ });
1882
+ window.addEventListener("unhandledrejection", (event) => {
1883
+ const reason = event && event.reason;
1884
+ reportEmbedError(
1885
+ "unhandled-rejection",
1886
+ (reason && reason.message) || String(reason || "Unhandled promise rejection in MCP app embed."),
1887
+ reason && reason.stack ? String(reason.stack) : undefined
1888
+ );
1889
+ });
1806
1890
  })();
1807
1891
  </script>
1808
1892
  </body>