@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +9 -0
- package/corpus/core/docs/content/extensions.mdx +23 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +4 -0
- package/corpus/core/src/agent/run-manager.ts +36 -10
- package/corpus/core/src/agent/types.ts +2 -0
- package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +16 -3
- package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
- package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
- package/corpus/core/src/deploy/build.ts +3 -0
- package/corpus/core/src/server/analytics.ts +154 -25
- package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
- package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
- package/corpus/core/src/templates/chat/netlify.toml +1 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
- package/corpus/core/src/vite/client.ts +6 -0
- package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
- package/corpus/templates/analytics/AGENTS.md +20 -8
- package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
- package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
- package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
- package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
- package/corpus/templates/analytics/actions/view-screen.ts +1 -1
- package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
- package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +103 -502
- package/corpus/templates/analytics/app/global.css +30 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
- package/corpus/templates/analytics/app/i18n-data.ts +80 -0
- package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -53
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +23 -25
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
- package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
- package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/server/db/schema.ts +4 -0
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
- package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
- package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
- package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
- package/corpus/templates/analytics/server/plugins/db.ts +10 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
- package/corpus/templates/assets/actions/generate-image.ts +28 -1
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +46 -8
- package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
- package/corpus/templates/assets/netlify.toml +1 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -8
- package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/brain/netlify.toml +1 -0
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +43 -5
- package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
- package/corpus/templates/calendar/netlify.toml +1 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +35 -6
- package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/chat/netlify.toml +1 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +52 -6
- package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
- package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
- package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
- package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
- package/corpus/templates/clips/desktop/src/styles.css +35 -3
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
- package/corpus/templates/clips/netlify.toml +1 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +3 -7
- package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
- package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
- package/corpus/templates/content/netlify.toml +1 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +55 -17
- package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
- package/corpus/templates/design/netlify.toml +1 -0
- package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
- package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
- package/corpus/templates/dispatch/netlify.toml +1 -0
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +41 -45
- package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
- package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
- package/corpus/templates/forms/netlify.toml +1 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +36 -5
- package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/macros/netlify.toml +1 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -9
- package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/mail/netlify.toml +1 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +38 -5
- package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/plan/netlify.toml +1 -0
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +47 -4
- package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
- package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
- package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
- package/corpus/templates/tasks/netlify.toml +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +4 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +9 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +25 -10
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/types.d.ts +2 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
- package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionsListPage.js +5 -3
- package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
- package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
- package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
- package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
- package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +10 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/deploy/build.js +1 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/server/analytics.d.ts +9 -4
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +111 -22
- package/dist/server/analytics.js.map +1 -1
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -6
- package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
- package/dist/templates/chat/netlify.toml +1 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +2 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/extensions.mdx +23 -4
- package/package.json +1 -1
- package/src/agent/production-agent.ts +4 -0
- package/src/agent/run-manager.ts +36 -10
- package/src/agent/types.ts +2 -0
- package/src/client/extensions/ExtensionsListPage.tsx +16 -3
- package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
- package/src/client/settings/SettingsPanel.tsx +10 -0
- package/src/deploy/build.ts +3 -0
- package/src/server/analytics.ts +154 -25
- package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
- package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
- package/src/templates/chat/netlify.toml +1 -0
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
- package/src/vite/client.ts +6 -0
- package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
- 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. **
|
|
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
|
-
##
|
|
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,
|
|
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
|
|
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.
|
|
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
|
package/src/agent/run-manager.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
989
|
+
terminalEventForCompletion || completionStatus !== run.status
|
|
970
990
|
? {
|
|
971
991
|
...run,
|
|
972
992
|
status: completionStatus,
|
|
973
|
-
events:
|
|
974
|
-
? [...run.events,
|
|
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
|
-
? (
|
|
1026
|
-
:
|
|
1027
|
-
|
|
1028
|
-
?
|
|
1029
|
-
:
|
|
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
|
-
|
|
1062
|
+
terminalEventForCompletion.event
|
|
1037
1063
|
: {
|
|
1038
1064
|
type: "error",
|
|
1039
1065
|
error: completionError
|
package/src/agent/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/src/deploy/build.ts
CHANGED
|
@@ -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
|
),
|
package/src/server/analytics.ts
CHANGED
|
@@ -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`).
|
|
12
|
-
* tag injection because
|
|
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,
|
|
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
|
|
86
|
-
if (!
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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 {
|
|
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 <
|
|
10
|
+
return <Navigate to="/settings#extensions" replace />;
|
|
11
11
|
}
|
|
@@ -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
|
|
package/src/vite/client.ts
CHANGED
|
@@ -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
|