@agent-native/core 0.99.0 → 0.99.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 +1 -1
- package/corpus/core/CHANGELOG.md +23 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +33 -38
- package/corpus/core/src/audit/config.ts +0 -1
- package/corpus/core/src/chat-threads/store.ts +57 -41
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +1 -2
- package/corpus/core/src/client/analytics.ts +13 -0
- package/corpus/core/src/client/chat/message-components.tsx +15 -3
- package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
- package/corpus/core/src/client/index.ts +7 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
- package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
- package/corpus/core/src/client/use-db-sync.ts +3 -15
- package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
- package/corpus/core/src/demo/browser-state.ts +47 -0
- package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
- package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
- package/corpus/core/src/server/action-discovery.ts +0 -1
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +13 -0
- package/corpus/core/src/server/agent-teams.ts +17 -0
- package/corpus/core/src/server/core-routes-plugin.ts +0 -30
- package/corpus/core/src/server/google-oauth.ts +6 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
- package/corpus/templates/analytics/AGENTS.md +4 -4
- package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
- package/corpus/templates/analytics/app/assets/replay-cursor.svg +17 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
- package/corpus/templates/analytics/app/global.css +17 -9
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +32 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
- package/corpus/templates/analytics/app/pages/monitoring/errors/IssueDetail.tsx +1 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +77 -23
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-07-13-all-time-dashboard-filters-no-longer-send-invalid-dates-to-b.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-error-issue-titles-wrap-instead-of-overflowing.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-saving-a-new-dashboard-view-no-longer-fails-with-a-server-er.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-session-replay-cursors-stay-hidden-until-their-first-recorde.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-session-replay-links-now-support-cmd-click-and-middle-click-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
- package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +43 -4
- package/corpus/templates/analytics/server/lib/error-capture.ts +30 -67
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
- package/corpus/templates/assets/app/routes/library.tsx +3 -4
- package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
- package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
- package/corpus/templates/clips/app/components/library/recording-card.tsx +27 -26
- package/corpus/templates/clips/app/components/library/space-card.tsx +4 -6
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
- package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
- package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +18 -16
- package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
- package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
- package/corpus/templates/design/app/pages/Index.tsx +6 -1
- package/corpus/templates/design/app/pages/Present.tsx +12 -6
- package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
- package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
- package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
- package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
- package/dist/agent/production-agent.d.ts +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +25 -37
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/audit/config.d.ts.map +1 -1
- package/dist/audit/config.js +0 -1
- package/dist/audit/config.js.map +1 -1
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +47 -33
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +11 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +9 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +3 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +10 -2
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +2 -6
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +2 -59
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +1 -13
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +5 -52
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +3 -7
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-demo-mode-status.d.ts +3 -4
- package/dist/client/use-demo-mode-status.d.ts.map +1 -1
- package/dist/client/use-demo-mode-status.js +8 -23
- package/dist/client/use-demo-mode-status.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/browser-state.d.ts +10 -0
- package/dist/demo/browser-state.d.ts.map +1 -0
- package/dist/demo/browser-state.js +49 -0
- package/dist/demo/browser-state.js.map +1 -0
- package/dist/demo/fetch-interceptor.d.ts +2 -7
- package/dist/demo/fetch-interceptor.d.ts.map +1 -1
- package/dist/demo/fetch-interceptor.js +12 -56
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +2 -2
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +0 -1
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +11 -1
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +15 -1
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +0 -27
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +6 -2
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/package.json +2 -2
- package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
- package/corpus/core/src/demo/config.ts +0 -57
- package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
- package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
- package/dist/demo/actions/toggle-demo-mode.js +0 -21
- package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
- package/dist/demo/config.d.ts +0 -9
- package/dist/demo/config.d.ts.map +0 -1
- package/dist/demo/config.js +0 -58
- package/dist/demo/config.js.map +0 -1
|
@@ -897,6 +897,11 @@ export function ReconnectStreamMessage({
|
|
|
897
897
|
content: ContentPart[];
|
|
898
898
|
}) {
|
|
899
899
|
const chatRunning = React.useContext(ChatRunningContext);
|
|
900
|
+
const latestReasoningPartIndex = content.reduce(
|
|
901
|
+
(latestIndex, part, index) =>
|
|
902
|
+
part.type === "reasoning" ? index : latestIndex,
|
|
903
|
+
-1,
|
|
904
|
+
);
|
|
900
905
|
const streamingTextPartIndex =
|
|
901
906
|
content.at(-1)?.type === "text" ? content.length - 1 : -1;
|
|
902
907
|
const streamingReasoningPartIndex =
|
|
@@ -924,7 +929,8 @@ export function ReconnectStreamMessage({
|
|
|
924
929
|
key={`reconnect-reasoning-${i}`}
|
|
925
930
|
text={part.text}
|
|
926
931
|
isStreaming={chatRunning && i === streamingReasoningPartIndex}
|
|
927
|
-
defaultOpen={
|
|
932
|
+
defaultOpen={i === latestReasoningPartIndex}
|
|
933
|
+
collapseWhenReplaced={i < latestReasoningPartIndex}
|
|
928
934
|
/>
|
|
929
935
|
);
|
|
930
936
|
}
|
|
@@ -969,6 +975,7 @@ export function ReasoningCell({
|
|
|
969
975
|
isStreaming = false,
|
|
970
976
|
defaultOpen,
|
|
971
977
|
autoCollapse = false,
|
|
978
|
+
collapseWhenReplaced = false,
|
|
972
979
|
durationMs,
|
|
973
980
|
}: {
|
|
974
981
|
text: string;
|
|
@@ -976,6 +983,8 @@ export function ReasoningCell({
|
|
|
976
983
|
defaultOpen?: boolean;
|
|
977
984
|
/** Animate closed when a live reasoning segment finishes during a run. */
|
|
978
985
|
autoCollapse?: boolean;
|
|
986
|
+
/** Animate closed when a newer reasoning segment replaces this one. */
|
|
987
|
+
collapseWhenReplaced?: boolean;
|
|
979
988
|
/**
|
|
980
989
|
* Elapsed thinking time in ms, once known. Only meaningful once streaming
|
|
981
990
|
* has finished — callers that track live timing (see ReasoningMessagePart)
|
|
@@ -987,6 +996,7 @@ export function ReasoningCell({
|
|
|
987
996
|
const embeddedInWorkSummary = React.useContext(WorkSummaryContentContext);
|
|
988
997
|
const [open, setOpen] = useState(defaultOpen ?? true);
|
|
989
998
|
const wasStreamingRef = useRef(isStreaming);
|
|
999
|
+
const wasReplacedRef = useRef(collapseWhenReplaced);
|
|
990
1000
|
const trimmed = text.trim();
|
|
991
1001
|
|
|
992
1002
|
useEffect(() => {
|
|
@@ -996,6 +1006,13 @@ export function ReasoningCell({
|
|
|
996
1006
|
wasStreamingRef.current = isStreaming;
|
|
997
1007
|
}, [autoCollapse, isStreaming]);
|
|
998
1008
|
|
|
1009
|
+
useEffect(() => {
|
|
1010
|
+
if (collapseWhenReplaced && !wasReplacedRef.current) {
|
|
1011
|
+
setOpen(false);
|
|
1012
|
+
}
|
|
1013
|
+
wasReplacedRef.current = collapseWhenReplaced;
|
|
1014
|
+
}, [collapseWhenReplaced]);
|
|
1015
|
+
|
|
999
1016
|
if (!trimmed && !isStreaming) return null;
|
|
1000
1017
|
|
|
1001
1018
|
if (embeddedInWorkSummary) {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "@tabler/icons-react";
|
|
7
7
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
8
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
-
import {
|
|
9
|
+
import { Link } from "react-router";
|
|
10
10
|
|
|
11
11
|
import { extensionPath } from "../../extensions/path.js";
|
|
12
12
|
import { THEME_VAR_NAMES } from "../../extensions/theme.js";
|
|
@@ -482,7 +482,6 @@ function EmbeddedToolMenu({
|
|
|
482
482
|
const [open, setOpen] = useState(false);
|
|
483
483
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
484
484
|
const queryClient = useQueryClient();
|
|
485
|
-
const navigate = useNavigate();
|
|
486
485
|
|
|
487
486
|
const closeMenu = () => {
|
|
488
487
|
setOpen(false);
|
|
@@ -545,17 +544,14 @@ function EmbeddedToolMenu({
|
|
|
545
544
|
<PopoverContent align="end" sideOffset={4} className="w-56 p-1">
|
|
546
545
|
{!confirmingDelete ? (
|
|
547
546
|
<div className="flex flex-col">
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
onClick={
|
|
551
|
-
closeMenu();
|
|
552
|
-
navigate(extensionPath(extensionId, toolName));
|
|
553
|
-
}}
|
|
547
|
+
<Link
|
|
548
|
+
to={extensionPath(extensionId, toolName)}
|
|
549
|
+
onClick={closeMenu}
|
|
554
550
|
className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-[12px] hover:bg-accent cursor-pointer text-left"
|
|
555
551
|
>
|
|
556
552
|
<IconExternalLink className="h-3.5 w-3.5" />
|
|
557
553
|
<span>{t("extensions.openFullView")}</span>
|
|
558
|
-
</
|
|
554
|
+
</Link>
|
|
559
555
|
<button
|
|
560
556
|
type="button"
|
|
561
557
|
onClick={removeFromSlot}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IconArrowBackUp,
|
|
3
3
|
IconChevronRight,
|
|
4
|
-
IconCode,
|
|
5
4
|
IconDotsVertical,
|
|
6
5
|
IconHistory,
|
|
7
6
|
IconLoader2,
|
|
@@ -21,7 +20,6 @@ import { SESSION_REPLAY_IFRAME_ATTRIBUTE } from "../../session-replay-iframe-pro
|
|
|
21
20
|
import { sendToAgentChat } from "../agent-chat.js";
|
|
22
21
|
import { AgentToggleButton } from "../AgentPanel.js";
|
|
23
22
|
import { agentNativePath, appPath } from "../api-path.js";
|
|
24
|
-
import { Dialog, DialogContent, DialogTitle } from "../components/ui/dialog.js";
|
|
25
23
|
import {
|
|
26
24
|
Popover,
|
|
27
25
|
PopoverContent,
|
|
@@ -366,131 +364,6 @@ function applyCanonicalLink(path: string): () => void {
|
|
|
366
364
|
};
|
|
367
365
|
}
|
|
368
366
|
|
|
369
|
-
function SourceCodeDialog({
|
|
370
|
-
extension,
|
|
371
|
-
onSaved,
|
|
372
|
-
}: {
|
|
373
|
-
extension: Extension;
|
|
374
|
-
onSaved?: () => void;
|
|
375
|
-
}) {
|
|
376
|
-
const queryClient = useQueryClient();
|
|
377
|
-
const [open, setOpen] = useState(false);
|
|
378
|
-
const [code, setCode] = useState(extension.content ?? "");
|
|
379
|
-
const [saving, setSaving] = useState(false);
|
|
380
|
-
const [error, setError] = useState<string | null>(null);
|
|
381
|
-
|
|
382
|
-
// Sync in when the dialog opens, or when the viewed extension changes
|
|
383
|
-
// while the dialog stays mounted (e.g. re-parented to a different id).
|
|
384
|
-
useEffect(() => {
|
|
385
|
-
if (open) setCode(extension.content ?? "");
|
|
386
|
-
}, [open, extension.id]);
|
|
387
|
-
|
|
388
|
-
const isDirty = code !== (extension.content ?? "");
|
|
389
|
-
|
|
390
|
-
// Block Escape / outside-click from closing while there are unsaved edits.
|
|
391
|
-
const handleOpenChange = (next: boolean) => {
|
|
392
|
-
if (!next && isDirty) return;
|
|
393
|
-
setOpen(next);
|
|
394
|
-
if (!next) setError(null);
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const handleCancel = () => {
|
|
398
|
-
setCode(extension.content ?? "");
|
|
399
|
-
setOpen(false);
|
|
400
|
-
setError(null);
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const handleSave = async () => {
|
|
404
|
-
setSaving(true);
|
|
405
|
-
setError(null);
|
|
406
|
-
try {
|
|
407
|
-
const res = await fetch(
|
|
408
|
-
agentNativePath(`/_agent-native/extensions/${extension.id}`),
|
|
409
|
-
{
|
|
410
|
-
method: "PUT",
|
|
411
|
-
headers: { "Content-Type": "application/json" },
|
|
412
|
-
body: JSON.stringify({ content: code }),
|
|
413
|
-
},
|
|
414
|
-
);
|
|
415
|
-
if (!res.ok) {
|
|
416
|
-
const body = await res.json().catch(() => ({}));
|
|
417
|
-
throw new Error(body?.error ?? `Save failed (${res.status})`);
|
|
418
|
-
}
|
|
419
|
-
setOpen(false);
|
|
420
|
-
queryClient.setQueryData<Extension>(["extension", extension.id], (old) =>
|
|
421
|
-
old ? { ...old, content: code } : old,
|
|
422
|
-
);
|
|
423
|
-
queryClient.invalidateQueries({
|
|
424
|
-
queryKey: ["extension", extension.id],
|
|
425
|
-
});
|
|
426
|
-
queryClient.invalidateQueries({ queryKey: ["extensions"] });
|
|
427
|
-
onSaved?.();
|
|
428
|
-
} catch (err: any) {
|
|
429
|
-
setError(err?.message ?? "Save failed");
|
|
430
|
-
} finally {
|
|
431
|
-
setSaving(false);
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
return (
|
|
436
|
-
<>
|
|
437
|
-
<Tooltip>
|
|
438
|
-
<TooltipTrigger asChild>
|
|
439
|
-
<button
|
|
440
|
-
type="button"
|
|
441
|
-
onClick={() => setOpen(true)}
|
|
442
|
-
className="inline-flex items-center justify-center rounded-md h-8 w-8 text-muted-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
443
|
-
>
|
|
444
|
-
<IconCode className="h-4 w-4" />
|
|
445
|
-
</button>
|
|
446
|
-
</TooltipTrigger>
|
|
447
|
-
<TooltipContent>View / edit source</TooltipContent>
|
|
448
|
-
</Tooltip>
|
|
449
|
-
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
450
|
-
<DialogContent className="flex h-[85vh] w-[90vw] max-w-[900px] flex-col gap-0 overflow-hidden p-0">
|
|
451
|
-
<div className="flex shrink-0 items-center border-b border-border px-5 py-3 pr-12">
|
|
452
|
-
<DialogTitle className="truncate text-sm font-medium">
|
|
453
|
-
{extension.name} — source
|
|
454
|
-
</DialogTitle>
|
|
455
|
-
</div>
|
|
456
|
-
<textarea
|
|
457
|
-
className="flex-1 resize-none bg-muted/40 px-5 py-4 font-mono text-xs leading-relaxed text-foreground focus:outline-none"
|
|
458
|
-
value={code}
|
|
459
|
-
onChange={(e) => setCode(e.target.value)}
|
|
460
|
-
spellCheck={false}
|
|
461
|
-
/>
|
|
462
|
-
<div className="flex shrink-0 items-center justify-between border-t border-border px-5 py-3">
|
|
463
|
-
{error ? (
|
|
464
|
-
<p className="text-xs text-destructive">{error}</p>
|
|
465
|
-
) : (
|
|
466
|
-
<span className="text-xs text-muted-foreground">
|
|
467
|
-
Alpine.js / HTML · {code.length.toLocaleString()} chars
|
|
468
|
-
</span>
|
|
469
|
-
)}
|
|
470
|
-
<div className="flex items-center gap-2">
|
|
471
|
-
<button
|
|
472
|
-
type="button"
|
|
473
|
-
onClick={handleCancel}
|
|
474
|
-
className="inline-flex h-8 cursor-pointer items-center rounded-md border border-input px-3 text-xs hover:bg-accent"
|
|
475
|
-
>
|
|
476
|
-
Cancel
|
|
477
|
-
</button>
|
|
478
|
-
<button
|
|
479
|
-
type="button"
|
|
480
|
-
onClick={handleSave}
|
|
481
|
-
disabled={saving}
|
|
482
|
-
className="inline-flex h-8 cursor-pointer items-center rounded-md bg-primary px-4 text-xs font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
|
|
483
|
-
>
|
|
484
|
-
{saving ? "Saving…" : "Save"}
|
|
485
|
-
</button>
|
|
486
|
-
</div>
|
|
487
|
-
</div>
|
|
488
|
-
</DialogContent>
|
|
489
|
-
</Dialog>
|
|
490
|
-
</>
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
367
|
function EditToolPopover({
|
|
495
368
|
extension,
|
|
496
369
|
onOpenChange,
|
|
@@ -1342,12 +1215,6 @@ export function ExtensionViewer({ extensionId }: ExtensionViewerProps) {
|
|
|
1342
1215
|
onRestored={() => setRefreshKey((k) => k + 1)}
|
|
1343
1216
|
onOpenChange={onPopoverOpenChange}
|
|
1344
1217
|
/>
|
|
1345
|
-
{extension.canEdit && (
|
|
1346
|
-
<SourceCodeDialog
|
|
1347
|
-
extension={extension}
|
|
1348
|
-
onSaved={() => setRefreshKey((k) => k + 1)}
|
|
1349
|
-
/>
|
|
1350
|
-
)}
|
|
1351
1218
|
<EditToolPopover
|
|
1352
1219
|
extension={extension}
|
|
1353
1220
|
onOpenChange={onPopoverOpenChange}
|
|
@@ -388,6 +388,13 @@ export {
|
|
|
388
388
|
type BuildAgentNativeExtensionHtmlOptions,
|
|
389
389
|
type CreateHttpAgentNativeExtensionStorageOptions,
|
|
390
390
|
} from "./extensions/portable-extension.js";
|
|
391
|
+
export { injectSessionReplayIframeBootstrap } from "../extensions/session-replay-iframe.js";
|
|
392
|
+
export {
|
|
393
|
+
SESSION_REPLAY_IFRAME_ATTRIBUTE,
|
|
394
|
+
SESSION_REPLAY_IFRAME_PROBE,
|
|
395
|
+
SESSION_REPLAY_IFRAME_START,
|
|
396
|
+
SESSION_REPLAY_IFRAME_STOP,
|
|
397
|
+
} from "../session-replay-iframe-protocol.js";
|
|
391
398
|
export {
|
|
392
399
|
AGENT_NATIVE_HOST_BRIDGE_VERSION,
|
|
393
400
|
AGENT_NATIVE_HOST_MESSAGE_TYPES,
|
|
@@ -1,61 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <DemoModeSection /> — toggle that replaces emails and numbers with realistic
|
|
3
|
-
* fake data everywhere (UI + what the agent sees) while preserving names,
|
|
4
|
-
* free text, labels, IDs, and structure so the app keeps working.
|
|
5
|
-
*
|
|
6
|
-
* The toggle is stored in application_state under `demo-mode`
|
|
7
|
-
* (`{ enabled: boolean }`) and written via `PUT
|
|
8
|
-
* /_agent-native/application-state/demo-mode` (optimistic — flip first,
|
|
9
|
-
* write in the background). It READS from `/_agent-native/demo/status`
|
|
10
|
-
* instead, which ORs the per-user toggle with the `DEMO_MODE` env: a hosted
|
|
11
|
-
* demo deployment is forced on and the switch is locked so it can't be
|
|
12
|
-
* accidentally turned off on stage.
|
|
13
|
-
*/
|
|
1
|
+
/** Browser-local presentation toggle. Backend and agent results stay real. */
|
|
14
2
|
|
|
15
3
|
import { IconEyeOff } from "@tabler/icons-react";
|
|
16
|
-
import { useEffect, useState } from "react";
|
|
17
4
|
|
|
18
|
-
import {
|
|
5
|
+
import { setBrowserDemoModeEnabled } from "../../demo/browser-state.js";
|
|
19
6
|
import { useDemoModeStatus } from "../use-demo-mode-status.js";
|
|
20
7
|
|
|
21
|
-
const DEMO_MODE_WRITE_URL = agentNativePath(
|
|
22
|
-
"/_agent-native/application-state/demo-mode",
|
|
23
|
-
);
|
|
24
|
-
|
|
25
8
|
export function DemoModeSection() {
|
|
26
|
-
const
|
|
27
|
-
const { enabled: serverEnabled, forced, isLoading } = useDemoModeStatus();
|
|
28
|
-
|
|
29
|
-
// Surface the server value once it arrives (and on subsequent polls), but
|
|
30
|
-
// never clobber an in-flight optimistic toggle with a stale read. When
|
|
31
|
-
// forced by env, always reflect the locked-on state.
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (forced) {
|
|
34
|
-
setEnabled(true);
|
|
35
|
-
} else if (!isLoading) {
|
|
36
|
-
setEnabled((prev) => (prev === null ? serverEnabled : prev));
|
|
37
|
-
}
|
|
38
|
-
}, [serverEnabled, forced, isLoading]);
|
|
39
|
-
|
|
40
|
-
const toggle = async (next: boolean) => {
|
|
41
|
-
if (forced) return;
|
|
42
|
-
const previous = enabled;
|
|
43
|
-
// Optimistic: flip immediately, write in the background.
|
|
44
|
-
setEnabled(next);
|
|
45
|
-
try {
|
|
46
|
-
const res = await fetch(DEMO_MODE_WRITE_URL, {
|
|
47
|
-
method: "PUT",
|
|
48
|
-
headers: { "Content-Type": "application/json" },
|
|
49
|
-
credentials: "include",
|
|
50
|
-
body: JSON.stringify({ enabled: next }),
|
|
51
|
-
});
|
|
52
|
-
if (!res.ok) {
|
|
53
|
-
throw new Error(`HTTP ${res.status}`);
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
setEnabled(previous);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
9
|
+
const { enabled } = useDemoModeStatus();
|
|
59
10
|
|
|
60
11
|
return (
|
|
61
12
|
<div className="flex items-start justify-between gap-3 rounded-md border border-border bg-accent/30 px-2.5 py-2">
|
|
@@ -64,30 +15,23 @@ export function DemoModeSection() {
|
|
|
64
15
|
Enable demo mode
|
|
65
16
|
</div>
|
|
66
17
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
67
|
-
Anonymize emails in
|
|
68
|
-
presentations.
|
|
69
|
-
|
|
18
|
+
Anonymize displayed emails in this browser and reshape supported
|
|
19
|
+
dashboard charts for presentations. Backend, MCP, and agent results
|
|
20
|
+
stay real and access-scoped.
|
|
70
21
|
</p>
|
|
71
|
-
{forced && (
|
|
72
|
-
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
73
|
-
Enforced by the <code>DEMO_MODE</code> environment variable — can't
|
|
74
|
-
be turned off here.
|
|
75
|
-
</p>
|
|
76
|
-
)}
|
|
77
22
|
</div>
|
|
78
23
|
<button
|
|
79
24
|
type="button"
|
|
80
25
|
role="switch"
|
|
81
|
-
aria-checked={
|
|
26
|
+
aria-checked={enabled}
|
|
82
27
|
aria-label="Enable demo mode"
|
|
83
|
-
|
|
84
|
-
onClick={() => toggle(!enabled)}
|
|
28
|
+
onClick={() => setBrowserDemoModeEnabled(!enabled)}
|
|
85
29
|
// Theme tokens; streaming agent owns layout.
|
|
86
30
|
className={`relative inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full transition-colors ${
|
|
87
31
|
enabled
|
|
88
32
|
? "bg-primary"
|
|
89
33
|
: "bg-muted-foreground/30 hover:bg-muted-foreground/50"
|
|
90
|
-
}
|
|
34
|
+
}`}
|
|
91
35
|
>
|
|
92
36
|
<span
|
|
93
37
|
className={`inline-block h-3 w-3 transform rounded-full bg-background transition-transform ${
|
|
@@ -2790,7 +2790,7 @@ function SettingsPanelContent({
|
|
|
2790
2790
|
id={settingsSectionDomId("demo-mode")}
|
|
2791
2791
|
icon={<IconEyeOff size={14} />}
|
|
2792
2792
|
title="Demo mode"
|
|
2793
|
-
subtitle="Replace emails
|
|
2793
|
+
subtitle="Replace displayed emails with realistic fake data in this browser and reshape supported charts for presentations. Backend, MCP, and agent results stay real and access-scoped."
|
|
2794
2794
|
open={openSection === "demo-mode"}
|
|
2795
2795
|
onToggle={() => toggle("demo-mode")}
|
|
2796
2796
|
>
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
ensureDemoModeFetchInterceptor,
|
|
5
|
-
refreshDemoModeFetchInterceptor,
|
|
6
|
-
} from "../demo/fetch-interceptor.js";
|
|
3
|
+
import { ensureDemoModeFetchInterceptor } from "../demo/fetch-interceptor.js";
|
|
7
4
|
import { agentNativePath } from "./api-path.js";
|
|
8
5
|
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
9
6
|
import {
|
|
@@ -314,15 +311,6 @@ class SyncTransport {
|
|
|
314
311
|
// -------------------------------------------------------------------------
|
|
315
312
|
|
|
316
313
|
private fan(events: SyncEvent[], version: number | undefined): void {
|
|
317
|
-
if (
|
|
318
|
-
events.some(
|
|
319
|
-
(event) =>
|
|
320
|
-
event.source === "app-state" &&
|
|
321
|
-
(event.key === "demo-mode" || event.key === "*"),
|
|
322
|
-
)
|
|
323
|
-
) {
|
|
324
|
-
void refreshDemoModeFetchInterceptor();
|
|
325
|
-
}
|
|
326
314
|
for (const sub of this.subscribers.values()) {
|
|
327
315
|
sub.onEvents(events, version);
|
|
328
316
|
}
|
|
@@ -544,8 +532,8 @@ class SyncTransport {
|
|
|
544
532
|
};
|
|
545
533
|
|
|
546
534
|
private start(): void {
|
|
547
|
-
// Universal demo-mode redaction
|
|
548
|
-
// a no-op until
|
|
535
|
+
// Universal browser-local demo-mode presentation redaction. Idempotent and
|
|
536
|
+
// a no-op until the local preference is on. Lives here because every root
|
|
549
537
|
// already mounts useDbSync, so this needs zero per-template wiring.
|
|
550
538
|
ensureEmbedAuthFetchInterceptor();
|
|
551
539
|
ensureDemoModeFetchInterceptor();
|
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
enabled?: boolean;
|
|
8
|
-
forced?: boolean;
|
|
9
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
getBrowserDemoModeEnabled,
|
|
5
|
+
subscribeToBrowserDemoMode,
|
|
6
|
+
} from "../demo/browser-state.js";
|
|
10
7
|
|
|
11
8
|
export interface DemoModeStatus {
|
|
12
9
|
enabled: boolean;
|
|
13
|
-
forced:
|
|
10
|
+
forced: false;
|
|
14
11
|
isLoading: boolean;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
|
|
18
|
-
|
|
19
14
|
/**
|
|
20
|
-
* Reads the
|
|
21
|
-
*
|
|
22
|
-
* so mounted consumers refresh without polling or duplicating the route call.
|
|
15
|
+
* Reads the browser-local Demo mode presentation preference. This deliberately
|
|
16
|
+
* has no backend request: the server and agent always operate on real data.
|
|
23
17
|
*/
|
|
24
18
|
export function useDemoModeStatus(): DemoModeStatus {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const res = await fetch(DEMO_STATUS_URL, {
|
|
31
|
-
credentials: "same-origin",
|
|
32
|
-
});
|
|
33
|
-
if (!res.ok) return null;
|
|
34
|
-
return (await res.json()) as DemoModeStatusResponse | null;
|
|
35
|
-
},
|
|
36
|
-
staleTime: Infinity,
|
|
37
|
-
});
|
|
19
|
+
const enabled = useSyncExternalStore(
|
|
20
|
+
subscribeToBrowserDemoMode,
|
|
21
|
+
getBrowserDemoModeEnabled,
|
|
22
|
+
() => false,
|
|
23
|
+
);
|
|
38
24
|
|
|
39
25
|
return {
|
|
40
|
-
enabled
|
|
41
|
-
forced:
|
|
42
|
-
isLoading,
|
|
26
|
+
enabled,
|
|
27
|
+
forced: false,
|
|
28
|
+
isLoading: false,
|
|
43
29
|
};
|
|
44
30
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Browser-local Demo mode state.
|
|
2
|
+
*
|
|
3
|
+
* Demo mode is a presentation preference for this browser only. It must not
|
|
4
|
+
* be persisted in application state or consulted by backend actions.
|
|
5
|
+
*/
|
|
6
|
+
export const DEMO_MODE_STORAGE_KEY = "agent-native:demo-mode";
|
|
7
|
+
|
|
8
|
+
const DEMO_MODE_CHANGE_EVENT = "agent-native:demo-mode-change";
|
|
9
|
+
|
|
10
|
+
export function getBrowserDemoModeEnabled(): boolean {
|
|
11
|
+
if (typeof window === "undefined") return false;
|
|
12
|
+
try {
|
|
13
|
+
return window.localStorage.getItem(DEMO_MODE_STORAGE_KEY) === "true";
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setBrowserDemoModeEnabled(enabled: boolean): void {
|
|
20
|
+
if (typeof window === "undefined") return;
|
|
21
|
+
try {
|
|
22
|
+
if (enabled) {
|
|
23
|
+
window.localStorage.setItem(DEMO_MODE_STORAGE_KEY, "true");
|
|
24
|
+
} else {
|
|
25
|
+
window.localStorage.removeItem(DEMO_MODE_STORAGE_KEY);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// A blocked or unavailable localStorage should not break the settings UI.
|
|
29
|
+
}
|
|
30
|
+
window.dispatchEvent(new Event(DEMO_MODE_CHANGE_EVENT));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function subscribeToBrowserDemoMode(callback: () => void): () => void {
|
|
34
|
+
if (typeof window === "undefined") return () => undefined;
|
|
35
|
+
|
|
36
|
+
const onStorage = (event: StorageEvent) => {
|
|
37
|
+
if (event.key === DEMO_MODE_STORAGE_KEY || event.key === null) callback();
|
|
38
|
+
};
|
|
39
|
+
const onChange = () => callback();
|
|
40
|
+
|
|
41
|
+
window.addEventListener("storage", onStorage);
|
|
42
|
+
window.addEventListener(DEMO_MODE_CHANGE_EVENT, onChange);
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener("storage", onStorage);
|
|
45
|
+
window.removeEventListener(DEMO_MODE_CHANGE_EVENT, onChange);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { agentNativePath } from "../client/api-path.js";
|
|
2
1
|
/**
|
|
3
2
|
* Client-side demo-mode redaction.
|
|
4
3
|
*
|
|
@@ -14,8 +13,8 @@ import { agentNativePath } from "../client/api-path.js";
|
|
|
14
13
|
* can only ever post-process JSON the app already parses for display, so it
|
|
15
14
|
* physically cannot break auth, SSE streams, SSR HTML, or binary downloads.
|
|
16
15
|
*
|
|
17
|
-
* The agent is
|
|
18
|
-
*
|
|
16
|
+
* The agent transport is intentionally skipped because it carries the real
|
|
17
|
+
* backend results; this interceptor is only a browser presentation transform.
|
|
19
18
|
*
|
|
20
19
|
* Scope intentionally narrow:
|
|
21
20
|
* - Only same-document `GET` requests are redacted. Mutation responses
|
|
@@ -23,20 +22,18 @@ import { agentNativePath } from "../client/api-path.js";
|
|
|
23
22
|
* typed isn't echoed back as fake data mid-demo.
|
|
24
23
|
* - Only `application/json` 2xx bodies. Streams (`text/event-stream`),
|
|
25
24
|
* HTML, and binary are skipped by content-type.
|
|
26
|
-
* - Framework infra endpoints (poll
|
|
27
|
-
* itself) are skipped — no PII and avoids self-recursion.
|
|
25
|
+
* - Framework infra endpoints (poll and events) are skipped.
|
|
28
26
|
* - Any error during interception falls back to the original response.
|
|
29
27
|
*/
|
|
28
|
+
import { getBrowserDemoModeEnabled } from "./browser-state.js";
|
|
30
29
|
import { redactDemoData } from "./redact.js";
|
|
31
30
|
|
|
32
|
-
const STATUS_PATH = agentNativePath("/_agent-native/demo/status");
|
|
33
31
|
const SKIP_SUBSTRINGS = [
|
|
34
|
-
"/_agent-native/demo/status",
|
|
35
32
|
"/_agent-native/poll",
|
|
36
33
|
"/_agent-native/events",
|
|
37
|
-
// Never touch agent transport. The agent
|
|
38
|
-
//
|
|
39
|
-
//
|
|
34
|
+
// Never touch agent transport. The agent receives real tool results; faking
|
|
35
|
+
// its own transcript adds no demo value and must stay clear of the
|
|
36
|
+
// tool_use/tool_result protocol. Covers
|
|
40
37
|
// "/_agent-native/agent" (stream) and "/_agent-native/agent-chat"
|
|
41
38
|
// (thread history) and any sub-paths.
|
|
42
39
|
"/_agent-native/agent",
|
|
@@ -73,16 +70,6 @@ export function shouldSkipDemoResponseRedaction(url: string): boolean {
|
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
let installed = false;
|
|
76
|
-
let demoEnabled = false;
|
|
77
|
-
let originalFetch: typeof fetch | null = null;
|
|
78
|
-
let refreshPromise: Promise<void> | null = null;
|
|
79
|
-
|
|
80
|
-
// Set once the first demo-status check completes. We DO NOT block requests on
|
|
81
|
-
// it — if status isn't known yet a response is simply passed through
|
|
82
|
-
// un-redacted (a brief first-paint window) rather than delaying transport.
|
|
83
|
-
// Injecting latency into early GETs previously risked the agent's streaming
|
|
84
|
-
// reconnect logic; transport safety wins over redacting the first paint.
|
|
85
|
-
let firstStatusDone = false;
|
|
86
73
|
|
|
87
74
|
/** Reject after `ms` so a misclassified streaming body can never hang. */
|
|
88
75
|
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
@@ -114,49 +101,17 @@ function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
|
|
|
114
101
|
return m.toUpperCase();
|
|
115
102
|
}
|
|
116
103
|
|
|
117
|
-
async function refreshDemoFlag(): Promise<void> {
|
|
118
|
-
const f = originalFetch ?? fetch;
|
|
119
|
-
try {
|
|
120
|
-
const res = await f(STATUS_PATH, { credentials: "same-origin" });
|
|
121
|
-
if (!res.ok) return;
|
|
122
|
-
const json = (await res.json()) as {
|
|
123
|
-
enabled?: boolean;
|
|
124
|
-
forced?: boolean;
|
|
125
|
-
} | null;
|
|
126
|
-
demoEnabled = json?.enabled === true || json?.forced === true;
|
|
127
|
-
} catch {
|
|
128
|
-
// Status endpoint unreachable — leave the last known value.
|
|
129
|
-
} finally {
|
|
130
|
-
firstStatusDone = true;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Refresh demo mode after the shared DB-sync stream reports a real change. */
|
|
135
|
-
export function refreshDemoModeFetchInterceptor(): Promise<void> {
|
|
136
|
-
if (!installed) return Promise.resolve();
|
|
137
|
-
if (!refreshPromise) {
|
|
138
|
-
refreshPromise = refreshDemoFlag().finally(() => {
|
|
139
|
-
refreshPromise = null;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return refreshPromise;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
104
|
/**
|
|
146
|
-
* Install the demo-mode fetch interceptor
|
|
147
|
-
* changes are driven by the shared DB-sync transport instead of a separate
|
|
148
|
-
* fixed polling loop.
|
|
105
|
+
* Install the browser-local demo-mode fetch interceptor.
|
|
149
106
|
* Idempotent and browser-only — safe to call from any hook that runs in
|
|
150
|
-
* every template root (we call it from `useDbSync`).
|
|
151
|
-
* mode is actually on.
|
|
107
|
+
* every template root (we call it from `useDbSync`).
|
|
152
108
|
*/
|
|
153
109
|
export function ensureDemoModeFetchInterceptor(): void {
|
|
154
110
|
if (typeof window === "undefined") return;
|
|
155
111
|
if (installed) return;
|
|
156
112
|
installed = true;
|
|
157
113
|
|
|
158
|
-
|
|
159
|
-
const base = originalFetch;
|
|
114
|
+
const base = window.fetch.bind(window);
|
|
160
115
|
|
|
161
116
|
window.fetch = async function patchedFetch(
|
|
162
117
|
input: RequestInfo | URL,
|
|
@@ -164,11 +119,11 @@ export function ensureDemoModeFetchInterceptor(): void {
|
|
|
164
119
|
): Promise<Response> {
|
|
165
120
|
const res = await base(input, init);
|
|
166
121
|
|
|
167
|
-
// Fast path: anything that isn't a demo
|
|
122
|
+
// Fast path: anything that isn't a browser-local-demo, plain GET returns the
|
|
168
123
|
// ORIGINAL response with zero body work and zero extra awaits — when
|
|
169
124
|
// demo mode is off this wrapper is byte-for-byte native fetch, so it
|
|
170
125
|
// cannot influence agent/run/stream transport.
|
|
171
|
-
if (!
|
|
126
|
+
if (!getBrowserDemoModeEnabled()) return res;
|
|
172
127
|
if (methodOf(input, init) !== "GET") return res;
|
|
173
128
|
if (!res.ok) return res;
|
|
174
129
|
|
|
@@ -217,6 +172,4 @@ export function ensureDemoModeFetchInterceptor(): void {
|
|
|
217
172
|
return res;
|
|
218
173
|
}
|
|
219
174
|
};
|
|
220
|
-
|
|
221
|
-
void refreshDemoModeFetchInterceptor();
|
|
222
175
|
}
|