@agent-native/core 0.75.0 → 0.75.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 +46 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +71 -3
- package/corpus/core/src/agent/production-agent.ts +16 -3
- package/corpus/core/src/cli/skills.ts +15 -11
- package/corpus/core/src/deploy/build.ts +100 -30
- package/corpus/core/src/templates/default/_gitignore +1 -0
- package/corpus/core/src/templates/headless/_gitignore +1 -0
- package/corpus/core/src/templates/workspace-root/_gitignore +1 -0
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +0 -8
- package/corpus/templates/analytics/actions/update-dashboard.ts +3 -1
- package/corpus/templates/analytics/app/components/SqlEditor.tsx +57 -0
- package/corpus/templates/analytics/app/components/SqlHighlight.tsx +27 -28
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +130 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +4 -4
- package/corpus/templates/analytics/changelog/2026-06-24-active-user-dashboard-panels-classify-templates-more-accurat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-panel-deletes-now-stay-deleted-after-refresh.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-sql-panel-editors-now-highlight-query-syntax-and-keep-one-cl.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -11
- package/corpus/templates/clips/actions/list-recordings.ts +21 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +21 -2
- package/corpus/templates/clips/app/hooks/use-library.ts +18 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +7 -5
- package/dist/agent/durable-background.d.ts +51 -3
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +64 -3
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +16 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +15 -11
- package/dist/cli/skills.js.map +1 -1
- package/dist/deploy/build.d.ts +38 -17
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +96 -29
- package/dist/deploy/build.js.map +1 -1
- package/dist/templates/default/_gitignore +1 -0
- package/dist/templates/headless/_gitignore +1 -0
- package/dist/templates/workspace-root/_gitignore +1 -0
- package/package.json +1 -1
- package/src/templates/default/_gitignore +1 -0
- package/src/templates/headless/_gitignore +1 -0
- package/src/templates/workspace-root/_gitignore +1 -0
|
@@ -3,11 +3,14 @@ import {
|
|
|
3
3
|
useCallback,
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useMemo,
|
|
8
|
+
useRef,
|
|
7
9
|
useState,
|
|
8
10
|
type CSSProperties,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from "react";
|
|
13
|
+
import { createPortal } from "react-dom";
|
|
11
14
|
import {
|
|
12
15
|
Area,
|
|
13
16
|
AreaChart,
|
|
@@ -49,7 +52,6 @@ import {
|
|
|
49
52
|
|
|
50
53
|
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
|
51
54
|
import { useSqlQuery } from "@/lib/sql-query";
|
|
52
|
-
import { useChartTooltipFlip } from "@/hooks/use-chart-tooltip-flip";
|
|
53
55
|
import type {
|
|
54
56
|
SqlPanel,
|
|
55
57
|
ChartType,
|
|
@@ -75,6 +77,11 @@ const CHART_TOOLTIP_WRAPPER_STYLE: CSSProperties = {
|
|
|
75
77
|
pointerEvents: "none",
|
|
76
78
|
};
|
|
77
79
|
|
|
80
|
+
const PORTAL_GUTTER_PADDING = 12;
|
|
81
|
+
const PORTAL_CURSOR_OFFSET = 24;
|
|
82
|
+
const useBrowserLayoutEffect =
|
|
83
|
+
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
84
|
+
|
|
78
85
|
const CHART_TOOLTIP_PROPS = {
|
|
79
86
|
allowEscapeViewBox: { x: true, y: true },
|
|
80
87
|
wrapperStyle: CHART_TOOLTIP_WRAPPER_STYLE,
|
|
@@ -435,11 +442,15 @@ function ChartTooltip({
|
|
|
435
442
|
seriesNameFormatter?: (value: string) => string;
|
|
436
443
|
valueFormatter?: (value: number) => string;
|
|
437
444
|
}) {
|
|
438
|
-
const
|
|
445
|
+
const anchorRef = useRef<HTMLDivElement | null>(null);
|
|
446
|
+
const portalRef = useRef<HTMLDivElement | null>(null);
|
|
447
|
+
const [portalPosition, setPortalPosition] = useState<{
|
|
448
|
+
left: number;
|
|
449
|
+
top: number;
|
|
450
|
+
} | null>(null);
|
|
439
451
|
const items = sortTooltipPayloadItems(
|
|
440
452
|
payload?.filter((item) => item.value != null && item.value !== "") ?? [],
|
|
441
453
|
);
|
|
442
|
-
if (!active || items.length === 0) return null;
|
|
443
454
|
|
|
444
455
|
const labelText =
|
|
445
456
|
label == null
|
|
@@ -448,11 +459,96 @@ function ChartTooltip({
|
|
|
448
459
|
? labelFormatter(String(label))
|
|
449
460
|
: String(label);
|
|
450
461
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
462
|
+
useBrowserLayoutEffect(() => {
|
|
463
|
+
if (!active || items.length === 0 || typeof window === "undefined") {
|
|
464
|
+
setPortalPosition(null);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const anchor = anchorRef.current;
|
|
469
|
+
const wrapper = anchor?.parentElement;
|
|
470
|
+
if (!anchor || !wrapper) return;
|
|
471
|
+
|
|
472
|
+
let frame = 0;
|
|
473
|
+
const apply = () => {
|
|
474
|
+
frame = 0;
|
|
475
|
+
if (!(wrapper as HTMLElement).style.transform) return;
|
|
476
|
+
|
|
477
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
478
|
+
const portalRect = portalRef.current?.getBoundingClientRect();
|
|
479
|
+
const width = portalRect?.width || anchorRect.width;
|
|
480
|
+
const height = portalRect?.height || anchorRect.height;
|
|
481
|
+
if (width === 0 || height === 0) return;
|
|
482
|
+
|
|
483
|
+
const sidebar = document.querySelector(".agent-sidebar-panel");
|
|
484
|
+
const sidebarRect = sidebar?.getBoundingClientRect();
|
|
485
|
+
const rightLimit =
|
|
486
|
+
sidebarRect && sidebarRect.width > 0 && sidebarRect.left > 0
|
|
487
|
+
? sidebarRect.left - PORTAL_GUTTER_PADDING
|
|
488
|
+
: window.innerWidth - PORTAL_GUTTER_PADDING;
|
|
489
|
+
const bottomLimit = window.innerHeight - PORTAL_GUTTER_PADDING;
|
|
490
|
+
|
|
491
|
+
let left = anchorRect.left;
|
|
492
|
+
let top = anchorRect.top;
|
|
493
|
+
|
|
494
|
+
if (left + width > rightLimit) {
|
|
495
|
+
left = anchorRect.left - width - PORTAL_CURSOR_OFFSET;
|
|
496
|
+
}
|
|
497
|
+
left = Math.max(
|
|
498
|
+
PORTAL_GUTTER_PADDING,
|
|
499
|
+
Math.min(left, rightLimit - width),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
if (top + height > bottomLimit) {
|
|
503
|
+
top = bottomLimit - height;
|
|
504
|
+
}
|
|
505
|
+
top = Math.max(PORTAL_GUTTER_PADDING, top);
|
|
506
|
+
|
|
507
|
+
setPortalPosition((prev) =>
|
|
508
|
+
prev &&
|
|
509
|
+
Math.abs(prev.left - left) < 0.5 &&
|
|
510
|
+
Math.abs(prev.top - top) < 0.5
|
|
511
|
+
? prev
|
|
512
|
+
: { left, top },
|
|
513
|
+
);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const schedule = () => {
|
|
517
|
+
if (frame) return;
|
|
518
|
+
frame = window.requestAnimationFrame(apply);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
schedule();
|
|
522
|
+
|
|
523
|
+
const mutationObserver = new MutationObserver(schedule);
|
|
524
|
+
mutationObserver.observe(wrapper, {
|
|
525
|
+
attributes: true,
|
|
526
|
+
attributeFilter: ["style", "class"],
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const resizeObserver =
|
|
530
|
+
typeof ResizeObserver === "undefined"
|
|
531
|
+
? null
|
|
532
|
+
: new ResizeObserver(schedule);
|
|
533
|
+
resizeObserver?.observe(anchor);
|
|
534
|
+
if (portalRef.current) resizeObserver?.observe(portalRef.current);
|
|
535
|
+
|
|
536
|
+
window.addEventListener("resize", schedule);
|
|
537
|
+
window.addEventListener("scroll", schedule, true);
|
|
538
|
+
|
|
539
|
+
return () => {
|
|
540
|
+
if (frame) window.cancelAnimationFrame(frame);
|
|
541
|
+
mutationObserver.disconnect();
|
|
542
|
+
resizeObserver?.disconnect();
|
|
543
|
+
window.removeEventListener("resize", schedule);
|
|
544
|
+
window.removeEventListener("scroll", schedule, true);
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
if (!active || items.length === 0) return null;
|
|
549
|
+
|
|
550
|
+
const tooltip = (
|
|
551
|
+
<div className="min-w-40 max-w-[280px] rounded-md border border-border bg-card px-3 py-2 text-xs text-foreground shadow-lg">
|
|
456
552
|
{labelText && (
|
|
457
553
|
<div className="mb-1.5 truncate font-medium text-foreground">
|
|
458
554
|
{labelText}
|
|
@@ -485,6 +581,32 @@ function ChartTooltip({
|
|
|
485
581
|
</div>
|
|
486
582
|
</div>
|
|
487
583
|
);
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<>
|
|
587
|
+
<div ref={anchorRef} aria-hidden="true" className="invisible">
|
|
588
|
+
{tooltip}
|
|
589
|
+
</div>
|
|
590
|
+
{portalPosition && typeof document !== "undefined"
|
|
591
|
+
? createPortal(
|
|
592
|
+
<div
|
|
593
|
+
ref={portalRef}
|
|
594
|
+
role="tooltip"
|
|
595
|
+
style={{
|
|
596
|
+
position: "fixed",
|
|
597
|
+
left: portalPosition.left,
|
|
598
|
+
top: portalPosition.top,
|
|
599
|
+
zIndex: 1000,
|
|
600
|
+
pointerEvents: "none",
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
{tooltip}
|
|
604
|
+
</div>,
|
|
605
|
+
document.body,
|
|
606
|
+
)
|
|
607
|
+
: null}
|
|
608
|
+
</>
|
|
609
|
+
);
|
|
488
610
|
}
|
|
489
611
|
|
|
490
612
|
function detectKeys(
|
|
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|
|
5
5
|
import { Input } from "@/components/ui/input";
|
|
6
6
|
import { Label } from "@/components/ui/label";
|
|
7
7
|
import { Textarea } from "@/components/ui/textarea";
|
|
8
|
+
import { SqlEditor } from "@/components/SqlEditor";
|
|
8
9
|
import {
|
|
9
10
|
Dialog,
|
|
10
11
|
DialogContent,
|
|
@@ -360,13 +361,11 @@ function PanelEditorContent({
|
|
|
360
361
|
</Button>
|
|
361
362
|
)}
|
|
362
363
|
</div>
|
|
363
|
-
<
|
|
364
|
+
<SqlEditor
|
|
364
365
|
id="panel-sql"
|
|
365
366
|
value={form.sql}
|
|
366
367
|
onChange={(e) => setForm((f) => ({ ...f, sql: e.target.value }))}
|
|
367
368
|
rows={10}
|
|
368
|
-
spellCheck={false}
|
|
369
|
-
className="font-mono text-xs resize-y min-h-[200px]"
|
|
370
369
|
placeholder="SELECT ..."
|
|
371
370
|
/>
|
|
372
371
|
<p className="text-xs text-muted-foreground">
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
PopoverTrigger,
|
|
8
8
|
} from "@/components/ui/popover";
|
|
9
9
|
import { Button } from "@/components/ui/button";
|
|
10
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
11
10
|
import { Badge } from "@/components/ui/badge";
|
|
11
|
+
import { SqlEditor } from "@/components/SqlEditor";
|
|
12
12
|
import {
|
|
13
13
|
Tooltip,
|
|
14
14
|
TooltipContent,
|
|
@@ -206,14 +206,14 @@ export function ViewSqlPopover({
|
|
|
206
206
|
</div>
|
|
207
207
|
</div>
|
|
208
208
|
|
|
209
|
-
<
|
|
209
|
+
<SqlEditor
|
|
210
|
+
aria-label="SQL"
|
|
210
211
|
value={draft}
|
|
211
212
|
onChange={(e) => {
|
|
212
213
|
if (canEditSql) setDraft(e.target.value);
|
|
213
214
|
}}
|
|
214
215
|
rows={12}
|
|
215
|
-
|
|
216
|
-
className="font-mono text-xs resize-y min-h-[240px]"
|
|
216
|
+
className="min-h-[240px]"
|
|
217
217
|
placeholder="SELECT ..."
|
|
218
218
|
readOnly={!canEditSql}
|
|
219
219
|
/>
|
|
@@ -428,7 +428,7 @@
|
|
|
428
428
|
"chartType": "area",
|
|
429
429
|
"source": "first-party",
|
|
430
430
|
"width": 2,
|
|
431
|
-
"sql": "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template",
|
|
431
|
+
"sql": "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') ORDER BY date, template",
|
|
432
432
|
"config": {
|
|
433
433
|
"xKey": "date",
|
|
434
434
|
"yKey": "users",
|
|
@@ -448,7 +448,7 @@
|
|
|
448
448
|
"chartType": "area",
|
|
449
449
|
"source": "first-party",
|
|
450
450
|
"width": 2,
|
|
451
|
-
"sql": "WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT d.date, b.template, COUNT(DISTINCT b.user_key) AS users FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template",
|
|
451
|
+
"sql": "WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT d.date, b.template, COUNT(DISTINCT b.user_key) AS users FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template",
|
|
452
452
|
"config": {
|
|
453
453
|
"xKey": "date",
|
|
454
454
|
"yKey": "users",
|
|
@@ -96,7 +96,7 @@ function fixed(sql: string): (window?: MetricWindow) => string {
|
|
|
96
96
|
return () => sql;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const TEMPLATE_EXPR =
|
|
100
100
|
"COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')";
|
|
101
101
|
const DASHBOARD_TIME_RANGE_FILTER =
|
|
102
102
|
"('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))";
|
|
@@ -142,10 +142,12 @@ export function usesFirstPartyDashboardFilters(sql: string): boolean {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
|
|
145
|
-
const SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY timestamp) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT substr(timestamp, 1, 10) AS date, ${
|
|
146
|
-
const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${
|
|
147
|
-
const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${
|
|
148
|
-
const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${
|
|
145
|
+
const SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY timestamp) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT substr(timestamp, 1, 10) AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
|
|
146
|
+
const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${TEMPLATE_EXPR} AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, template, MIN(event_date) AS cohort_date FROM base GROUP BY user_key, template), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '1 day', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT c.cohort_date AS date, c.template, COALESCE(COUNT(DISTINCT c.user_key) FILTER (WHERE b.event_date = to_char(c.cohort_date::date + INTERVAL '1 day', 'YYYY-MM-DD'))::float / NULLIF(COUNT(DISTINCT c.user_key), 0), 0) AS rate FROM cohorts c LEFT JOIN base b ON b.user_key = c.user_key AND b.template = c.template GROUP BY c.cohort_date, c.template ORDER BY c.cohort_date, c.template`;
|
|
147
|
+
const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${TEMPLATE_EXPR} AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, template, MIN(event_date) AS cohort_date FROM base GROUP BY user_key, template), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT c.cohort_date AS date, c.template, COALESCE(COUNT(DISTINCT c.user_key) FILTER (WHERE b.event_date = to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD'))::float / NULLIF(COUNT(DISTINCT c.user_key), 0), 0) AS rate FROM cohorts c LEFT JOIN base b ON b.user_key = c.user_key AND b.template = c.template GROUP BY c.cohort_date, c.template ORDER BY c.cohort_date, c.template`;
|
|
148
|
+
const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
|
|
149
|
+
const DAU_BY_TEMPLATE_SQL = `SELECT substr(timestamp, 1, 10) AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY substr(timestamp, 1, 10), ${TEMPLATE_EXPR} ORDER BY date, template`;
|
|
150
|
+
const WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${TEMPLATE_EXPR} AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ${DASHBOARD_EMAIL_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_TIME_RANGE_FILTER.split("substr(timestamp, 1, 10)").join("event_date")}) SELECT d.date, b.template, COUNT(DISTINCT b.user_key) AS users FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
|
|
149
151
|
|
|
150
152
|
/**
|
|
151
153
|
* Catalog entries. Order here is the default panel order when a caller passes
|
|
@@ -578,9 +580,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
578
580
|
source: "first-party",
|
|
579
581
|
width: 2,
|
|
580
582
|
windowed: false,
|
|
581
|
-
buildSql: fixed(
|
|
582
|
-
"SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND substr(timestamp, 1, 10) >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template",
|
|
583
|
-
),
|
|
583
|
+
buildSql: fixed(DAU_BY_TEMPLATE_SQL),
|
|
584
584
|
config: {
|
|
585
585
|
xKey: "date",
|
|
586
586
|
yKey: "users",
|
|
@@ -602,9 +602,7 @@ const ENTRIES: FirstPartyMetric[] = [
|
|
|
602
602
|
source: "first-party",
|
|
603
603
|
width: 2,
|
|
604
604
|
windowed: false,
|
|
605
|
-
buildSql: fixed(
|
|
606
|
-
"WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, substr(timestamp, 1, 10) AS event_date, user_id FROM analytics_events WHERE COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) IS NOT NULL AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) SELECT d.date, b.template, COUNT(DISTINCT b.user_key) AS users FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template",
|
|
607
|
-
),
|
|
605
|
+
buildSql: fixed(WAU_BY_TEMPLATE_SQL),
|
|
608
606
|
config: {
|
|
609
607
|
xKey: "date",
|
|
610
608
|
yKey: "users",
|
|
@@ -55,6 +55,15 @@ export default defineAction({
|
|
|
55
55
|
.describe("Sort order"),
|
|
56
56
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
57
57
|
offset: z.coerce.number().int().min(0).default(0),
|
|
58
|
+
countOnly: z
|
|
59
|
+
// Robust coercion: a GET query param arrives as the string "true"/"false",
|
|
60
|
+
// and z.coerce.boolean would treat "false" as true. Map strings explicitly.
|
|
61
|
+
.preprocess(
|
|
62
|
+
(v) => (typeof v === "string" ? v === "true" : v),
|
|
63
|
+
z.boolean(),
|
|
64
|
+
)
|
|
65
|
+
.default(false)
|
|
66
|
+
.describe("Return only the total count, skipping the row payload"),
|
|
58
67
|
}),
|
|
59
68
|
http: { method: "GET" },
|
|
60
69
|
run: async (args) => {
|
|
@@ -135,6 +144,18 @@ export default defineAction({
|
|
|
135
144
|
);
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
// Count-only callers (e.g. the sidebar badge) need just the total for the
|
|
148
|
+
// same filters, ignoring limit/offset. Run the COUNT and short-circuit
|
|
149
|
+
// before the row select, joins, and tag/view subqueries. Keeping it inside
|
|
150
|
+
// this branch means the normal list path doesn't pay for an extra query.
|
|
151
|
+
if (args.countOnly) {
|
|
152
|
+
const totalRows = await db
|
|
153
|
+
.select({ count: sql<number>`COUNT(1)` })
|
|
154
|
+
.from(schema.recordings)
|
|
155
|
+
.where(and(...whereClauses));
|
|
156
|
+
return { recordings: [], total: Number(totalRows[0]?.count ?? 0) };
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
// Sort
|
|
139
160
|
const viewCountOrder = sql<number>`(
|
|
140
161
|
SELECT COUNT(1)
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
useSpaces,
|
|
47
47
|
useOrganizations,
|
|
48
48
|
useCreateFolder,
|
|
49
|
+
useRecordingsCount,
|
|
49
50
|
} from "@/hooks/use-library";
|
|
50
51
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
51
52
|
import { useDesktopPromo } from "@/hooks/use-desktop-promo";
|
|
@@ -124,6 +125,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
124
125
|
{ enabled: hasActiveOrg && Boolean(currentOrganizationId) },
|
|
125
126
|
);
|
|
126
127
|
|
|
128
|
+
// Clip count for the "Library" nav item — count-only, no row payload or
|
|
129
|
+
// title polling across the app shell.
|
|
130
|
+
const { data: libraryCount } = useRecordingsCount({ view: "library" });
|
|
131
|
+
|
|
127
132
|
const libFolderList: FolderNode[] = useMemo(
|
|
128
133
|
() =>
|
|
129
134
|
(libFolders?.folders ?? [])
|
|
@@ -176,12 +181,14 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
176
181
|
label: string;
|
|
177
182
|
icon: React.ComponentType<{ className?: string }>;
|
|
178
183
|
match: (path: string) => boolean;
|
|
184
|
+
count?: number;
|
|
179
185
|
}[] = [
|
|
180
186
|
{
|
|
181
187
|
to: "/library",
|
|
182
188
|
label: "Library",
|
|
183
189
|
icon: IconInbox,
|
|
184
190
|
match: (p) => p.startsWith("/library"),
|
|
191
|
+
count: libraryCount,
|
|
185
192
|
},
|
|
186
193
|
{
|
|
187
194
|
to: "/spaces",
|
|
@@ -374,7 +381,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
374
381
|
</div>
|
|
375
382
|
|
|
376
383
|
<nav className="mt-3 space-y-0.5 px-2">
|
|
377
|
-
{navItems.map(({ to, label, icon: Icon, match }) => {
|
|
384
|
+
{navItems.map(({ to, label, icon: Icon, match, count }) => {
|
|
378
385
|
const active = match(location.pathname);
|
|
379
386
|
return (
|
|
380
387
|
<NavLink
|
|
@@ -388,7 +395,19 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
388
395
|
)}
|
|
389
396
|
>
|
|
390
397
|
<Icon className="h-4 w-4" />
|
|
391
|
-
{label}
|
|
398
|
+
<span className="flex-1 truncate">{label}</span>
|
|
399
|
+
{count !== undefined && count > 0 && (
|
|
400
|
+
<span
|
|
401
|
+
className={cn(
|
|
402
|
+
"shrink-0 tabular-nums text-[11px]",
|
|
403
|
+
active
|
|
404
|
+
? "text-primary/80"
|
|
405
|
+
: "text-muted-foreground",
|
|
406
|
+
)}
|
|
407
|
+
>
|
|
408
|
+
{count}
|
|
409
|
+
</span>
|
|
410
|
+
)}
|
|
392
411
|
</NavLink>
|
|
393
412
|
);
|
|
394
413
|
})}
|
|
@@ -87,6 +87,24 @@ export function useRecordings(args: ListRecordingsArgs = {}) {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Count-only variant for surfaces like the sidebar badge that need a total but
|
|
92
|
+
* not the rows. Hits `list-recordings` with `countOnly`, so it skips the row
|
|
93
|
+
* payload server-side and doesn't share (or pay for) the full-list query or its
|
|
94
|
+
* title polling.
|
|
95
|
+
*/
|
|
96
|
+
export function useRecordingsCount(
|
|
97
|
+
args: Omit<ListRecordingsArgs, "limit" | "offset"> = {},
|
|
98
|
+
) {
|
|
99
|
+
return useActionQuery<number>(
|
|
100
|
+
"list-recordings",
|
|
101
|
+
{ ...args, countOnly: true } as any,
|
|
102
|
+
{
|
|
103
|
+
select: (data: any) => (typeof data?.total === "number" ? data.total : 0),
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
export interface SearchHit {
|
|
91
109
|
id: string;
|
|
92
110
|
title: string;
|
|
@@ -489,14 +489,16 @@ sign-in at setup — this is intended), so the first tool call in that client do
|
|
|
489
489
|
not hit an OAuth wall:
|
|
490
490
|
|
|
491
491
|
```bash
|
|
492
|
-
npx @agent-native/core@latest skills add visual-
|
|
492
|
+
npx @agent-native/core@latest skills add visual-plans
|
|
493
493
|
```
|
|
494
494
|
|
|
495
495
|
After that, `/visual-plan` and `/visual-recap` are the two installed slash
|
|
496
|
-
commands.
|
|
497
|
-
`
|
|
498
|
-
|
|
499
|
-
|
|
496
|
+
commands. If you only need one command, use `skills add visual-plan` or
|
|
497
|
+
`skills add visual-recap` instead. The other planning modes
|
|
498
|
+
(`create-ui-plan`, `create-prototype-plan`, `create-plan-design`,
|
|
499
|
+
`create-visual-questions`) are MCP tools reachable from `/visual-plan`, not
|
|
500
|
+
separate slash commands. Pass `--no-connect` to register the connector without
|
|
501
|
+
authenticating, then run
|
|
500
502
|
`npx @agent-native/core@latest connect https://plan.agent-native.com --client all`
|
|
501
503
|
whenever you are ready, or choose a narrower `--client`. Auth and MCP tool
|
|
502
504
|
loading are per client config/session.
|
|
@@ -4,6 +4,50 @@
|
|
|
4
4
|
* function, so it inherits the 15-min budget.
|
|
5
5
|
*/
|
|
6
6
|
export declare const AGENT_CHAT_PROCESS_RUN_PATH = "/_agent-native/agent-chat/_process-run";
|
|
7
|
+
/**
|
|
8
|
+
* Name of the standalone Netlify background function the build emits (see
|
|
9
|
+
* `emitSingleTemplateNetlifyBackgroundFunction` in deploy/build.ts). Shared so
|
|
10
|
+
* the emit and the dispatch-path helper below can never drift on the name.
|
|
11
|
+
*
|
|
12
|
+
* MUST end in `-background` — both because that is the conventional Netlify
|
|
13
|
+
* async-function suffix and because `isInBackgroundFunctionRuntime()` reads the
|
|
14
|
+
* `AWS_LAMBDA_FUNCTION_NAME` `-background` suffix as a secondary runtime signal.
|
|
15
|
+
*/
|
|
16
|
+
export declare const AGENT_BACKGROUND_FUNCTION_NAME = "server-agent-background";
|
|
17
|
+
/**
|
|
18
|
+
* Direct invocation URL of the standalone background function on Netlify.
|
|
19
|
+
*
|
|
20
|
+
* The function is emitted into the STANDARD functions dir with `background:true`
|
|
21
|
+
* and NO custom `config.path`, so Netlify exposes it (and ONLY it) at the
|
|
22
|
+
* default function URL `/.netlify/functions/<name>` and invokes it
|
|
23
|
+
* asynchronously (immediate HTTP 202 ack, 15-min budget). Hitting this URL
|
|
24
|
+
* directly BYPASSES Nitro's `/*` catch-all `server` function — which is the
|
|
25
|
+
* whole point: the previous approach put a custom `config.path` on the function
|
|
26
|
+
* and Netlify routed `/_agent-native/agent-chat/_process-run` to the synchronous
|
|
27
|
+
* `server` catch-all instead (verified live: a synchronous 401 from the handler
|
|
28
|
+
* rather than a 202 async ack).
|
|
29
|
+
*/
|
|
30
|
+
export declare const AGENT_BACKGROUND_FUNCTION_URL_PATH = "/.netlify/functions/server-agent-background";
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the path the foreground POST should self-dispatch the chat background
|
|
33
|
+
* worker to.
|
|
34
|
+
*
|
|
35
|
+
* On hosted Netlify (`NETLIFY` truthy and not `netlify dev`) the standalone
|
|
36
|
+
* async background function is reachable ONLY at its direct function URL
|
|
37
|
+
* (`/.netlify/functions/server-agent-background`); POSTing the framework route
|
|
38
|
+
* `AGENT_CHAT_PROCESS_RUN_PATH` would land on Nitro's synchronous `/*` catch-all
|
|
39
|
+
* (the ~60s `server` function) and NEVER get the 15-min budget. The standalone
|
|
40
|
+
* function's entry rewrites the incoming path back to `AGENT_CHAT_PROCESS_RUN_PATH`
|
|
41
|
+
* before delegating to the Nitro handler, so the `_process-run` plugin still
|
|
42
|
+
* dispatches it — only the OUTER URL differs.
|
|
43
|
+
*
|
|
44
|
+
* Everywhere else (local dev, `netlify dev`, non-Netlify hosts where the
|
|
45
|
+
* standalone function does not exist) we keep dispatching to the framework route
|
|
46
|
+
* directly — the same in-process catch-all handles it, and there is no separate
|
|
47
|
+
* async function to reach. The HMAC token (signed over the runId) is unchanged
|
|
48
|
+
* either way; only the URL path differs.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveAgentChatProcessRunDispatchPath(): string;
|
|
7
51
|
/**
|
|
8
52
|
* Env flag for durable background runs. DEFAULT-ON: unset means enabled; an app
|
|
9
53
|
* opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`).
|
|
@@ -39,9 +83,13 @@ export declare function isHostedRuntimeForDurableBackground(): boolean;
|
|
|
39
83
|
* hard wall and get killed at 60s, then re-dispatch/resume in a wasteful loop.
|
|
40
84
|
* So the 13-min budget is taken ONLY when this returns true.
|
|
41
85
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
86
|
+
* The PRIMARY signal is a `globalThis` marker the emitted background function's
|
|
87
|
+
* entry sets at cold start — the deployed Lambda name is not guaranteed to end
|
|
88
|
+
* in `-background` on Netlify, so the entry marks its own runtime. A `globalThis`
|
|
89
|
+
* flag (not `process.env`) keeps the no-env-mutation guard satisfied and carries
|
|
90
|
+
* no cross-request state (set once per isolate). The `AWS_LAMBDA_FUNCTION_NAME`
|
|
91
|
+
* suffix and the explicit `AGENT_CHAT_FORCE_BACKGROUND_RUNTIME` env (truthy) are
|
|
92
|
+
* additional signals — the latter an operator escape hatch. Off by default.
|
|
45
93
|
*/
|
|
46
94
|
export declare function isInBackgroundFunctionRuntime(): boolean;
|
|
47
95
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED
|
|
1
|
+
{"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AAExE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kCAAkC,gDAA0D,CAAC;AAE1G;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sCAAsC,IAAI,MAAM,CAS/D;AAED;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAsBvD;AAyBD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAM7D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CA0CvB"}
|