@agent-native/core 0.161.22 → 0.162.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 (125) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
  4. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
  5. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
  6. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
  7. package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
  8. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
  10. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
  11. package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
  12. package/corpus/templates/clips/desktop/src/app.tsx +30 -40
  13. package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
  14. package/corpus/templates/clips/desktop/src/styles.css +110 -0
  15. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
  16. package/corpus/templates/clips/shared/recording-capture.ts +18 -13
  17. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
  20. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
  21. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
  22. package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
  23. package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
  24. package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
  25. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +13 -7
  26. package/corpus/templates/design/app/hooks/use-navigation-state.ts +18 -21
  27. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +2 -5
  28. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +4 -12
  29. package/corpus/templates/design/app/pages/design-editor/types.ts +38 -1
  30. package/corpus/templates/design/changelog/2026-08-18-advanced-design-editor-panels-hidden-by-default.md +6 -0
  31. package/corpus/templates/design/playwright.config.ts +19 -2
  32. package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
  33. package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
  34. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  35. package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
  36. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  37. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
  38. package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
  39. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  40. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
  41. package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
  42. package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
  43. package/corpus/templates/slides/actions/add-slide.ts +6 -2
  44. package/corpus/templates/slides/actions/create-deck.ts +2 -1
  45. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
  46. package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
  47. package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
  48. package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
  49. package/corpus/templates/slides/server/handlers/assets.ts +23 -12
  50. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
  51. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
  52. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
  53. package/corpus/templates/slides/server/handlers/share.ts +1 -1
  54. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  55. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
  56. package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
  57. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
  58. package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
  59. package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
  60. package/corpus/templates/slides/shared/api.ts +7 -1
  61. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  62. package/dist/agent/engine/builder-engine.js +15 -8
  63. package/dist/agent/engine/translate-anthropic.js +27 -2
  64. package/dist/agent/production-agent.d.ts +17 -2
  65. package/dist/agent/production-agent.js +48 -17
  66. package/dist/agent/thread-data-builder.d.ts +1 -0
  67. package/dist/agent/thread-data-builder.js +6 -4
  68. package/dist/agent/tool-approval-store.d.ts +10 -1
  69. package/dist/agent/tool-approval-store.js +19 -2
  70. package/dist/agent/types.d.ts +9 -0
  71. package/dist/client/AssistantChat.js +14 -6
  72. package/dist/client/EnvironmentBadge.d.ts +15 -0
  73. package/dist/client/EnvironmentBadge.js +79 -0
  74. package/dist/client/MultiTabAssistantChat.js +73 -17
  75. package/dist/client/agent-chat-adapter.js +11 -1
  76. package/dist/client/api-path.js +14 -1
  77. package/dist/client/app-providers.js +2 -1
  78. package/dist/client/chat/tool-call-display.d.ts +12 -2
  79. package/dist/client/chat/tool-call-display.js +17 -7
  80. package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
  81. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
  82. package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
  83. package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
  84. package/dist/client/onboarding/use-onboarding.d.ts +8 -1
  85. package/dist/client/onboarding/use-onboarding.js +22 -7
  86. package/dist/client/session-replay.js +58 -15
  87. package/dist/client/settings/SettingsTabsPage.js +19 -6
  88. package/dist/client/sse-event-processor.d.ts +7 -0
  89. package/dist/client/sse-event-processor.js +4 -1
  90. package/dist/client/ui/index.d.ts +1 -0
  91. package/dist/client/ui/index.js +1 -0
  92. package/dist/client/use-action.js +26 -0
  93. package/dist/collab/awareness.d.ts +2 -2
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/config.d.ts +15 -0
  96. package/dist/config.js +53 -0
  97. package/dist/extensions/fetch-tool.js +8 -2
  98. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  99. package/dist/mcp/embed-app.js +1 -1
  100. package/dist/observability/routes.d.ts +5 -5
  101. package/dist/progress/routes.d.ts +1 -1
  102. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  103. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  104. package/dist/provider-api/index.d.ts +10 -0
  105. package/dist/provider-api/index.js +45 -9
  106. package/dist/secrets/routes.d.ts +9 -9
  107. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  108. package/dist/server/auth.js +10 -3
  109. package/dist/server/poll.js +7 -3
  110. package/dist/server/sentry.d.ts +1 -1
  111. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  112. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  113. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  114. package/dist/vite/client.d.ts +5 -0
  115. package/dist/vite/client.js +64 -2
  116. package/dist/workspace-files/actions.d.ts +1 -8
  117. package/dist/workspace-files/actions.js +5 -21
  118. package/dist/workspace-files/index.d.ts +1 -1
  119. package/dist/workspace-files/index.js +1 -1
  120. package/dist/workspace-files/store.d.ts +18 -0
  121. package/dist/workspace-files/store.js +24 -3
  122. package/package.json +3 -3
  123. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  124. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  125. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
@@ -1,7 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Skeleton } from "@agent-native/toolkit/ui/skeleton";
3
3
  import { IconArrowRight, IconBrandGithub, IconCheck, IconExternalLink, IconInfoCircle, IconKey, IconLoader2, IconSearch, } from "@tabler/icons-react";
4
- import React, { useMemo, useState } from "react";
4
+ import React, { useCallback, useMemo, useState } from "react";
5
5
  import { docsUrl } from "../../shared/docs-url.js";
6
6
  import { appPath } from "../api-path.js";
7
7
  import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js";
@@ -28,7 +28,14 @@ const BUILDER_MORE_SERVICES = [
28
28
  ];
29
29
  export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegrations(), initialFirstRun = false, } = {}) {
30
30
  const previewMode = useOnboardingPreviewMode();
31
- const { firstRun, loading, error, profile, completeFirstRun } = useOnboarding({ preview: previewMode, initialFirstRun });
31
+ const { firstRun, loading, error, profile, completeFirstRun, completeFirstRunError, } = useOnboarding({ preview: previewMode, initialFirstRun });
32
+ // completeFirstRun() rejects on failure — swallow it here so a Skip/
33
+ // Continue click never becomes an unhandled rejection; completeFirstRunError
34
+ // (rendered below) is the real signal, and the user stays on this screen
35
+ // to retry instead of being bounced to an unrelated error screen.
36
+ const finishOnboarding = useCallback(() => {
37
+ completeFirstRun().catch(() => { });
38
+ }, [completeFirstRun]);
32
39
  const [screen, setScreen] = useState("intro");
33
40
  const [extensionIndex, setExtensionIndex] = useState(0);
34
41
  const [integrationQuery, setIntegrationQuery] = useState("");
@@ -86,15 +93,15 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
86
93
  trackingFlow: "connect_llm",
87
94
  });
88
95
  };
89
- const handleOpenSettings = async () => {
96
+ const handleOpenSettings = () => {
90
97
  window.dispatchEvent(new CustomEvent("agent-panel:open-settings", {
91
98
  detail: { section: "integrations" },
92
99
  }));
93
- await completeFirstRun();
100
+ finishOnboarding();
94
101
  };
95
102
  const handleFinish = () => {
96
103
  if (extensions.length === 0) {
97
- void completeFirstRun();
104
+ finishOnboarding();
98
105
  return;
99
106
  }
100
107
  setExtensionIndex(0);
@@ -155,7 +162,7 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
155
162
  if (screen === "extension") {
156
163
  const extension = extensions[extensionIndex];
157
164
  if (!extension) {
158
- void completeFirstRun();
165
+ finishOnboarding();
159
166
  return null;
160
167
  }
161
168
  const Extension = extension.component;
@@ -164,9 +171,9 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
164
171
  setExtensionIndex((current) => current + 1);
165
172
  return;
166
173
  }
167
- void completeFirstRun();
174
+ finishOnboarding();
168
175
  };
169
- return (_jsx(Extension, { onComplete: advanceExtension, onSkip: () => void completeFirstRun() }));
176
+ return (_jsxs(_Fragment, { children: [_jsx(Extension, { onComplete: advanceExtension, onSkip: finishOnboarding }), completeFirstRunError && (_jsx(FirstRunCompletionError, { message: completeFirstRunError, onRetry: finishOnboarding }))] }));
170
177
  }
171
178
  if (screen === "intro") {
172
179
  return (_jsx(OnboardingShell, { profile: profile, screen: "intro", children: _jsxs("div", { className: "mx-auto flex w-full max-w-lg flex-col items-center text-center", children: [_jsxs("h1", { className: "text-3xl font-semibold tracking-[-0.05em] sm:text-4xl", children: ["Free forever.", _jsx("br", {}), _jsx("span", { className: "text-primary", children: "Open source for life." })] }), _jsxs("div", { className: "mt-7 grid w-full gap-2 text-left sm:grid-cols-3", children: [_jsxs("div", { className: "rounded-lg bg-muted/35 px-3 py-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Fully customizable" }), _jsx("p", { className: "mt-1 text-[11px] leading-4 text-muted-foreground", children: "Change the UI, code, and behavior." })] }), _jsxs("div", { className: "rounded-lg bg-muted/35 px-3 py-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Bring your own keys" }), _jsx("p", { className: "mt-1 text-[11px] leading-4 text-muted-foreground", children: "Use your own providers and accounts." })] }), _jsxs("div", { className: "rounded-lg bg-muted/35 px-3 py-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Build your own" }), _jsx("p", { className: "mt-1 text-[11px] leading-4 text-muted-foreground", children: "Mix and match toolkit pieces in your own apps." })] })] }), _jsxs("button", { type: "button", className: cn(primaryButtonClass, "mt-6"), onClick: () => setScreen("choice"), children: ["Continue", _jsx(IconArrowRight, { size: 15 })] }), _jsxs("a", { href: "https://github.com/builderio/agent-native", target: "_blank", rel: "noreferrer", className: "mt-3 inline-flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(IconBrandGithub, { size: 14 }), _jsx("span", { children: "View source" }), _jsx(IconExternalLink, { size: 13 })] })] }) }));
@@ -204,29 +211,29 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
204
211
  if (screen === "connecting") {
205
212
  return (_jsx(OnboardingShell, { profile: profile, screen: "choice", children: _jsxs("div", { className: "mx-auto flex w-full max-w-md flex-col items-center text-center", children: [_jsx("div", { className: "flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(IconLoader2, { className: "animate-spin", size: 19 }) }), _jsx("h1", { className: "mt-5 text-xl font-semibold tracking-[-0.04em]", children: "Connecting Builder.io free credits" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Finish the one-click connection in the new window." }), _jsxs("div", { className: "mt-7 w-full rounded-xl bg-muted/35 p-4 text-left", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Skeleton, { className: "h-3 w-28" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" })] }), _jsx(Skeleton, { className: "mt-4 h-8 w-full" }), _jsxs("div", { className: "mt-3 grid grid-cols-3 gap-2", children: [_jsx(Skeleton, { className: "h-7 w-full" }), _jsx(Skeleton, { className: "h-7 w-full" }), _jsx(Skeleton, { className: "h-7 w-full" })] })] }), connectFlow.error && (_jsxs("div", { className: "mt-4 flex flex-col items-center gap-2", children: [_jsx("p", { className: "text-xs text-destructive", children: connectFlow.error }), _jsx("button", { type: "button", className: secondaryButtonClass, onClick: () => setScreen("choice"), children: "Try again" })] }))] }) }));
206
213
  }
207
- return (_jsx(OnboardingShell, { profile: profile, screen: "ready", children: _jsxs("div", { className: "mx-auto flex w-full max-w-2xl flex-col items-center text-center", children: [_jsx("div", { className: "flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary", children: _jsx(IconCheck, { size: 20 }) }), _jsx("h1", { className: "mt-5 text-xl font-semibold tracking-[-0.04em]", children: "Your agent is ready." }), _jsx("p", { className: "mt-2 max-w-md text-sm leading-6 text-muted-foreground", children: "Start with a chat, then connect more tools whenever you need them." }), _jsx("div", { className: "mt-7 grid w-full gap-2 text-left sm:grid-cols-3", children: (skipIntegrations
208
- ? [
209
- [
210
- "Workflow actions",
211
- "Use the app's buttons and sidebar to run the workflow.",
212
- ],
213
- [
214
- "AI sidebar",
215
- "Ask the agent to review or refine a step in context.",
216
- ],
217
- [
218
- "Flexible providers",
219
- "Use Builder.io free credits or your own keys.",
220
- ],
221
- ]
222
- : [
223
- ["Chat + actions", "Ask your agent to work across the app."],
224
- ["Agent integrations", "Connect tools from Settings anytime."],
225
- [
226
- "Flexible providers",
227
- "Use Builder.io free credits or your own keys.",
228
- ],
229
- ]).map(([title, description]) => (_jsxs("div", { className: "rounded-xl bg-muted/35 px-4 py-4", children: [_jsx("span", { className: "flex size-7 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(IconCheck, { size: 14 }) }), _jsx("p", { className: "mt-3 text-sm font-medium", children: title }), _jsx("p", { className: "mt-1 text-xs leading-5 text-muted-foreground", children: description })] }, title))) }), _jsxs("button", { type: "button", "data-testid": "first-run-open-app", className: cn(primaryButtonClass, "mt-7"), onClick: handleFinish, children: ["Open app", _jsx(IconArrowRight, { size: 15 })] })] }) }));
214
+ return (_jsxs(OnboardingShell, { profile: profile, screen: "ready", children: [_jsxs("div", { className: "mx-auto flex w-full max-w-2xl flex-col items-center text-center", children: [_jsx("div", { className: "flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary", children: _jsx(IconCheck, { size: 20 }) }), _jsx("h1", { className: "mt-5 text-xl font-semibold tracking-[-0.04em]", children: "Your agent is ready." }), _jsx("p", { className: "mt-2 max-w-md text-sm leading-6 text-muted-foreground", children: "Start with a chat, then connect more tools whenever you need them." }), _jsx("div", { className: "mt-7 grid w-full gap-2 text-left sm:grid-cols-3", children: (skipIntegrations
215
+ ? [
216
+ [
217
+ "Workflow actions",
218
+ "Use the app's buttons and sidebar to run the workflow.",
219
+ ],
220
+ [
221
+ "AI sidebar",
222
+ "Ask the agent to review or refine a step in context.",
223
+ ],
224
+ [
225
+ "Flexible providers",
226
+ "Use Builder.io free credits or your own keys.",
227
+ ],
228
+ ]
229
+ : [
230
+ ["Chat + actions", "Ask your agent to work across the app."],
231
+ ["Agent integrations", "Connect tools from Settings anytime."],
232
+ [
233
+ "Flexible providers",
234
+ "Use Builder.io free credits or your own keys.",
235
+ ],
236
+ ]).map(([title, description]) => (_jsxs("div", { className: "rounded-xl bg-muted/35 px-4 py-4", children: [_jsx("span", { className: "flex size-7 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(IconCheck, { size: 14 }) }), _jsx("p", { className: "mt-3 text-sm font-medium", children: title }), _jsx("p", { className: "mt-1 text-xs leading-5 text-muted-foreground", children: description })] }, title))) }), _jsxs("button", { type: "button", "data-testid": "first-run-open-app", className: cn(primaryButtonClass, "mt-7"), onClick: handleFinish, children: ["Open app", _jsx(IconArrowRight, { size: 15 })] })] }), completeFirstRunError && (_jsx(FirstRunCompletionError, { message: completeFirstRunError, onRetry: finishOnboarding }))] }));
230
237
  }
231
238
  function OnboardingShell({ profile, screen, footer, children, }) {
232
239
  return (_jsxs("div", { className: "fixed inset-0 z-[100] flex h-full min-h-0 flex-col bg-background text-foreground", "data-onboarding-screen": screen, role: "dialog", "aria-modal": "true", "aria-label": `${profile?.appName ?? "Your app"} setup`, children: [_jsx("div", { className: "h-0.5 shrink-0 bg-muted", "data-testid": "onboarding-progress", children: _jsx("div", { className: "h-full bg-primary transition-[width] duration-200", style: {
@@ -266,6 +273,13 @@ function CapabilityInfoButton({ capability, ariaLabel, }) {
266
273
  return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": ariaLabel, className: "inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(IconInfoCircle, { size: 13 }) }) }), _jsx(TooltipContent, { side: "top", className: "max-w-xs text-xs", children: capability.why })] }));
267
274
  }
268
275
  const primaryButtonClass = "inline-flex min-h-9 items-center justify-center gap-2 rounded-lg bg-primary px-4 text-xs font-medium text-primary-foreground shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
276
+ /** Inline failure signal for a failed completeFirstRun() call — keeps the
277
+ * user on their current screen with a way forward, instead of swapping to
278
+ * an unrelated full-screen error or leaving Skip/Continue looking like it
279
+ * did nothing. */
280
+ function FirstRunCompletionError({ message, onRetry, }) {
281
+ return (_jsxs("div", { className: "fixed inset-x-0 bottom-4 z-50 mx-auto flex w-fit max-w-[90vw] items-center gap-3 rounded-lg border border-destructive/30 bg-background px-4 py-2 text-xs shadow-lg", children: [_jsx("span", { className: "text-destructive", children: message }), _jsx("button", { type: "button", className: "font-medium underline underline-offset-2 hover:no-underline", onClick: onRetry, children: "Try again" })] }));
282
+ }
269
283
  const secondaryButtonClass = "inline-flex min-h-9 items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
270
284
  function compareUrl(value) {
271
285
  try {
@@ -30,8 +30,15 @@ export interface UseOnboardingResult {
30
30
  reopen: () => Promise<void>;
31
31
  /** True until the post-signup full-screen flow is completed. */
32
32
  firstRun: boolean;
33
- /** Clear the post-signup full-screen flow marker. */
33
+ /** Clear the post-signup full-screen flow marker. Rejects instead of
34
+ * resolving silently when the server call fails — see
35
+ * `completeFirstRunError` for the message to show the user. */
34
36
  completeFirstRun: () => Promise<void>;
37
+ /** Set when the last `completeFirstRun()` call failed. Cleared on the next
38
+ * attempt (success or failure). Distinct from `error` (the initial steps
39
+ * load failure) so a failed Skip/Continue doesn't swap the whole screen
40
+ * for an unrelated "could not load" message. */
41
+ completeFirstRunError: string | null;
35
42
  }
36
43
  export declare function useOnboarding(options?: {
37
44
  preview?: boolean;
@@ -10,6 +10,7 @@ export function useOnboarding(options = {}) {
10
10
  const [error, setError] = useState(null);
11
11
  const [dismissed, setDismissed] = useState(false);
12
12
  const [firstRun, setFirstRun] = useState(preview || initialFirstRun);
13
+ const [completeFirstRunError, setCompleteFirstRunError] = useState(null);
13
14
  const mountedRef = useRef(true);
14
15
  useEffect(() => {
15
16
  setFirstRun(preview || initialFirstRun);
@@ -116,14 +117,27 @@ export function useOnboarding(options = {}) {
116
117
  }
117
118
  return;
118
119
  }
119
- const response = await fetch(agentNativePath("/_agent-native/onboarding/first-run/complete"), {
120
- method: "POST",
121
- headers: { "Content-Type": "application/json" },
122
- body: "{}",
123
- });
120
+ setCompleteFirstRunError(null);
121
+ // Both a rejected fetch (offline, dropped connection) and a non-ok
122
+ // response are real failures — neither may look like success to the
123
+ // caller, so both throw instead of returning as if the step advanced.
124
+ let response;
125
+ try {
126
+ response = await fetch(agentNativePath("/_agent-native/onboarding/first-run/complete"), {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: "{}",
130
+ });
131
+ }
132
+ catch (e) {
133
+ const message = e instanceof Error ? e.message : "first-run completion request failed";
134
+ setCompleteFirstRunError(message);
135
+ throw e instanceof Error ? e : new Error(message);
136
+ }
124
137
  if (!response.ok) {
125
- setError(`first-run completion: ${response.status}`);
126
- return;
138
+ const message = `first-run completion failed: ${response.status}`;
139
+ setCompleteFirstRunError(message);
140
+ throw new Error(message);
127
141
  }
128
142
  setFirstRun(false);
129
143
  if (typeof window !== "undefined") {
@@ -153,5 +167,6 @@ export function useOnboarding(options = {}) {
153
167
  reopen,
154
168
  firstRun,
155
169
  completeFirstRun,
170
+ completeFirstRunError,
156
171
  };
157
172
  }
@@ -1040,6 +1040,31 @@ function isDefinitiveReplayUploadClientError(status) {
1040
1040
  return status === 400 || status === 409 || status === 413 || status === 422;
1041
1041
  }
1042
1042
  const MAX_TRANSIENT_REPLAY_CLIENT_FAILURES = 3;
1043
+ // A hung upload never settles, so `state.flushing` stays set, every later flush
1044
+ // early-returns while still queueing rrweb events, and the queue grows until the
1045
+ // session ends. Time the request out so the failure is observable and the lock
1046
+ // is released.
1047
+ const REPLAY_UPLOAD_TIMEOUT_MS = 15_000;
1048
+ function startReplayUploadTimeout() {
1049
+ // Use an explicit controller/timer pair instead of relying only on
1050
+ // AbortSignal.timeout: the controller is available in older supported
1051
+ // browsers, and the timer can always be cleared when a normal upload
1052
+ // settles so completed uploads do not leave timeout work behind.
1053
+ const controller = typeof AbortController === "undefined" ? undefined : new AbortController();
1054
+ let rejectTimeout;
1055
+ const promise = new Promise((_, reject) => {
1056
+ rejectTimeout = reject;
1057
+ });
1058
+ const timer = setTimeout(() => {
1059
+ controller?.abort();
1060
+ rejectTimeout(new Error("Session replay upload timed out"));
1061
+ }, REPLAY_UPLOAD_TIMEOUT_MS);
1062
+ return {
1063
+ signal: controller?.signal,
1064
+ promise,
1065
+ done: () => clearTimeout(timer),
1066
+ };
1067
+ }
1043
1068
  function isTransientReplayUploadClientError(status) {
1044
1069
  return status === 401 || status === 403 || status === 404;
1045
1070
  }
@@ -1048,12 +1073,21 @@ async function sendReplayUpload(options, body, callbacks = {}) {
1048
1073
  const canUseKeepalive = canUseReplayKeepalive(body);
1049
1074
  if (canUseKeepalive)
1050
1075
  callbacks.beforeKeepaliveUpload?.();
1051
- const response = await fetch(options.endpoint, {
1052
- method: "POST",
1053
- body,
1054
- keepalive: canUseKeepalive,
1055
- headers: { "Content-Type": "text/plain;charset=UTF-8" },
1056
- });
1076
+ const timeout = startReplayUploadTimeout();
1077
+ let response;
1078
+ try {
1079
+ const request = fetch(options.endpoint, {
1080
+ method: "POST",
1081
+ body,
1082
+ keepalive: canUseKeepalive,
1083
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
1084
+ signal: timeout.signal,
1085
+ });
1086
+ response = await Promise.race([request, timeout.promise]);
1087
+ }
1088
+ finally {
1089
+ timeout.done();
1090
+ }
1057
1091
  if (!response.ok) {
1058
1092
  throw new ReplayUploadHttpError(response.status);
1059
1093
  }
@@ -1063,15 +1097,24 @@ async function sendReplayUpload(options, body, callbacks = {}) {
1063
1097
  const canUseKeepalive = canUseReplayKeepalive(upload.body);
1064
1098
  if (canUseKeepalive)
1065
1099
  callbacks.beforeKeepaliveUpload?.();
1066
- const response = await fetch(options.endpoint, {
1067
- method: "POST",
1068
- body: upload.body,
1069
- keepalive: canUseKeepalive,
1070
- headers: {
1071
- ...upload.headers,
1072
- "X-Agent-Native-Analytics-Key": options.publicKey,
1073
- },
1074
- });
1100
+ const timeout = startReplayUploadTimeout();
1101
+ let response;
1102
+ try {
1103
+ const request = fetch(options.endpoint, {
1104
+ method: "POST",
1105
+ body: upload.body,
1106
+ keepalive: canUseKeepalive,
1107
+ headers: {
1108
+ ...upload.headers,
1109
+ "X-Agent-Native-Analytics-Key": options.publicKey,
1110
+ },
1111
+ signal: timeout.signal,
1112
+ });
1113
+ response = await Promise.race([request, timeout.promise]);
1114
+ }
1115
+ finally {
1116
+ timeout.done();
1117
+ }
1075
1118
  if (!response.ok) {
1076
1119
  throw new ReplayUploadHttpError(response.status);
1077
1120
  }
@@ -3,6 +3,7 @@ import { Tabs, useDesignSystem } from "@agent-native/toolkit/design-system";
3
3
  import { IconArrowUpRight, IconHistory, IconSearch, IconSettings, IconUserCircle, IconUsers, IconX, } from "@tabler/icons-react";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
5
5
  import { Link } from "react-router";
6
+ import { appBasePath, appPath } from "../../client/api-path.js";
6
7
  import { buildSettingsRoute } from "../../navigation/index.js";
7
8
  import { cn } from "../utils.js";
8
9
  function normalizeTabId(value) {
@@ -70,12 +71,13 @@ function resolveTabId(tabs, value) {
70
71
  function activeTabFromLocation(tabs, defaultTab) {
71
72
  if (typeof window === "undefined")
72
73
  return defaultTab;
74
+ const pathname = appLocalPathname();
73
75
  const settingsPrefix = "/settings";
74
- if (window.location.pathname === settingsPrefix) {
76
+ if (pathname === settingsPrefix) {
75
77
  return resolveTabId(tabs, window.location.hash) ?? defaultTab;
76
78
  }
77
- if (window.location.pathname.startsWith(`${settingsPrefix}/`)) {
78
- const segments = window.location.pathname
79
+ if (pathname.startsWith(`${settingsPrefix}/`)) {
80
+ const segments = pathname
79
81
  .slice(`${settingsPrefix}/`.length)
80
82
  .split("/")
81
83
  .filter(Boolean)
@@ -95,6 +97,17 @@ function activeTabFromLocation(tabs, defaultTab) {
95
97
  }
96
98
  return resolveTabId(tabs, window.location.hash) ?? defaultTab;
97
99
  }
100
+ function appLocalPathname() {
101
+ if (typeof window === "undefined")
102
+ return "/";
103
+ const pathname = window.location.pathname;
104
+ const basePath = appBasePath();
105
+ if (basePath &&
106
+ (pathname === basePath || pathname.startsWith(`${basePath}/`))) {
107
+ return pathname.slice(basePath.length) || "/";
108
+ }
109
+ return pathname;
110
+ }
98
111
  function buildSettingsEntryRoute(tabId, section) {
99
112
  const normalizedSection = section?.replace(/^#/, "").trim();
100
113
  if (!normalizedSection || normalizedSection === tabId) {
@@ -112,7 +125,7 @@ function updateRouteForTab(tabId, section) {
112
125
  if (typeof window === "undefined")
113
126
  return;
114
127
  const route = buildSettingsEntryRoute(tabId, section);
115
- window.history.pushState(null, "", `${route}${window.location.search}`);
128
+ window.history.pushState(null, "", `${appPath(route)}${window.location.search}`);
116
129
  }
117
130
  function isEditableElement(element) {
118
131
  if (!(element instanceof HTMLElement))
@@ -233,7 +246,7 @@ export function SettingsTabsPage({ general, account, team, whatsNew, extraTabs =
233
246
  const fromPath = activeTabFromLocation(tabs, fallbackTab);
234
247
  if (fromPath)
235
248
  setInternalTab(fromPath);
236
- if (window.location.pathname.startsWith("/settings/"))
249
+ if (appLocalPathname().startsWith("/settings/"))
237
250
  return;
238
251
  const hashValue = window.location.hash.replace(/^#/, "");
239
252
  const fromHash = resolveTabId(tabs, hashValue);
@@ -254,7 +267,7 @@ export function SettingsTabsPage({ general, account, team, whatsNew, extraTabs =
254
267
  if (!isControlled)
255
268
  return;
256
269
  const syncControlledLocation = () => {
257
- const fromPath = window.location.pathname.startsWith("/settings/")
270
+ const fromPath = appLocalPathname().startsWith("/settings/")
258
271
  ? activeTabFromLocation(tabs, defaultTab)
259
272
  : null;
260
273
  const hashValue = window.location.hash.replace(/^#/, "");
@@ -39,10 +39,15 @@ export type ContentPart = {
39
39
  * call (opt-in `needsApproval` actions). The action did NOT run; the UI
40
40
  * renders an Approve/Deny affordance. `approvalKey` is echoed back in
41
41
  * `approvedToolCalls` to approve, `dismissed` records a local Deny.
42
+ * `askId` identifies THIS gate hit; it changes when a failed resume
43
+ * re-emits `approval_required` for the same call, which is how the UI
44
+ * tells that apart from the same ask simply re-rendering (see
45
+ * `ApprovalAffordance` in chat/tool-call-display.tsx).
42
46
  */
43
47
  approval?: {
44
48
  approvalKey: string;
45
49
  dismissed?: boolean;
50
+ askId?: string;
46
51
  };
47
52
  /**
48
53
  * Structured metadata from the coding-tools executor side-channel.
@@ -70,6 +75,8 @@ export interface SSEEvent {
70
75
  approvalKey?: string;
71
76
  /** Model-side tool-call id for `approval_required` (mirrors AgentChatEvent). */
72
77
  toolCallId?: string;
78
+ /** Identifies this `approval_required` gate hit (mirrors AgentChatEvent). */
79
+ askId?: string;
73
80
  error?: string;
74
81
  seq?: number;
75
82
  agent?: string;
@@ -1182,7 +1182,10 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
1182
1182
  if (idx >= 0) {
1183
1183
  const part = content[idx];
1184
1184
  if (part.type === "tool-call") {
1185
- part.approval = { approvalKey };
1185
+ part.approval = {
1186
+ approvalKey,
1187
+ ...(ev.askId ? { askId: ev.askId } : {}),
1188
+ };
1186
1189
  }
1187
1190
  }
1188
1191
  }
@@ -12,6 +12,7 @@ export { ErrorBoundary } from "../ErrorBoundary.js";
12
12
  export { ClientOnly } from "../ClientOnly.js";
13
13
  export { DefaultSpinner } from "../DefaultSpinner.js";
14
14
  export { RuntimeConfigNotice } from "../RuntimeConfigNotice.js";
15
+ export { EnvironmentBadge, buildEnvironmentUrl, isBuilderIoEmployee, resolveEnvironmentChannel, resolveEnvironmentTargets, type EnvironmentBadgeTargets, } from "../EnvironmentBadge.js";
15
16
  export { RouteTransitionIndicator, ROUTE_TRANSITION_INDICATOR_DELAY_MS, } from "../RouteTransitionIndicator.js";
16
17
  export { applyEmbeddedThemeUpdate, buildEmbeddedThemeUpdate, EMBEDDED_THEME_CHANGE_EVENT, EMBEDDED_THEME_UPDATE_MESSAGE, getThemeInitScript, parseEmbeddedThemeUpdate, themeInitScript, type EmbeddedThemeUpdate, type NormalizedEmbeddedThemeUpdate, type ResolvedTheme, type ThemePreference, } from "../theme.js";
17
18
  export { APPEARANCE_PRESETS, applyAppearance, getStoredAppearance, useAppearance, useAppearanceSync, type AppearancePresetId, } from "../appearance.js";
@@ -12,6 +12,7 @@ export { ErrorBoundary } from "../ErrorBoundary.js";
12
12
  export { ClientOnly } from "../ClientOnly.js";
13
13
  export { DefaultSpinner } from "../DefaultSpinner.js";
14
14
  export { RuntimeConfigNotice } from "../RuntimeConfigNotice.js";
15
+ export { EnvironmentBadge, buildEnvironmentUrl, isBuilderIoEmployee, resolveEnvironmentChannel, resolveEnvironmentTargets, } from "../EnvironmentBadge.js";
15
16
  export { RouteTransitionIndicator, ROUTE_TRANSITION_INDICATOR_DELAY_MS, } from "../RouteTransitionIndicator.js";
16
17
  export { applyEmbeddedThemeUpdate, buildEmbeddedThemeUpdate, EMBEDDED_THEME_CHANGE_EVENT, EMBEDDED_THEME_UPDATE_MESSAGE, getThemeInitScript, parseEmbeddedThemeUpdate, themeInitScript, } from "../theme.js";
17
18
  export { APPEARANCE_PRESETS, applyAppearance, getStoredAppearance, useAppearance, useAppearanceSync, } from "../appearance.js";
@@ -52,6 +52,11 @@ function isActionTimeout(error) {
52
52
  typeof error === "object" &&
53
53
  error.timedOut === true);
54
54
  }
55
+ function isActionMethodMismatch(error) {
56
+ return (!!error &&
57
+ typeof error === "object" &&
58
+ error.code === "action_method_mismatch");
59
+ }
55
60
  /** @internal exported for tests */
56
61
  export function defaultActionQueryRetry(failureCount, error) {
57
62
  if (isAuthFailure(error))
@@ -60,6 +65,9 @@ export function defaultActionQueryRetry(failureCount, error) {
60
65
  // silently retrying would multiply that wait. Surface it instead.
61
66
  if (isActionTimeout(error))
62
67
  return false;
68
+ // Wrong verb is deterministic — retrying sends the same wrong verb again.
69
+ if (isActionMethodMismatch(error))
70
+ return false;
63
71
  if (isBrowserResourceExhaustion(error))
64
72
  return false;
65
73
  // Network-level failures never carry an HTTP `status` (actionFetch only
@@ -338,6 +346,24 @@ async function performActionFetch(name, method, params, options) {
338
346
  (raw && raw.slice(0, 200)) ||
339
347
  res.statusText ||
340
348
  `HTTP ${res.status}`;
349
+ // mountActionRoutes only ever returns 405 for one reason: the verb this
350
+ // call sent doesn't match the action's declared `http.method`. The client
351
+ // has no way to look that method up ahead of time (defineAction lives in
352
+ // server-only modules), so callers routinely default to POST and hit this
353
+ // blind. Name the action and the required method instead of leaving a
354
+ // bare 405 for the caller to reverse-engineer from a "Use X" string.
355
+ if (res.status === 405) {
356
+ const requiredMethod = /\bUse (GET|POST|PUT|DELETE)\b/.exec(message)?.[1];
357
+ const error = new Error(`Action ${name} was called with ${method}, but it declares ` +
358
+ `http: { method: "${requiredMethod ?? "?"}" }. Pass { method: "${requiredMethod ?? "..."}" } ` +
359
+ `to this call (or use the hook that defaults to it) to match the action's declared method.`);
360
+ error.status = 405;
361
+ error.code = "action_method_mismatch";
362
+ error.sentMethod = method;
363
+ if (requiredMethod)
364
+ error.requiredMethod = requiredMethod;
365
+ throw error;
366
+ }
341
367
  const error = new Error(`Action ${name} failed: ${message}`);
342
368
  error.status = res.status;
343
369
  throw error;
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
package/dist/config.d.ts CHANGED
@@ -38,6 +38,11 @@ export interface AgentNativeRuntimeConfig {
38
38
  database?: AgentNativeRuntimeDatabaseConfig;
39
39
  environment?: AgentNativeRuntimeEnvironmentConfig;
40
40
  }
41
+ export type AgentNativeDeploymentEnvironment = "local" | "beta" | "production" | "preview";
42
+ export interface AgentNativeDeploymentConfig {
43
+ /** The release lane that produced the currently running client bundle. */
44
+ environment?: AgentNativeDeploymentEnvironment;
45
+ }
41
46
  export interface AgentNativeDiagnosticsConfig {
42
47
  /** Fail a production Vite build when runtime configuration has issues. */
43
48
  failOnBuild?: boolean;
@@ -73,6 +78,7 @@ export interface AgentNativeConfig {
73
78
  version?: typeof AGENT_NATIVE_CONFIG_VERSION;
74
79
  onboarding?: AgentNativeOnboardingConfig;
75
80
  runtime?: AgentNativeRuntimeConfig;
81
+ deployment?: AgentNativeDeploymentConfig;
76
82
  diagnostics?: AgentNativeDiagnosticsConfig;
77
83
  instructions?: AgentNativeInstructionsConfig;
78
84
  translations?: AgentNativeTranslationsConfig;
@@ -99,3 +105,12 @@ export declare function normalizeAgentNativeConfig(input: unknown, source?: stri
99
105
  export declare function mergeAgentNativeConfigs(base: AgentNativeConfig, override: AgentNativeConfig): AgentNativeConfig;
100
106
  export declare function resolveAgentNativeConfig(input: AgentNativeConfigInput | undefined, context: AgentNativeConfigContext): AgentNativeConfig;
101
107
  export declare function resolveFirstRunOnboardingMode(setting: AgentNativeFirstRunOnboardingSetting, context: AgentNativeConfigContext): AgentNativeFirstRunOnboardingMode;
108
+ export declare function isAgentNativeDeploymentEnvironment(value: unknown): value is AgentNativeDeploymentEnvironment;
109
+ /**
110
+ * Resolve the public deployment lane from hosting facts at build time.
111
+ *
112
+ * Netlify exposes `CONTEXT` and `BRANCH` to builds. Keeping this inference in
113
+ * the Vite/config boundary means browser code consumes typed public config and
114
+ * never parses process.env itself.
115
+ */
116
+ export declare function inferAgentNativeDeploymentEnvironment(env: Record<string, string | undefined>, mode?: string): AgentNativeDeploymentEnvironment | undefined;
package/dist/config.js CHANGED
@@ -26,6 +26,7 @@ export function normalizeAgentNativeConfig(input, source = "agent-native config"
26
26
  }
27
27
  const onboardingValue = input.onboarding;
28
28
  const runtimeValue = input.runtime;
29
+ const deploymentValue = input.deployment;
29
30
  const diagnosticsValue = input.diagnostics;
30
31
  const instructionsValue = input.instructions;
31
32
  const translationsValue = input.translations;
@@ -46,6 +47,9 @@ export function normalizeAgentNativeConfig(input, source = "agent-native config"
46
47
  if (runtimeValue !== undefined) {
47
48
  normalized.runtime = normalizeRuntimeConfig(runtimeValue, `${source}.runtime`);
48
49
  }
50
+ if (deploymentValue !== undefined) {
51
+ normalized.deployment = normalizeDeploymentConfig(deploymentValue, `${source}.deployment`);
52
+ }
49
53
  if (diagnosticsValue !== undefined) {
50
54
  normalized.diagnostics = normalizeDiagnosticsConfig(diagnosticsValue, `${source}.diagnostics`);
51
55
  }
@@ -101,6 +105,12 @@ export function mergeAgentNativeConfigs(base, override) {
101
105
  : undefined,
102
106
  }
103
107
  : undefined,
108
+ deployment: base.deployment || override.deployment
109
+ ? {
110
+ ...base.deployment,
111
+ ...override.deployment,
112
+ }
113
+ : undefined,
104
114
  diagnostics: base.diagnostics || override.diagnostics
105
115
  ? {
106
116
  ...base.diagnostics,
@@ -205,6 +215,18 @@ function normalizeRuntimeConfig(value, source) {
205
215
  }
206
216
  return result;
207
217
  }
218
+ function normalizeDeploymentConfig(value, source) {
219
+ if (!isRecord(value)) {
220
+ throw new Error(`${source} must be an object`);
221
+ }
222
+ const environment = value.environment;
223
+ if (environment === undefined)
224
+ return {};
225
+ if (!isAgentNativeDeploymentEnvironment(environment)) {
226
+ throw new Error(`${source}.environment must be "local", "beta", "production", or "preview"`);
227
+ }
228
+ return { environment };
229
+ }
208
230
  function normalizeDiagnosticsConfig(value, source) {
209
231
  if (!isRecord(value)) {
210
232
  throw new Error(`${source} must be an object`);
@@ -338,6 +360,37 @@ function isAgentNativeHarnessRuntime(value) {
338
360
  value === "pi" ||
339
361
  value === "opencode");
340
362
  }
363
+ export function isAgentNativeDeploymentEnvironment(value) {
364
+ return (value === "local" ||
365
+ value === "beta" ||
366
+ value === "production" ||
367
+ value === "preview");
368
+ }
369
+ /**
370
+ * Resolve the public deployment lane from hosting facts at build time.
371
+ *
372
+ * Netlify exposes `CONTEXT` and `BRANCH` to builds. Keeping this inference in
373
+ * the Vite/config boundary means browser code consumes typed public config and
374
+ * never parses process.env itself.
375
+ */
376
+ export function inferAgentNativeDeploymentEnvironment(env, mode) {
377
+ const context = env.CONTEXT?.trim().toLowerCase();
378
+ const branch = env.BRANCH?.trim().toLowerCase();
379
+ if (branch === "production" || context === "production") {
380
+ return "production";
381
+ }
382
+ if (branch === "beta" || (context === "branch-deploy" && branch === "main")) {
383
+ return "beta";
384
+ }
385
+ if (context === "deploy-preview" ||
386
+ context === "branch-deploy" ||
387
+ branch?.startsWith("deploy-preview")) {
388
+ return "preview";
389
+ }
390
+ if (mode === "development")
391
+ return "local";
392
+ return undefined;
393
+ }
341
394
  function isRecord(value) {
342
395
  return !!value && typeof value === "object" && !Array.isArray(value);
343
396
  }