@agent-native/core 0.120.1 → 0.120.2

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 +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/docs/content/extensions.mdx +23 -4
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +4 -0
  6. package/corpus/core/src/agent/run-manager.ts +36 -10
  7. package/corpus/core/src/agent/types.ts +2 -0
  8. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +16 -3
  9. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  10. package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
  11. package/corpus/core/src/deploy/build.ts +3 -0
  12. package/corpus/core/src/server/analytics.ts +154 -25
  13. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  14. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  15. package/corpus/core/src/templates/chat/netlify.toml +1 -0
  16. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  17. package/corpus/core/src/vite/client.ts +6 -0
  18. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
  19. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
  20. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
  21. package/corpus/templates/analytics/AGENTS.md +20 -8
  22. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
  23. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
  24. package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
  25. package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
  26. package/corpus/templates/analytics/actions/view-screen.ts +1 -1
  27. package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
  28. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  29. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  30. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +103 -502
  31. package/corpus/templates/analytics/app/global.css +30 -0
  32. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
  33. package/corpus/templates/analytics/app/i18n-data.ts +80 -0
  34. package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
  35. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
  36. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -53
  37. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +23 -25
  38. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  39. package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
  40. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  41. package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
  42. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
  43. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
  44. package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
  45. package/corpus/templates/analytics/netlify.toml +4 -0
  46. package/corpus/templates/analytics/server/db/schema.ts +4 -0
  47. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
  48. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
  49. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
  50. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
  51. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
  52. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  53. package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
  54. package/corpus/templates/assets/actions/generate-image.ts +28 -1
  55. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +46 -8
  56. package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
  57. package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
  58. package/corpus/templates/assets/netlify.toml +1 -0
  59. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -8
  60. package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
  61. package/corpus/templates/brain/netlify.toml +1 -0
  62. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +43 -5
  63. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
  64. package/corpus/templates/calendar/netlify.toml +1 -0
  65. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  66. package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
  67. package/corpus/templates/chat/netlify.toml +1 -0
  68. package/corpus/templates/clips/app/components/library/library-layout.tsx +52 -6
  69. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
  70. package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  71. package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
  72. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
  73. package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
  74. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
  75. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
  76. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
  77. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
  78. package/corpus/templates/clips/desktop/src/styles.css +35 -3
  79. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
  80. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
  81. package/corpus/templates/clips/netlify.toml +1 -0
  82. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +3 -7
  83. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
  84. package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  85. package/corpus/templates/content/netlify.toml +1 -0
  86. package/corpus/templates/design/app/components/layout/Sidebar.tsx +55 -17
  87. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  88. package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  89. package/corpus/templates/design/netlify.toml +1 -0
  90. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
  91. package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  92. package/corpus/templates/dispatch/netlify.toml +1 -0
  93. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +41 -45
  94. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
  95. package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  96. package/corpus/templates/forms/netlify.toml +1 -0
  97. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +36 -5
  98. package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
  99. package/corpus/templates/macros/netlify.toml +1 -0
  100. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -9
  101. package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
  102. package/corpus/templates/mail/netlify.toml +1 -0
  103. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +38 -5
  104. package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
  105. package/corpus/templates/plan/netlify.toml +1 -0
  106. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +47 -4
  107. package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
  108. package/corpus/templates/slides/netlify.toml +1 -0
  109. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
  110. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
  111. package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
  112. package/corpus/templates/tasks/netlify.toml +1 -0
  113. package/dist/agent/production-agent.d.ts.map +1 -1
  114. package/dist/agent/production-agent.js +4 -0
  115. package/dist/agent/production-agent.js.map +1 -1
  116. package/dist/agent/run-manager.d.ts +9 -0
  117. package/dist/agent/run-manager.d.ts.map +1 -1
  118. package/dist/agent/run-manager.js +25 -10
  119. package/dist/agent/run-manager.js.map +1 -1
  120. package/dist/agent/types.d.ts +2 -2
  121. package/dist/agent/types.d.ts.map +1 -1
  122. package/dist/agent/types.js.map +1 -1
  123. package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
  124. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  125. package/dist/client/extensions/ExtensionsListPage.js +5 -3
  126. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  127. package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
  128. package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
  129. package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
  130. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
  131. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  132. package/dist/client/settings/SettingsPanel.js +10 -1
  133. package/dist/client/settings/SettingsPanel.js.map +1 -1
  134. package/dist/collab/awareness.d.ts +2 -2
  135. package/dist/collab/awareness.d.ts.map +1 -1
  136. package/dist/deploy/build.js +1 -0
  137. package/dist/deploy/build.js.map +1 -1
  138. package/dist/observability/routes.d.ts +3 -3
  139. package/dist/progress/routes.d.ts +1 -1
  140. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  141. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  142. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  143. package/dist/server/analytics.d.ts +9 -4
  144. package/dist/server/analytics.d.ts.map +1 -1
  145. package/dist/server/analytics.js +111 -22
  146. package/dist/server/analytics.js.map +1 -1
  147. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  148. package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
  149. package/dist/templates/chat/netlify.toml +1 -0
  150. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  151. package/dist/vite/client.d.ts.map +1 -1
  152. package/dist/vite/client.js +2 -0
  153. package/dist/vite/client.js.map +1 -1
  154. package/docs/content/extensions.mdx +23 -4
  155. package/package.json +1 -1
  156. package/src/agent/production-agent.ts +4 -0
  157. package/src/agent/run-manager.ts +36 -10
  158. package/src/agent/types.ts +2 -0
  159. package/src/client/extensions/ExtensionsListPage.tsx +16 -3
  160. package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  161. package/src/client/settings/SettingsPanel.tsx +10 -0
  162. package/src/deploy/build.ts +3 -0
  163. package/src/server/analytics.ts +154 -25
  164. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  165. package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  166. package/src/templates/chat/netlify.toml +1 -0
  167. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  168. package/src/vite/client.ts +6 -0
  169. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
  170. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +0 -175
@@ -223,7 +223,7 @@ Pattern across all of these: extensions are about **the moment** the user is in
223
223
 
224
224
  The simple path:
225
225
 
226
- 1. **Click "New Extension"** in the sidebar (or just ask in chat).
226
+ 1. Open **Settings Extensions** (or just ask in chat), then click **New Extension**.
227
227
  2. **Describe what you want in one sentence.** "A sticky-note pad for the contact I'm emailing." "A KPI box for trials started this week."
228
228
  3. **The agent writes it and it appears in your Extensions list, ready to use.**
229
229
 
@@ -617,9 +617,23 @@ Extensions are private to the user who created them by default. To share:
617
617
 
618
618
  Shared extensions have their own URLs and plug into the same share dialog as documents, decks, and dashboards. Slot installs are always personal — sharing an extension means others _can_ install it; it doesn't auto-pin it to their UI.
619
619
 
620
- ## Extensions vs. editing the app code {#vs-app-code}
620
+ ## Should I use an extension or edit the app code? {#vs-app-code}
621
621
 
622
- The framework lets the agent edit the app's source code directly — components, routes, styles. So when should you reach for an extension instead?
622
+ The framework lets the agent edit the app's source code directly — components,
623
+ routes, styles, and business logic. Use this question when either path could
624
+ solve the request:
625
+
626
+ - **Start with an extension** for a self-contained, relatively one-off widget:
627
+ a custom visualization or interaction, a dashboard tile, a standalone
628
+ utility, or an add-on in an existing slot. It is fast, requires no source
629
+ change, build, or deploy, and can be private or shared.
630
+ - **Use app code** for a core feature of the template, a change to native UI,
631
+ layout, routes, or business logic, or anything that should be reused broadly
632
+ across dashboards or users. If the host has no suitable slot or the request
633
+ needs a new slot, use code.
634
+ - **Promote repeated extensions.** If people keep rebuilding the same
635
+ extension, or maintaining copies in many places would be worse than one
636
+ shared implementation, move it into app code as a reusable native feature.
623
637
 
624
638
  | | Extension | App code edit |
625
639
  | --------------------- | ------------------------------------------------- | ------------------------------------ |
@@ -630,7 +644,12 @@ The framework lets the agent edit the app's source code directly — components,
630
644
  | **Scope** | One user (or shared with org) | The entire product, every user |
631
645
  | **Best for** | Personal widgets, custom KPIs, per-team utilities | Core features that ship to all users |
632
646
 
633
- Rule of thumb: **if it's for one user or one team, it's an extension.** If every user of the template should get it, ship it as a real feature.
647
+ Rule of thumb: **if an extension can solve the problem, generally start there.**
648
+ Use code when the extension boundary does not fit or when broad reuse and
649
+ product-wide availability justify a native implementation. For example, a
650
+ one-off custom visualization on a dashboard is a good extension; a new pie,
651
+ multi-bar, or line chart type that should be available across dashboards
652
+ belongs in the template code.
634
653
 
635
654
  If what you actually need is a cached, refreshable data source for an existing chart or table — not a new interactive widget — see [Data Programs](/docs/data-programs) instead; it shares this page's sandbox and security posture but has no UI of its own.
636
655
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.120.1",
3
+ "version": "0.120.2",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -6401,6 +6401,10 @@ export async function chainServerDrivenContinuation(opts: {
6401
6401
  terminalEvent: run.events.at(-1)?.event,
6402
6402
  })
6403
6403
  .catch(() => {});
6404
+ run.continuationTerminalEvent = {
6405
+ type: "auto_continue",
6406
+ reason: continuationReason,
6407
+ };
6404
6408
  } catch (chainErr) {
6405
6409
  // Chain dispatch failed — fail loud so the held row goes terminal
6406
6410
  // instead of spinning. The reaper would also catch it, but this is
@@ -41,6 +41,16 @@ export interface ActiveRun {
41
41
  subscribers: Set<(event: RunEvent) => void>;
42
42
  abort: AbortController;
43
43
  abortReason?: string;
44
+ /**
45
+ * Terminal event to emit when a server-driven continuation has been handed
46
+ * off successfully. The continuation runs outside this process, so the
47
+ * normal loop-level auto_continue event is not sent through this run's
48
+ * `send` callback.
49
+ */
50
+ continuationTerminalEvent?: Extract<
51
+ AgentChatEvent,
52
+ { type: "auto_continue" }
53
+ >;
44
54
  startedAt: number;
45
55
  }
46
56
 
@@ -954,7 +964,17 @@ export function startRun(
954
964
  // /runs/active check while we wait for SQL writes to land.
955
965
  let completionError: unknown = null;
956
966
  let terminalPersistenceError: unknown = null;
957
- const terminalEvent = pendingTerminalEvent?.event ?? null;
967
+ const resolveTerminalEventForCompletion = () => {
968
+ const continuationTerminalEvent = run.continuationTerminalEvent
969
+ ? {
970
+ seq: run.events.length,
971
+ event: run.continuationTerminalEvent,
972
+ }
973
+ : null;
974
+ return continuationTerminalEvent ?? pendingTerminalEvent;
975
+ };
976
+ let terminalEventForCompletion = resolveTerminalEventForCompletion();
977
+ let terminalEvent = terminalEventForCompletion?.event ?? null;
958
978
  if (
959
979
  onComplete &&
960
980
  !(run.status === "aborted" && run.abortReason === "no_progress")
@@ -966,12 +986,12 @@ export function startRun(
966
986
  ? "errored"
967
987
  : run.status;
968
988
  const completionRun: ActiveRun =
969
- pendingTerminalEvent || completionStatus !== run.status
989
+ terminalEventForCompletion || completionStatus !== run.status
970
990
  ? {
971
991
  ...run,
972
992
  status: completionStatus,
973
- events: pendingTerminalEvent
974
- ? [...run.events, pendingTerminalEvent]
993
+ events: terminalEventForCompletion
994
+ ? [...run.events, terminalEventForCompletion]
975
995
  : run.events,
976
996
  }
977
997
  : run;
@@ -986,6 +1006,12 @@ export function startRun(
986
1006
  }
987
1007
  }
988
1008
 
1009
+ // Server-driven continuation is installed by onComplete after the
1010
+ // successor has been dispatched. Resolve the terminal event again so
1011
+ // this chunk emits auto_continue instead of a misleading done event.
1012
+ terminalEventForCompletion = resolveTerminalEventForCompletion();
1013
+ terminalEvent = terminalEventForCompletion?.event ?? null;
1014
+
989
1015
  // 2. Compute final status. If the completion callback threw, we'd
990
1016
  // rather mark the run errored than claim success with incomplete
991
1017
  // thread_data.
@@ -1022,18 +1048,18 @@ export function startRun(
1022
1048
  // re-stamp the seq at emit time (max-seq+1) just below.
1023
1049
  const terminalEvent: AgentChatEvent =
1024
1050
  finalStatus === "completed"
1025
- ? (pendingTerminalEvent?.event ?? { type: "done" })
1026
- : pendingTerminalEvent?.event.type === "error" ||
1027
- pendingTerminalEvent?.event.type === "missing_api_key"
1028
- ? pendingTerminalEvent.event
1029
- : pendingTerminalEvent?.event.type === "auto_continue"
1051
+ ? (terminalEventForCompletion?.event ?? { type: "done" })
1052
+ : terminalEventForCompletion?.event.type === "error" ||
1053
+ terminalEventForCompletion?.event.type === "missing_api_key"
1054
+ ? terminalEventForCompletion.event
1055
+ : terminalEventForCompletion?.event.type === "auto_continue"
1030
1056
  ? // The run was checkpointed at a soft-timeout/loop boundary and
1031
1057
  // is recoverable: the partial turn is in agent_run_events and
1032
1058
  // the continuation run will re-attempt the thread_data save.
1033
1059
  // Even though the completion save failed (finalStatus stays
1034
1060
  // "errored" for SQL/diagnostics), re-emit the auto_continue so
1035
1061
  // the client resumes instead of seeing a dead chat.
1036
- pendingTerminalEvent.event
1062
+ terminalEventForCompletion.event
1037
1063
  : {
1038
1064
  type: "error",
1039
1065
  error: completionError
@@ -167,6 +167,7 @@ export interface AgentChatRequest {
167
167
  continuationReason?:
168
168
  | "run_timeout"
169
169
  | "loop_limit"
170
+ | "max_tokens"
170
171
  | "no_progress"
171
172
  | "stream_ended"
172
173
  | "gateway_timeout"
@@ -371,6 +372,7 @@ export type AgentChatEvent =
371
372
  reason:
372
373
  | "run_timeout"
373
374
  | "loop_limit"
375
+ | "max_tokens"
374
376
  | "no_progress"
375
377
  | "stream_ended"
376
378
  | "gateway_timeout"
@@ -99,7 +99,14 @@ function CreateToolInput({ className }: { className?: string }) {
99
99
  );
100
100
  }
101
101
 
102
- export function ExtensionsListPage() {
102
+ export interface ExtensionsListPageProps {
103
+ /** Skip the standalone extensions navigation state when embedded in Settings. */
104
+ embedded?: boolean;
105
+ }
106
+
107
+ export function ExtensionsListPage({
108
+ embedded = false,
109
+ }: ExtensionsListPageProps = {}) {
103
110
  const t = useT();
104
111
  const [showCreate, setShowCreate] = useState(false);
105
112
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
@@ -111,12 +118,13 @@ export function ExtensionsListPage() {
111
118
  );
112
119
 
113
120
  useEffect(() => {
121
+ if (embedded) return;
114
122
  fetch(agentNativePath("/_agent-native/application-state/navigation"), {
115
123
  method: "PUT",
116
124
  headers: { "Content-Type": "application/json" },
117
125
  body: JSON.stringify({ view: "extensions" }),
118
126
  }).catch(() => {});
119
- }, []);
127
+ }, [embedded]);
120
128
 
121
129
  useEffect(() => {
122
130
  if (typeof window === "undefined") return;
@@ -189,7 +197,12 @@ export function ExtensionsListPage() {
189
197
  };
190
198
 
191
199
  return (
192
- <div className="flex h-full w-full flex-col">
200
+ <div
201
+ className={cn(
202
+ "flex w-full flex-col",
203
+ embedded ? "min-h-[28rem]" : "h-full",
204
+ )}
205
+ >
193
206
  <header className="flex h-12 items-center justify-between border-b px-4 shrink-0">
194
207
  <div className="flex items-center gap-2">
195
208
  <h1 className="text-sm font-semibold">{t("extensions.title")}</h1>
@@ -0,0 +1,24 @@
1
+ import { IconLoader2 } from "@tabler/icons-react";
2
+ import { lazy, Suspense } from "react";
3
+
4
+ const ExtensionsListPage = lazy(() =>
5
+ import("../extensions/ExtensionsListPage.js").then((module) => ({
6
+ default: module.ExtensionsListPage,
7
+ })),
8
+ );
9
+
10
+ export function ExtensionsSettingsContent() {
11
+ return (
12
+ <div className="mx-auto w-full max-w-5xl overflow-hidden rounded-lg border border-border bg-background">
13
+ <Suspense
14
+ fallback={
15
+ <div className="flex min-h-[28rem] items-center justify-center text-muted-foreground">
16
+ <IconLoader2 className="size-5 animate-spin" aria-label="Loading" />
17
+ </div>
18
+ }
19
+ >
20
+ <ExtensionsListPage embedded />
21
+ </Suspense>
22
+ </div>
23
+ );
24
+ }
@@ -29,6 +29,7 @@ import {
29
29
  IconGauge,
30
30
  IconApps,
31
31
  IconUsersGroup,
32
+ IconTool,
32
33
  } from "@tabler/icons-react";
33
34
  import React, {
34
35
  Suspense,
@@ -67,6 +68,7 @@ import {
67
68
  import { AgentsSection } from "./AgentsSection.js";
68
69
  import { AutomationsSection } from "./AutomationsSection.js";
69
70
  import { DemoModeSection } from "./DemoModeSection.js";
71
+ import { ExtensionsSettingsContent } from "./ExtensionsSettingsContent.js";
70
72
  import { SecretsSection } from "./SecretsSection.js";
71
73
  import {
72
74
  SettingsSection,
@@ -3037,6 +3039,14 @@ export function useAgentSettingsTabs(): SettingsTabItem[] {
3037
3039
  />
3038
3040
  ),
3039
3041
  },
3042
+ {
3043
+ id: "extensions",
3044
+ label: "Extensions",
3045
+ icon: IconTool,
3046
+ group: "workspace",
3047
+ keywords: "extensions widgets mini apps tools sandboxed apps",
3048
+ content: <ExtensionsSettingsContent />,
3049
+ },
3040
3050
  ];
3041
3051
  }, [baseProps]);
3042
3052
  }
@@ -3708,6 +3708,9 @@ export default bundle;
3708
3708
  "process.env.AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID": JSON.stringify(
3709
3709
  process.env.GA_MEASUREMENT_ID?.trim() || "",
3710
3710
  ),
3711
+ "process.env.AGENT_NATIVE_BUILD_GTM_CONTAINER_ID": JSON.stringify(
3712
+ process.env.GTM_CONTAINER_ID?.trim() || "",
3713
+ ),
3711
3714
  "process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT": JSON.stringify(
3712
3715
  process.env.CONTEXT?.trim() || "",
3713
3716
  ),
@@ -2,16 +2,21 @@
2
2
  * Opt-in analytics injection for SSR streams.
3
3
  * Supported environment variables:
4
4
  * - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID
5
+ * - `GTM_CONTAINER_ID` — Google Tag Manager web container ID
5
6
  *
6
7
  * Netlify configuration-file env vars are build-time only for serverless
7
8
  * functions, so the Vite/Nitro build paths also bake this public value into
8
9
  * SSR bundles.
9
10
  *
10
11
  * Amplitude and Sentry are initialized client-side via their npm packages
11
- * (see `packages/core/src/client/analytics.ts`). Only GA requires script
12
- * tag injection because the gtag.js loader must be a `<script src>`.
12
+ * (see `packages/core/src/client/analytics.ts`). GTM and GA require script
13
+ * tag injection because their loaders must be `<script>` elements.
13
14
  *
14
- * When set, the corresponding script tags are injected before `</head>`.
15
+ * When GTM is set, its head bootstrap is injected before `</head>` and its
16
+ * noscript fallback immediately after `<body>`. GTM takes precedence over GA
17
+ * so pageviews are not double-counted; configure the GA tag inside GTM.
18
+ * When only GA is set, the corresponding script tags are injected before
19
+ * `</head>`.
15
20
  * When not set, the stream passes through untouched (zero overhead).
16
21
  *
17
22
  * Usage in entry.server.tsx:
@@ -22,6 +27,7 @@
22
27
  */
23
28
 
24
29
  declare const __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: string | undefined;
30
+ declare const __AGENT_NATIVE_BUILD_GTM_CONTAINER_ID__: string | undefined;
25
31
 
26
32
  function normalizeMeasurementId(value: string | undefined): string | null {
27
33
  const trimmed = value?.trim();
@@ -34,6 +40,25 @@ function getViteBakedGaMeasurementId(): string | undefined {
34
40
  : undefined;
35
41
  }
36
42
 
43
+ function normalizeContainerId(value: string | undefined): string | null {
44
+ const trimmed = value?.trim().toUpperCase();
45
+ return trimmed && /^GTM-[A-Z0-9]+$/.test(trimmed) ? trimmed : null;
46
+ }
47
+
48
+ function getViteBakedGtmContainerId(): string | undefined {
49
+ return typeof __AGENT_NATIVE_BUILD_GTM_CONTAINER_ID__ === "string"
50
+ ? __AGENT_NATIVE_BUILD_GTM_CONTAINER_ID__
51
+ : undefined;
52
+ }
53
+
54
+ function getGtmContainerId(): string | null {
55
+ return (
56
+ normalizeContainerId(process.env.GTM_CONTAINER_ID) ||
57
+ normalizeContainerId(process.env.AGENT_NATIVE_BUILD_GTM_CONTAINER_ID) ||
58
+ normalizeContainerId(getViteBakedGtmContainerId())
59
+ );
60
+ }
61
+
37
62
  function getGaMeasurementId(): string | null {
38
63
  return (
39
64
  normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||
@@ -74,6 +99,37 @@ function getGaScript(): string | null {
74
99
  );
75
100
  }
76
101
 
102
+ function getGtmHeadScript(containerId: string): string {
103
+ const jsId = JSON.stringify(containerId);
104
+ return `<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer',${jsId});</script>`;
105
+ }
106
+
107
+ function getGtmBodyFallback(containerId: string): string {
108
+ const src = encodeURIComponent(containerId);
109
+ return `<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${src}" height="0" width="0" style="display:none;visibility:hidden" title="Google Tag Manager"></iframe></noscript>`;
110
+ }
111
+
112
+ type AnalyticsInjection = {
113
+ head: string;
114
+ body: string;
115
+ };
116
+
117
+ function getAnalyticsInjection(): AnalyticsInjection | null {
118
+ const containerId = getGtmContainerId();
119
+ if (containerId) {
120
+ return {
121
+ head: getGtmHeadScript(containerId),
122
+ body: getGtmBodyFallback(containerId),
123
+ };
124
+ }
125
+
126
+ const gaScript = getGaScript();
127
+ return gaScript ? { head: gaScript, body: "" } : null;
128
+ }
129
+
130
+ const HEAD_CLOSE_PATTERN = /<\/head>/i;
131
+ const BODY_OPEN_PATTERN = /<body\b[^>]*>/i;
132
+
77
133
  /**
78
134
  * Add the configured analytics scripts to a complete HTML document.
79
135
  *
@@ -82,38 +138,111 @@ function getGaScript(): string | null {
82
138
  * auth guard and need the same injection path.
83
139
  */
84
140
  export function injectAnalyticsIntoHtml(html: string): string {
85
- const script = getGaScript();
86
- if (!script) return html;
87
- const headCloseIdx = html.indexOf("</head>");
88
- if (headCloseIdx === -1) return html;
89
- return html.slice(0, headCloseIdx) + script + html.slice(headCloseIdx);
141
+ const injection = getAnalyticsInjection();
142
+ if (!injection) return html;
143
+
144
+ const headCloseMatch = HEAD_CLOSE_PATTERN.exec(html);
145
+ if (!headCloseMatch || headCloseMatch.index === undefined) return html;
146
+
147
+ let output =
148
+ html.slice(0, headCloseMatch.index) +
149
+ injection.head +
150
+ html.slice(headCloseMatch.index);
151
+
152
+ if (injection.body) {
153
+ const bodyOpenMatch = BODY_OPEN_PATTERN.exec(output);
154
+ if (bodyOpenMatch && bodyOpenMatch.index !== undefined) {
155
+ const bodyEnd = bodyOpenMatch.index + bodyOpenMatch[0].length;
156
+ output =
157
+ output.slice(0, bodyEnd) + injection.body + output.slice(bodyEnd);
158
+ }
159
+ }
160
+
161
+ return output;
90
162
  }
91
163
 
92
- export function wrapWithAnalytics(body: ReadableStream): ReadableStream {
93
- const scripts = [getGaScript()].filter(Boolean).join("");
94
- if (!scripts) return body;
164
+ export function wrapWithAnalytics(
165
+ body: ReadableStream<Uint8Array>,
166
+ ): ReadableStream<Uint8Array> {
167
+ const injection = getAnalyticsInjection();
168
+ if (!injection) return body;
95
169
 
96
170
  const decoder = new TextDecoder();
97
171
  const encoder = new TextEncoder();
98
- let injected = false;
172
+ let pending = "";
173
+ let headInjected = false;
174
+ let bodyInjected = !injection.body;
99
175
 
100
176
  return body.pipeThrough(
101
- new TransformStream({
177
+ new TransformStream<Uint8Array, Uint8Array>({
102
178
  transform(chunk, controller) {
103
- if (injected) {
104
- controller.enqueue(chunk);
105
- return;
179
+ pending += decoder.decode(chunk, { stream: true });
180
+
181
+ if (!headInjected) {
182
+ const headCloseMatch = HEAD_CLOSE_PATTERN.exec(pending);
183
+ if (!headCloseMatch || headCloseMatch.index === undefined) return;
184
+
185
+ const headEnd = headCloseMatch.index + headCloseMatch[0].length;
186
+ controller.enqueue(
187
+ encoder.encode(
188
+ pending.slice(0, headCloseMatch.index) +
189
+ injection.head +
190
+ pending.slice(headCloseMatch.index, headEnd),
191
+ ),
192
+ );
193
+ pending = pending.slice(headEnd);
194
+ headInjected = true;
106
195
  }
107
- const text = decoder.decode(chunk, { stream: true });
108
- const headCloseIdx = text.indexOf("</head>");
109
- if (headCloseIdx !== -1) {
110
- const modified =
111
- text.slice(0, headCloseIdx) + scripts + text.slice(headCloseIdx);
112
- controller.enqueue(encoder.encode(modified));
113
- injected = true;
114
- } else {
115
- controller.enqueue(chunk);
196
+
197
+ if (!bodyInjected) {
198
+ const bodyOpenMatch = BODY_OPEN_PATTERN.exec(pending);
199
+ if (!bodyOpenMatch || bodyOpenMatch.index === undefined) return;
200
+
201
+ const bodyEnd = bodyOpenMatch.index + bodyOpenMatch[0].length;
202
+ controller.enqueue(
203
+ encoder.encode(pending.slice(0, bodyEnd) + injection.body),
204
+ );
205
+ pending = pending.slice(bodyEnd);
206
+ bodyInjected = true;
116
207
  }
208
+
209
+ if (pending) {
210
+ controller.enqueue(encoder.encode(pending));
211
+ pending = "";
212
+ }
213
+ },
214
+ flush(controller) {
215
+ pending += decoder.decode();
216
+
217
+ if (!headInjected) {
218
+ const headCloseMatch = HEAD_CLOSE_PATTERN.exec(pending);
219
+ if (headCloseMatch && headCloseMatch.index !== undefined) {
220
+ const headEnd = headCloseMatch.index + headCloseMatch[0].length;
221
+ controller.enqueue(
222
+ encoder.encode(
223
+ pending.slice(0, headCloseMatch.index) +
224
+ injection.head +
225
+ pending.slice(headCloseMatch.index, headEnd),
226
+ ),
227
+ );
228
+ pending = pending.slice(headEnd);
229
+ headInjected = true;
230
+ }
231
+ }
232
+
233
+ if (headInjected && !bodyInjected) {
234
+ const bodyOpenMatch = BODY_OPEN_PATTERN.exec(pending);
235
+ if (bodyOpenMatch && bodyOpenMatch.index !== undefined) {
236
+ const bodyEnd = bodyOpenMatch.index + bodyOpenMatch[0].length;
237
+ controller.enqueue(
238
+ encoder.encode(pending.slice(0, bodyEnd) + injection.body),
239
+ );
240
+ pending = pending.slice(bodyEnd);
241
+ bodyInjected = true;
242
+ }
243
+ }
244
+
245
+ if (pending) controller.enqueue(encoder.encode(pending));
117
246
  },
118
247
  }),
119
248
  );
@@ -4,7 +4,6 @@ import {
4
4
  type ChatThreadSummary,
5
5
  } from "@agent-native/core/client/agent-chat";
6
6
  import { appPath } from "@agent-native/core/client/api-path";
7
- import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
8
7
  import { useT } from "@agent-native/core/client/i18n";
9
8
  import { OrgSwitcher } from "@agent-native/core/client/org";
10
9
  import { FeedbackButton } from "@agent-native/core/client/ui";
@@ -38,6 +37,9 @@ const navItems = [
38
37
  href: "/",
39
38
  view: "chat",
40
39
  },
40
+ ];
41
+
42
+ const bottomNavItems = [
41
43
  {
42
44
  icon: IconHierarchy2,
43
45
  labelKey: "settings.agentTitle",
@@ -419,11 +421,38 @@ export function Sidebar({
419
421
  </nav>
420
422
 
421
423
  <div className={cn("mt-auto shrink-0", collapsed && "py-2")}>
422
- {!collapsed ? (
423
- <div className="px-2 py-1">
424
- <ExtensionsSidebarSection />
425
- </div>
426
- ) : null}
424
+ <nav
425
+ className={cn(
426
+ "grid",
427
+ collapsed ? "gap-0 px-1 py-1" : "gap-1 px-2 py-1",
428
+ )}
429
+ >
430
+ {bottomNavItems.map((item) => {
431
+ const Icon = item.icon;
432
+ const isActive = location.pathname.startsWith(item.href);
433
+ const link = (
434
+ <Link
435
+ to={item.href}
436
+ className={navClass({ isActive })}
437
+ aria-current={isActive ? "page" : undefined}
438
+ aria-label={collapsed ? t(item.labelKey) : undefined}
439
+ >
440
+ <Icon className="size-4 shrink-0" />
441
+ <span className={collapsed ? "sr-only" : "truncate"}>
442
+ {t(item.labelKey)}
443
+ </span>
444
+ </Link>
445
+ );
446
+ return collapsed ? (
447
+ <Tooltip key={item.href}>
448
+ <TooltipTrigger asChild>{link}</TooltipTrigger>
449
+ <TooltipContent side="right">{t(item.labelKey)}</TooltipContent>
450
+ </Tooltip>
451
+ ) : (
452
+ <div key={item.href}>{link}</div>
453
+ );
454
+ })}
455
+ </nav>
427
456
 
428
457
  <div className={cn(collapsed ? "px-1 py-1" : "px-3 py-2")}>
429
458
  <OrgSwitcher
@@ -1,4 +1,4 @@
1
- import { ExtensionsListPage } from "@agent-native/core/client/extensions";
1
+ import { Navigate } from "react-router";
2
2
 
3
3
  import { APP_TITLE } from "@/lib/app-config";
4
4
 
@@ -7,5 +7,5 @@ export function meta() {
7
7
  }
8
8
 
9
9
  export default function ExtensionsRoute() {
10
- return <ExtensionsListPage />;
10
+ return <Navigate to="/settings#extensions" replace />;
11
11
  }
@@ -6,5 +6,6 @@ functions = "templates/chat/.netlify/functions-internal"
6
6
 
7
7
  [build.environment]
8
8
  GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
+ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
9
10
  NITRO_PRESET = "netlify"
10
11
  NPM_CONFIG_PRODUCTION = "false"
@@ -76,6 +76,22 @@ Full source-code customization is a core Agent Native capability. Extensions
76
76
  are the fast, sandboxed, no-deploy layer—not the limit of what the app can
77
77
  become.
78
78
 
79
+ Use this decision rule when either path could solve the request:
80
+
81
+ - Start with an extension for a self-contained, relatively one-off widget,
82
+ custom visualization or interaction, dashboard tile, standalone utility, or
83
+ add-on in an existing slot.
84
+ - Use app code for a core template feature, native UI/layout/routes/business
85
+ logic, a new slot, or behavior that should be reused broadly across dashboards
86
+ or users.
87
+ - If the same extension keeps getting rebuilt, promote it into app code as one
88
+ reusable native feature instead of maintaining copies.
89
+
90
+ As a rule of thumb, if an extension can solve the problem, generally start
91
+ there. An isolated custom dashboard visualization belongs in an extension; a
92
+ new chart type that should be available across dashboards belongs in the
93
+ template code.
94
+
79
95
  **When a user asks to "make an extension", "create an extension", or "build
80
96
  a ... extension" (or the older phrasings "make a tool" / "create a tool"):**
81
97
 
@@ -3066,6 +3066,12 @@ function createAgentNativeConfig(
3066
3066
  "process.env.AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID": JSON.stringify(
3067
3067
  process.env.GA_MEASUREMENT_ID?.trim() || "",
3068
3068
  ),
3069
+ __AGENT_NATIVE_BUILD_GTM_CONTAINER_ID__: JSON.stringify(
3070
+ process.env.GTM_CONTAINER_ID?.trim() || "",
3071
+ ),
3072
+ "process.env.AGENT_NATIVE_BUILD_GTM_CONTAINER_ID": JSON.stringify(
3073
+ process.env.GTM_CONTAINER_ID?.trim() || "",
3074
+ ),
3069
3075
  // Framework route warmup controls how SSR `.data` routes are fetched:
3070
3076
  // ordinary fetches keep them CDN-cacheable, while native prefetch headers
3071
3077
  // can be refused before the CDN/origin sees the request. Keep this value