@agent-native/core 0.70.2 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/multi-app-workspace.md +2 -2
  4. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +21 -10
  7. package/corpus/core/src/agent/durable-background.ts +192 -0
  8. package/corpus/core/src/agent/production-agent.ts +330 -13
  9. package/corpus/core/src/agent/run-manager.ts +66 -3
  10. package/corpus/core/src/agent/run-store.ts +129 -23
  11. package/corpus/core/src/agent/types.ts +20 -0
  12. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  13. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  14. package/corpus/core/src/deploy/build.ts +96 -0
  15. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  16. package/corpus/core/src/mcp/build-server.ts +22 -13
  17. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  18. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  19. package/corpus/templates/analytics/AGENTS.md +8 -0
  20. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  21. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +33 -6
  22. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  23. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  24. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +8 -2
  25. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -9
  26. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-06-23-screen-recordings-now-capture-at-a-crisp-1080p-bitrate-inste.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  29. package/corpus/templates/clips/chrome-extension/scripts/dev.ts +102 -0
  30. package/corpus/templates/clips/chrome-extension/src/background.ts +164 -32
  31. package/corpus/templates/clips/chrome-extension/src/content-script.ts +163 -13
  32. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +198 -35
  33. package/corpus/templates/clips/chrome-extension/src/overlay.css +73 -23
  34. package/corpus/templates/clips/chrome-extension/src/overlay.ts +82 -13
  35. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  36. package/corpus/templates/clips/chrome-extension/src/popup.ts +290 -1
  37. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  38. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  39. package/corpus/templates/clips/desktop/src/lib/recorder.ts +17 -20
  40. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  41. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  42. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  43. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  44. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  45. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  46. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +2 -2
  47. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +9 -1
  48. package/corpus/templates/clips/shared/upload-limits.ts +5 -2
  49. package/corpus/templates/plan/app/pages/PlansPage.tsx +11 -7
  50. package/corpus/templates/plan/changelog/2026-06-23-plan-loading-skeleton-no-longer-cuts-off-the-canvas-preview-.md +6 -0
  51. package/dist/action.js +21 -10
  52. package/dist/action.js.map +1 -1
  53. package/dist/agent/durable-background.d.ts +60 -0
  54. package/dist/agent/durable-background.d.ts.map +1 -0
  55. package/dist/agent/durable-background.js +144 -0
  56. package/dist/agent/durable-background.js.map +1 -0
  57. package/dist/agent/production-agent.d.ts +20 -0
  58. package/dist/agent/production-agent.d.ts.map +1 -1
  59. package/dist/agent/production-agent.js +292 -14
  60. package/dist/agent/production-agent.js.map +1 -1
  61. package/dist/agent/run-manager.d.ts +48 -0
  62. package/dist/agent/run-manager.d.ts.map +1 -1
  63. package/dist/agent/run-manager.js +45 -3
  64. package/dist/agent/run-manager.js.map +1 -1
  65. package/dist/agent/run-store.d.ts +30 -1
  66. package/dist/agent/run-store.d.ts.map +1 -1
  67. package/dist/agent/run-store.js +124 -24
  68. package/dist/agent/run-store.js.map +1 -1
  69. package/dist/agent/types.d.ts +20 -0
  70. package/dist/agent/types.d.ts.map +1 -1
  71. package/dist/agent/types.js.map +1 -1
  72. package/dist/client/AgentPanel.d.ts.map +1 -1
  73. package/dist/client/AgentPanel.js +2 -2
  74. package/dist/client/AgentPanel.js.map +1 -1
  75. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  76. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  77. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  78. package/dist/deploy/build.d.ts +29 -0
  79. package/dist/deploy/build.d.ts.map +1 -1
  80. package/dist/deploy/build.js +85 -0
  81. package/dist/deploy/build.js.map +1 -1
  82. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  83. package/dist/deploy/workspace-deploy.js +108 -0
  84. package/dist/deploy/workspace-deploy.js.map +1 -1
  85. package/dist/mcp/build-server.d.ts.map +1 -1
  86. package/dist/mcp/build-server.js +23 -9
  87. package/dist/mcp/build-server.js.map +1 -1
  88. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  89. package/dist/server/agent-chat-plugin.js +68 -13
  90. package/dist/server/agent-chat-plugin.js.map +1 -1
  91. package/docs/content/multi-app-workspace.md +2 -2
  92. package/docs/design/durable-agent-runs.md +458 -4
  93. package/package.json +1 -1
@@ -0,0 +1,317 @@
1
+ import { defineAction, embedApp } from "@agent-native/core";
2
+ import {
3
+ getRequestUserEmail,
4
+ getRequestOrgId,
5
+ buildDeepLink,
6
+ } from "@agent-native/core/server";
7
+ import { z } from "zod";
8
+ import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store";
9
+ import {
10
+ buildPanel,
11
+ listMetricKeys,
12
+ type ComposedPanel,
13
+ type MetricWindow,
14
+ } from "../server/lib/first-party-metric-catalog";
15
+ import { validateFirstPartyAnalyticsSql } from "../server/lib/first-party-analytics.js";
16
+ import {
17
+ hasCollabState,
18
+ applyText,
19
+ seedFromText,
20
+ } from "@agent-native/core/collab";
21
+
22
+ /**
23
+ * Push the saved config through the collab layer so open dashboard editors get
24
+ * the change in real time (mirrors update-dashboard / install-dashboard-template).
25
+ */
26
+ async function syncToCollab(
27
+ dashboardId: string,
28
+ config: Record<string, unknown>,
29
+ ): Promise<void> {
30
+ const docId = `dash-${dashboardId}`;
31
+ const configStr = JSON.stringify(config);
32
+ try {
33
+ if (await hasCollabState(docId)) {
34
+ await applyText(docId, configStr, "content", "agent");
35
+ } else {
36
+ await seedFromText(docId, configStr);
37
+ }
38
+ } catch {
39
+ // Collab sync is best-effort — the SQL write is the source of truth.
40
+ }
41
+ }
42
+
43
+ const WINDOWS = new Set<MetricWindow>(["30d", "90d", "all"]);
44
+
45
+ function normalizeWindow(raw: unknown): MetricWindow | undefined {
46
+ if (typeof raw !== "string") return undefined;
47
+ const v = raw.trim() as MetricWindow;
48
+ return WINDOWS.has(v) ? v : undefined;
49
+ }
50
+
51
+ /** One requested metric, normalized to a key plus optional overrides. */
52
+ interface NormalizedRequest {
53
+ metric: string;
54
+ id?: string;
55
+ title?: string;
56
+ chartType?: string;
57
+ width?: number;
58
+ window?: MetricWindow;
59
+ }
60
+
61
+ function normalizeRequest(raw: unknown): NormalizedRequest | null {
62
+ if (typeof raw === "string") {
63
+ const metric = raw.trim();
64
+ return metric ? { metric } : null;
65
+ }
66
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
67
+ const obj = raw as Record<string, unknown>;
68
+ const metric = typeof obj.metric === "string" ? obj.metric.trim() : "";
69
+ if (!metric) return null;
70
+ const width =
71
+ typeof obj.width === "number" && Number.isFinite(obj.width)
72
+ ? obj.width
73
+ : undefined;
74
+ return {
75
+ metric,
76
+ id: typeof obj.id === "string" ? obj.id : undefined,
77
+ title: typeof obj.title === "string" ? obj.title : undefined,
78
+ chartType: typeof obj.chartType === "string" ? obj.chartType : undefined,
79
+ width,
80
+ window: normalizeWindow(obj.window),
81
+ };
82
+ }
83
+ return null;
84
+ }
85
+
86
+ const metricSchema = z.union([
87
+ z.string(),
88
+ z.object({
89
+ metric: z.string(),
90
+ id: z.string().optional(),
91
+ title: z.string().optional(),
92
+ chartType: z.string().optional(),
93
+ width: z.number().optional(),
94
+ window: z.string().optional(),
95
+ }),
96
+ ]);
97
+
98
+ const METRIC_KEYS = listMetricKeys();
99
+
100
+ export default defineAction({
101
+ description:
102
+ "Build a large first-party analytics dashboard in ONE fast call: name the metrics you want and the SERVER generates the validated SQL + chart config for every panel. " +
103
+ "Do NOT hand-author large `update-dashboard` configs panel-by-panel — producing/streaming a big multi-panel config inside the ~40s run budget fails and thrashes. " +
104
+ "Each metric expands into a complete first-party panel from the shipped, already-validated metric catalog. Unknown metric keys are skipped and reported (not fatal); each panel's SQL is validated independently (valid panels are saved, invalid ones reported), and the dashboard is assembled and saved in a single atomic store write. " +
105
+ "If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped); with `overwrite: true` the config is replaced. " +
106
+ "Returns { dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
107
+ `Available metric keys: ${METRIC_KEYS.join(", ")}. ` +
108
+ "Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics).",
109
+ schema: z.object({
110
+ dashboardId: z
111
+ .string()
112
+ .describe(
113
+ "Dashboard id (without the `sql-dashboard-` prefix), e.g. 'first-party-overview'.",
114
+ ),
115
+ title: z
116
+ .string()
117
+ .optional()
118
+ .describe(
119
+ "Dashboard name. Used on create; on append it is applied only if the existing dashboard has no name.",
120
+ ),
121
+ metrics: z
122
+ .preprocess(
123
+ (v) => (typeof v === "string" ? JSON.parse(v) : v),
124
+ z.array(metricSchema),
125
+ )
126
+ .describe(
127
+ "Metric keys to include, in order. Each item is either a key string or { metric, title?, chartType?, width?, window? }. " +
128
+ `Valid keys: ${METRIC_KEYS.join(", ")}.`,
129
+ ),
130
+ overwrite: z
131
+ .boolean()
132
+ .optional()
133
+ .describe(
134
+ "If true, replace the whole dashboard config. If false (default) and the dashboard exists, APPEND the new panels (skipping ids already present).",
135
+ ),
136
+ }),
137
+ mcpApp: {
138
+ compactCatalog: true,
139
+ resource: embedApp({
140
+ title: "Dashboard preview",
141
+ description: "Open the composed dashboard in the real Analytics UI.",
142
+ iframeTitle: "Agent-Native Analytics",
143
+ openLabel: "Open dashboard",
144
+ height: 680,
145
+ }),
146
+ },
147
+ run: async (args) => {
148
+ const email = getRequestUserEmail();
149
+ if (!email) throw new Error("no authenticated user");
150
+ const ctx = { email, orgId: getRequestOrgId() || null };
151
+
152
+ // The CLI/gateway may hand `metrics` over as a JSON string; the schema
153
+ // preprocess handles that, but normalize defensively here too.
154
+ const rawMetrics: unknown[] = Array.isArray(args.metrics)
155
+ ? (args.metrics as unknown[])
156
+ : typeof args.metrics === "string"
157
+ ? (JSON.parse(args.metrics) as unknown[])
158
+ : [];
159
+
160
+ const requests: NormalizedRequest[] = [];
161
+ for (const raw of rawMetrics) {
162
+ const normalized = normalizeRequest(raw);
163
+ if (normalized) requests.push(normalized);
164
+ }
165
+
166
+ const createdMetrics: string[] = [];
167
+ const unknownMetrics: string[] = [];
168
+ const invalidMetrics: Array<{ metric: string; reason: string }> = [];
169
+ const composedPanels: ComposedPanel[] = [];
170
+
171
+ for (const req of requests) {
172
+ const panel = buildPanel(req.metric, {
173
+ id: req.id,
174
+ title: req.title,
175
+ chartType: req.chartType,
176
+ width: req.width,
177
+ window: req.window,
178
+ });
179
+ if (!panel) {
180
+ // Unknown key — report, never throw.
181
+ if (!unknownMetrics.includes(req.metric)) {
182
+ unknownMetrics.push(req.metric);
183
+ }
184
+ continue;
185
+ }
186
+ // Per-panel graceful validation: a bad panel is dropped + reported, the
187
+ // rest of the dashboard still builds. (Catalog SQL is known-good, so this
188
+ // is a defensive net, e.g. if a future window/override produces bad SQL.)
189
+ try {
190
+ validateFirstPartyAnalyticsSql(panel.sql);
191
+ } catch (e: any) {
192
+ invalidMetrics.push({
193
+ metric: req.metric,
194
+ reason: e?.message ?? String(e),
195
+ });
196
+ continue;
197
+ }
198
+ composedPanels.push(panel);
199
+ createdMetrics.push(req.metric);
200
+ }
201
+
202
+ const existing = await getDashboard(args.dashboardId, ctx);
203
+ const dashboardName =
204
+ args.title?.trim() ||
205
+ (existing && typeof existing.config?.name === "string"
206
+ ? (existing.config.name as string)
207
+ : args.dashboardId);
208
+
209
+ let finalConfig: Record<string, unknown>;
210
+ let appendedCount = composedPanels.length;
211
+ let skippedExistingIds: string[] = [];
212
+
213
+ if (existing && !args.overwrite) {
214
+ // Append: preserve existing panels + order, add only new panel ids.
215
+ const existingConfig = existing.config as Record<string, unknown>;
216
+ const existingPanels = Array.isArray(existingConfig.panels)
217
+ ? (existingConfig.panels as Array<Record<string, unknown>>)
218
+ : [];
219
+ const existingIds = new Set(
220
+ existingPanels
221
+ .map((p) => (typeof p?.id === "string" ? p.id : null))
222
+ .filter((id): id is string => !!id),
223
+ );
224
+ const toAppend: ComposedPanel[] = [];
225
+ for (const panel of composedPanels) {
226
+ if (existingIds.has(panel.id)) {
227
+ skippedExistingIds.push(panel.id);
228
+ continue;
229
+ }
230
+ toAppend.push(panel);
231
+ existingIds.add(panel.id);
232
+ }
233
+ appendedCount = toAppend.length;
234
+ finalConfig = {
235
+ ...existingConfig,
236
+ name:
237
+ typeof existingConfig.name === "string" && existingConfig.name.trim()
238
+ ? existingConfig.name
239
+ : dashboardName,
240
+ panels: [...existingPanels, ...toAppend],
241
+ };
242
+ } else {
243
+ // Create or overwrite: a fresh config with exactly the composed panels.
244
+ finalConfig = {
245
+ name: dashboardName,
246
+ description:
247
+ "First-party analytics dashboard composed from the metric catalog.",
248
+ panels: composedPanels,
249
+ };
250
+ }
251
+
252
+ const panelCount = Array.isArray(finalConfig.panels)
253
+ ? (finalConfig.panels as unknown[]).length
254
+ : 0;
255
+
256
+ // Single atomic save through the same store path update-dashboard uses.
257
+ await upsertDashboard(args.dashboardId, "sql", finalConfig, ctx);
258
+ await syncToCollab(args.dashboardId, finalConfig);
259
+
260
+ const parts: string[] = [];
261
+ if (existing && !args.overwrite) {
262
+ parts.push(`Appended ${appendedCount} panel(s) to "${args.dashboardId}"`);
263
+ if (skippedExistingIds.length > 0) {
264
+ parts.push(`${skippedExistingIds.length} already present`);
265
+ }
266
+ } else {
267
+ parts.push(
268
+ `${existing ? "Replaced" : "Created"} "${args.dashboardId}" with ${createdMetrics.length} panel(s)`,
269
+ );
270
+ }
271
+ if (unknownMetrics.length > 0) {
272
+ parts.push(
273
+ `skipped ${unknownMetrics.length} unknown metric(s): ${unknownMetrics.join(", ")}`,
274
+ );
275
+ }
276
+ if (invalidMetrics.length > 0) {
277
+ parts.push(
278
+ `skipped ${invalidMetrics.length} invalid metric(s): ${invalidMetrics.map((m) => m.metric).join(", ")}`,
279
+ );
280
+ }
281
+ parts.push(`Dashboard now has ${panelCount} panel(s).`);
282
+
283
+ return {
284
+ id: args.dashboardId,
285
+ dashboardId: args.dashboardId,
286
+ name: dashboardName,
287
+ panelCount,
288
+ createdMetrics,
289
+ unknownMetrics,
290
+ invalidMetrics,
291
+ skippedExistingIds,
292
+ urlPath: `/dashboards/${args.dashboardId}`,
293
+ deepLink: buildDeepLink({
294
+ app: "analytics",
295
+ view: "adhoc",
296
+ params: { dashboardId: args.dashboardId },
297
+ }),
298
+ message: parts.join("; ") + ".",
299
+ };
300
+ },
301
+ link: ({ result }) => {
302
+ const dashboardId =
303
+ result && typeof result === "object"
304
+ ? (result as { dashboardId?: string }).dashboardId
305
+ : undefined;
306
+ if (!dashboardId) return null;
307
+ return {
308
+ url: buildDeepLink({
309
+ app: "analytics",
310
+ view: "adhoc",
311
+ params: { dashboardId },
312
+ }),
313
+ label: "Open composed dashboard in Analytics",
314
+ view: "adhoc",
315
+ };
316
+ },
317
+ });
@@ -103,6 +103,34 @@ function formatYValue(
103
103
  return value.toLocaleString();
104
104
  }
105
105
 
106
+ /**
107
+ * Format a single metric value for display. Coerces Postgres numeric/bigint
108
+ * columns (returned as strings, e.g. a rate of "0.00000000000000000000") to a
109
+ * number so the formatter applies — SQLite returns JS numbers, so this only
110
+ * bites on Postgres/Neon, where the raw high-scale decimal would otherwise be
111
+ * dumped verbatim. A configured `valueLabels` mapping wins; a non-numeric
112
+ * string falls through unformatted.
113
+ */
114
+ export function formatMetricValue(
115
+ raw: unknown,
116
+ formatter?: "number" | "currency" | "percent",
117
+ valueLabels?: Record<string, string>,
118
+ ): string {
119
+ const valueLabel = valueLabels?.[String(raw)];
120
+ if (valueLabel !== undefined) return valueLabel;
121
+ const numericRaw =
122
+ typeof raw === "number"
123
+ ? raw
124
+ : typeof raw === "string" &&
125
+ raw.trim() !== "" &&
126
+ Number.isFinite(Number(raw))
127
+ ? Number(raw)
128
+ : null;
129
+ return numericRaw !== null
130
+ ? formatYValue(numericRaw, formatter)
131
+ : String(raw ?? "-");
132
+ }
133
+
106
134
  function parsePrometheusSeriesLabel(label: string): {
107
135
  metric: string;
108
136
  labels: Record<string, string>;
@@ -647,12 +675,11 @@ function MetricRenderer({
647
675
  } else {
648
676
  raw = row[valueCol];
649
677
  }
650
- const valueLabel = panel.config?.valueLabels?.[String(raw)];
651
- const value =
652
- valueLabel ??
653
- (typeof raw === "number"
654
- ? formatYValue(raw, panel.config?.yFormatter)
655
- : String(raw ?? "-"));
678
+ const value = formatMetricValue(
679
+ raw,
680
+ panel.config?.yFormatter,
681
+ panel.config?.valueLabels,
682
+ );
656
683
 
657
684
  return (
658
685
  <div className="flex flex-1 flex-col items-center justify-center py-2 text-center">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Build large first-party analytics dashboards in one fast call by naming metrics