@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,574 @@
1
+ /**
2
+ * First-party metric catalog — a keyed registry of reusable, already-validated
3
+ * first-party analytics panels.
4
+ *
5
+ * Why this exists: authoring a large multi-panel dashboard forces the agent to
6
+ * stream one giant `update-dashboard` argument (SQL + chart config for every
7
+ * panel) inside the ~40s hosted run budget. That big tool-call can't be resumed
8
+ * mid-stream and is all-or-nothing on validation, so the agent thrashes. This
9
+ * catalog moves panel authoring server-side: the agent names the metrics it
10
+ * wants and the server expands each one into a full, correct panel from SQL that
11
+ * already ships (and is exercised) in the
12
+ * `agent-native-templates-first-party` seed. See `compose-dashboard.ts`.
13
+ *
14
+ * The SQL here is copied verbatim from
15
+ * `seeds/dashboards/agent-native-templates-first-party.json` — do NOT re-invent
16
+ * it. Windowed metrics swap the `interval 'N days'` literal for the requested
17
+ * window via `buildSql(window)`.
18
+ *
19
+ * Distinct from the `<data-dictionary>` (the user/org-scoped catalog of business
20
+ * metric definitions the agent consults before writing ad-hoc SQL). That is a
21
+ * settings-backed knowledge layer for free-form querying; this is a code-level
22
+ * registry of canned, validated panels for one-call dashboard composition. They
23
+ * complement each other.
24
+ */
25
+
26
+ export type MetricWindow = "30d" | "90d" | "all";
27
+
28
+ export interface FirstPartyMetric {
29
+ /** Stable catalog key, also used as the default panel id. */
30
+ key: string;
31
+ title: string;
32
+ chartType: string;
33
+ source: "first-party";
34
+ /** Default grid columns this panel spans (1..6). */
35
+ width: number;
36
+ /**
37
+ * Build the panel SQL for an optional window. Windowed metrics substitute the
38
+ * `interval 'N days'` literal; non-windowed metrics ignore the argument and
39
+ * return their fixed SQL.
40
+ */
41
+ buildSql: (window?: MetricWindow) => string;
42
+ /** Whether `buildSql` actually varies with `window`. */
43
+ windowed: boolean;
44
+ /** Panel `config` block (chart keys, formatters, description). */
45
+ config: Record<string, unknown>;
46
+ }
47
+
48
+ const WINDOW_DAYS: Record<Exclude<MetricWindow, "all">, number> = {
49
+ "30d": 30,
50
+ "90d": 90,
51
+ };
52
+
53
+ /**
54
+ * Apply a window to SQL that contains `interval 'N days'` clauses.
55
+ *
56
+ * - "30d" / "90d": replace every `interval '<n> days'` with the requested days.
57
+ * - "all": strip the entire `AND timestamp::timestamptz >= now() - interval 'N days'`
58
+ * clause so the metric covers all time. (Only the standard
59
+ * `>= now() - interval` window clause is removed; any other interval usage is
60
+ * left intact.)
61
+ */
62
+ function applyWindow(sql: string, window: MetricWindow): string {
63
+ if (window === "all") {
64
+ return sql
65
+ .replace(
66
+ /\s+AND\s+timestamp::timestamptz\s*>=\s*now\(\)\s*-\s*interval\s*'\d+\s*days?'/gi,
67
+ "",
68
+ )
69
+ .replace(
70
+ /\s+WHERE\s+timestamp::timestamptz\s*>=\s*now\(\)\s*-\s*interval\s*'\d+\s*days?'\s+AND\s+/gi,
71
+ " WHERE ",
72
+ );
73
+ }
74
+ const days = WINDOW_DAYS[window];
75
+ return sql.replace(/interval\s*'\d+\s*days?'/gi, `interval '${days} days'`);
76
+ }
77
+
78
+ /**
79
+ * Helper for windowed metrics: keep the canonical SQL (with its default window
80
+ * baked in) and only rewrite when a different window is requested.
81
+ */
82
+ function windowed(sql: string): (window?: MetricWindow) => string {
83
+ return (window) => (window ? applyWindow(sql, window) : sql);
84
+ }
85
+
86
+ /** Helper for fixed (non-windowed) metrics. */
87
+ function fixed(sql: string): (window?: MetricWindow) => string {
88
+ return () => sql;
89
+ }
90
+
91
+ /**
92
+ * Catalog entries. Order here is the default panel order when a caller passes
93
+ * metrics; callers can reorder by listing keys in the order they want.
94
+ */
95
+ const ENTRIES: FirstPartyMetric[] = [
96
+ // --- Signups -------------------------------------------------------------
97
+ {
98
+ key: "total-signups",
99
+ title: "Signups",
100
+ chartType: "metric",
101
+ source: "first-party",
102
+ width: 1,
103
+ windowed: false,
104
+ buildSql: fixed(
105
+ "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup'",
106
+ ),
107
+ config: {
108
+ yKey: "count",
109
+ yFormatter: "number",
110
+ description:
111
+ "Better Auth user creation events from all Agent Native templates",
112
+ },
113
+ },
114
+ {
115
+ key: "signups-over-time",
116
+ title: "Signups Over Time",
117
+ chartType: "area",
118
+ source: "first-party",
119
+ width: 2,
120
+ windowed: false,
121
+ buildSql: fixed(
122
+ "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
123
+ ),
124
+ config: {
125
+ xKey: "date",
126
+ yKey: "count",
127
+ color: "#8b5cf6",
128
+ description: "Daily signup events across all Agent Native templates",
129
+ },
130
+ },
131
+ {
132
+ key: "signups-by-template",
133
+ title: "Signups by Template",
134
+ chartType: "bar",
135
+ source: "first-party",
136
+ width: 1,
137
+ windowed: false,
138
+ buildSql: fixed(
139
+ "SELECT COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' GROUP BY COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
140
+ ),
141
+ config: {
142
+ xKey: "template",
143
+ yKey: "count",
144
+ color: "#8b5cf6",
145
+ description: "Signup events grouped by inferred template/app",
146
+ },
147
+ },
148
+
149
+ // --- Sessions ------------------------------------------------------------
150
+ {
151
+ key: "sessions-by-app",
152
+ title: "Sessions by Agent-Native App",
153
+ chartType: "bar",
154
+ source: "first-party",
155
+ width: 2,
156
+ windowed: false,
157
+ buildSql: fixed(
158
+ "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
159
+ ),
160
+ config: {
161
+ xKey: "app",
162
+ yKey: "count",
163
+ color: "#10b981",
164
+ description:
165
+ "Per-template-site session activity (mail, calendar, slides, ...). Each tab fires session status once.",
166
+ },
167
+ },
168
+ {
169
+ key: "sessions-over-time",
170
+ title: "Sessions Over Time",
171
+ chartType: "area",
172
+ source: "first-party",
173
+ width: 1,
174
+ windowed: false,
175
+ buildSql: fixed(
176
+ "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
177
+ ),
178
+ config: { xKey: "date", yKey: "count", color: "#10b981" },
179
+ },
180
+ {
181
+ key: "signed-in-vs-anon",
182
+ title: "Signed-In vs Anonymous Sessions",
183
+ chartType: "bar",
184
+ source: "first-party",
185
+ width: 1,
186
+ windowed: false,
187
+ buildSql: fixed(
188
+ "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
189
+ ),
190
+ config: {
191
+ xKey: "signed_in",
192
+ yKey: "count",
193
+ color: "#f59e0b",
194
+ description:
195
+ "true = signed in, false = anonymous. Best proxy for total signups per period (still includes returning users).",
196
+ },
197
+ },
198
+
199
+ // --- Template / demo / CLI engagement ------------------------------------
200
+ {
201
+ key: "total-template-clicks",
202
+ title: "Template Clicks",
203
+ chartType: "metric",
204
+ source: "first-party",
205
+ width: 1,
206
+ windowed: false,
207
+ buildSql: fixed(
208
+ "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template'",
209
+ ),
210
+ config: {
211
+ yKey: "count",
212
+ yFormatter: "number",
213
+ description: "First-party events",
214
+ },
215
+ },
216
+ {
217
+ key: "total-demo-clicks",
218
+ title: "Demo Clicks",
219
+ chartType: "metric",
220
+ source: "first-party",
221
+ width: 1,
222
+ windowed: false,
223
+ buildSql: fixed(
224
+ "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo'",
225
+ ),
226
+ config: {
227
+ yKey: "count",
228
+ yFormatter: "number",
229
+ description: "First-party events",
230
+ },
231
+ },
232
+ {
233
+ key: "total-cli-copies",
234
+ title: "CLI Copies",
235
+ chartType: "metric",
236
+ source: "first-party",
237
+ width: 1,
238
+ windowed: false,
239
+ buildSql: fixed(
240
+ "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command'",
241
+ ),
242
+ config: {
243
+ yKey: "count",
244
+ yFormatter: "number",
245
+ description: "First-party events",
246
+ },
247
+ },
248
+ {
249
+ key: "template-interest-over-time",
250
+ title: "Template Interest Over Time",
251
+ chartType: "area",
252
+ source: "first-party",
253
+ width: 2,
254
+ windowed: false,
255
+ buildSql: fixed(
256
+ "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
257
+ ),
258
+ config: { xKey: "date", yKey: "count", color: "var(--brand-blue)" },
259
+ },
260
+ {
261
+ key: "clicks-by-template",
262
+ title: "Clicks by Template",
263
+ chartType: "bar",
264
+ source: "first-party",
265
+ width: 1,
266
+ windowed: false,
267
+ buildSql: fixed(
268
+ "SELECT COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' GROUP BY COALESCE(NULLIF(template, ''), 'unknown') ORDER BY count DESC LIMIT 20",
269
+ ),
270
+ config: { xKey: "template", yKey: "count", color: "var(--brand-blue)" },
271
+ },
272
+ {
273
+ key: "demo-clicks-by-template",
274
+ title: "Try-Demo Clicks by Template",
275
+ chartType: "bar",
276
+ source: "first-party",
277
+ width: 1,
278
+ windowed: false,
279
+ buildSql: fixed(
280
+ "SELECT COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' GROUP BY COALESCE(NULLIF(template, ''), 'unknown') ORDER BY count DESC LIMIT 20",
281
+ ),
282
+ config: { xKey: "template", yKey: "count", color: "var(--brand-teal)" },
283
+ },
284
+ {
285
+ key: "cli-copies-by-template",
286
+ title: "CLI Copies by Template",
287
+ chartType: "bar",
288
+ source: "first-party",
289
+ width: 1,
290
+ windowed: false,
291
+ buildSql: fixed(
292
+ "SELECT COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' GROUP BY COALESCE(NULLIF(template, ''), 'unknown') ORDER BY count DESC LIMIT 20",
293
+ ),
294
+ config: { xKey: "template", yKey: "count", color: "#06b6d4" },
295
+ },
296
+ {
297
+ key: "cli-copies-over-time",
298
+ title: "CLI Copies Over Time",
299
+ chartType: "area",
300
+ source: "first-party",
301
+ width: 2,
302
+ windowed: false,
303
+ buildSql: fixed(
304
+ "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
305
+ ),
306
+ config: { xKey: "date", yKey: "count", color: "#06b6d4" },
307
+ },
308
+
309
+ // --- Activity / pageviews ------------------------------------------------
310
+ {
311
+ key: "pageviews-over-time",
312
+ title: "Pageviews Over Time",
313
+ chartType: "area",
314
+ source: "first-party",
315
+ width: 2,
316
+ windowed: false,
317
+ // First-party tracking has no distinct `pageview` event; total tracked
318
+ // events per day is the honest activity/pageviews proxy over analytics_events.
319
+ buildSql: fixed(
320
+ "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events GROUP BY substr(timestamp, 1, 10) ORDER BY date",
321
+ ),
322
+ config: {
323
+ xKey: "date",
324
+ yKey: "count",
325
+ color: "var(--brand-blue)",
326
+ description:
327
+ "Total first-party tracked events per day (activity proxy — there is no distinct pageview event in /track data).",
328
+ },
329
+ },
330
+
331
+ // --- Virality & referrals (windowed) ------------------------------------
332
+ {
333
+ key: "referred-signups-30d",
334
+ title: "Referred Signups (30d)",
335
+ chartType: "metric",
336
+ source: "first-party",
337
+ width: 1,
338
+ windowed: true,
339
+ buildSql: windowed(
340
+ "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '30 days' AND properties::jsonb ->> 'referral_source' IS NOT NULL AND properties::jsonb ->> 'referral_source' <> '' AND properties::jsonb ->> 'referral_source' <> 'direct'",
341
+ ),
342
+ config: {
343
+ yKey: "count",
344
+ yFormatter: "number",
345
+ description:
346
+ "Signups in the window with a non-direct referral_source (clip_share, plan_share, external).",
347
+ },
348
+ },
349
+ {
350
+ key: "viral-signup-share-30d",
351
+ title: "Viral Signup Share (30d)",
352
+ chartType: "metric",
353
+ source: "first-party",
354
+ width: 1,
355
+ windowed: true,
356
+ buildSql: windowed(
357
+ "SELECT CASE WHEN COUNT(*) = 0 THEN 0 ELSE 1.0 * COUNT(*) FILTER (WHERE properties::jsonb ->> 'referral_source' IS NOT NULL AND properties::jsonb ->> 'referral_source' <> '' AND properties::jsonb ->> 'referral_source' <> 'direct') / COUNT(*) END AS rate FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '30 days'",
358
+ ),
359
+ config: {
360
+ yKey: "rate",
361
+ yFormatter: "percent",
362
+ description:
363
+ "Headline virality number: referred signups divided by all signups over the window.",
364
+ },
365
+ },
366
+ {
367
+ key: "clip-share-signups-30d",
368
+ title: "Clip-Share Signups (30d)",
369
+ chartType: "metric",
370
+ source: "first-party",
371
+ width: 1,
372
+ windowed: true,
373
+ buildSql: windowed(
374
+ "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '30 days' AND properties::jsonb ->> 'referral_source' = 'clip_share'",
375
+ ),
376
+ config: {
377
+ yKey: "count",
378
+ yFormatter: "number",
379
+ description: "Signups in the window attributed to a shared clip.",
380
+ },
381
+ },
382
+ {
383
+ key: "signups-by-referral-source",
384
+ title: "Signups by Referral Source (90d)",
385
+ chartType: "bar",
386
+ source: "first-party",
387
+ width: 1,
388
+ windowed: true,
389
+ buildSql: windowed(
390
+ "SELECT COALESCE(NULLIF(properties::jsonb ->> 'referral_source', ''), 'direct') AS referral_source, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '90 days' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'referral_source', ''), 'direct') ORDER BY count DESC LIMIT 20",
391
+ ),
392
+ config: {
393
+ xKey: "referral_source",
394
+ yKey: "count",
395
+ color: "var(--brand-purple)",
396
+ description:
397
+ "Signups grouped by referral_source over the window. Null/empty sources are bucketed as direct.",
398
+ },
399
+ },
400
+ {
401
+ key: "referred-signups-over-time",
402
+ title: "Referred Signups Over Time (90d)",
403
+ chartType: "area",
404
+ source: "first-party",
405
+ width: 2,
406
+ windowed: true,
407
+ buildSql: windowed(
408
+ "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '90 days' AND properties::jsonb ->> 'referral_source' IS NOT NULL AND properties::jsonb ->> 'referral_source' <> '' AND properties::jsonb ->> 'referral_source' <> 'direct' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
409
+ ),
410
+ config: {
411
+ xKey: "date",
412
+ yKey: "count",
413
+ color: "var(--brand-purple)",
414
+ description: "Daily referred (non-direct) signups over the window.",
415
+ },
416
+ },
417
+ {
418
+ key: "top-referrers",
419
+ title: "Top Referrers (90d)",
420
+ chartType: "table",
421
+ source: "first-party",
422
+ width: 1,
423
+ windowed: true,
424
+ buildSql: windowed(
425
+ "SELECT properties::jsonb ->> 'referrer_user' AS referrer_user, COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '90 days' AND properties::jsonb ->> 'referrer_user' IS NOT NULL AND properties::jsonb ->> 'referrer_user' <> '' GROUP BY properties::jsonb ->> 'referrer_user' ORDER BY signups DESC LIMIT 20",
426
+ ),
427
+ config: {
428
+ description:
429
+ "Users (by id) driving the most referred signups in the window.",
430
+ columns: [
431
+ { key: "referrer_user", label: "Referrer (user id)" },
432
+ { key: "signups", label: "Signups", format: "number" },
433
+ ],
434
+ },
435
+ },
436
+ {
437
+ key: "share-funnel-30d",
438
+ title: "Share Funnel (30d)",
439
+ chartType: "bar",
440
+ source: "first-party",
441
+ width: 2,
442
+ windowed: true,
443
+ buildSql: windowed(
444
+ "SELECT 'Share views' AS stage, COUNT(*) AS count FROM analytics_events WHERE event_name = 'share_view' AND timestamp::timestamptz >= now() - interval '30 days' UNION ALL SELECT 'CTA clicks' AS stage, COUNT(*) AS count FROM analytics_events WHERE event_name = 'share_cta_click' AND timestamp::timestamptz >= now() - interval '30 days' UNION ALL SELECT 'Clip-share signups' AS stage, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '30 days' AND properties::jsonb ->> 'referral_source' = 'clip_share'",
445
+ ),
446
+ config: {
447
+ xKey: "stage",
448
+ yKey: "count",
449
+ color: "var(--brand-teal)",
450
+ description:
451
+ "View to click to signup funnel for shared surfaces over the window: share_view, share_cta_click, then clip_share signups.",
452
+ },
453
+ },
454
+ {
455
+ key: "viral-participation-rate-90d",
456
+ title: "Viral Participation Rate (90d)",
457
+ chartType: "metric",
458
+ source: "first-party",
459
+ width: 1,
460
+ windowed: true,
461
+ buildSql: windowed(
462
+ "SELECT COALESCE(COUNT(*) FILTER (WHERE recent.auth_user_id IN (SELECT properties::jsonb ->> 'referrer_user' FROM analytics_events WHERE event_name = 'signup' AND properties::jsonb ->> 'referrer_user' IS NOT NULL AND properties::jsonb ->> 'referrer_user' <> ''))::float / NULLIF(COUNT(*), 0), 0) AS rate FROM (SELECT DISTINCT properties::jsonb ->> 'auth_user_id' AS auth_user_id FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '90 days' AND properties::jsonb ->> 'auth_user_id' IS NOT NULL AND properties::jsonb ->> 'auth_user_id' <> '') AS recent",
463
+ ),
464
+ config: {
465
+ yKey: "rate",
466
+ yFormatter: "percent",
467
+ description:
468
+ "Share of users who signed up in the window who have since referred at least one new signup (referrers counted across all time). New cohorts under-count: recent signups haven't had time to refer yet. Matches auth_user_id to referrer_user (both better-auth ids).",
469
+ },
470
+ },
471
+ {
472
+ key: "viral-coefficient-90d",
473
+ title: "Viral Coefficient K (90d)",
474
+ chartType: "metric",
475
+ source: "first-party",
476
+ width: 1,
477
+ windowed: true,
478
+ buildSql: windowed(
479
+ "SELECT COALESCE(COUNT(*) FILTER (WHERE properties::jsonb ->> 'referrer_user' IS NOT NULL AND properties::jsonb ->> 'referrer_user' <> '')::float / NULLIF(COUNT(DISTINCT properties::jsonb ->> 'auth_user_id'), 0), 0) AS k FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '90 days'",
480
+ ),
481
+ config: {
482
+ yKey: "k",
483
+ yFormatter: "number",
484
+ description:
485
+ "Viral coefficient (K): referred signups divided by new users over the window. K >= 1 means each user brings on at least one more, i.e. self-sustaining growth.",
486
+ },
487
+ },
488
+ {
489
+ key: "activated-referrers-90d",
490
+ title: "Activated Referrers (90d)",
491
+ chartType: "metric",
492
+ source: "first-party",
493
+ width: 1,
494
+ windowed: true,
495
+ buildSql: windowed(
496
+ "SELECT COUNT(DISTINCT properties::jsonb ->> 'referrer_user') AS count FROM analytics_events WHERE event_name = 'signup' AND timestamp::timestamptz >= now() - interval '90 days' AND properties::jsonb ->> 'referrer_user' IS NOT NULL AND properties::jsonb ->> 'referrer_user' <> ''",
497
+ ),
498
+ config: {
499
+ yKey: "count",
500
+ yFormatter: "number",
501
+ description:
502
+ "Distinct users who drove at least one referred signup in the window (distinct referrer_user on signups in the window).",
503
+ },
504
+ },
505
+ ];
506
+
507
+ const CATALOG: Map<string, FirstPartyMetric> = new Map(
508
+ ENTRIES.map((entry) => [entry.key, entry]),
509
+ );
510
+
511
+ /** All metric keys, in catalog order. */
512
+ export function listMetricKeys(): string[] {
513
+ return ENTRIES.map((entry) => entry.key);
514
+ }
515
+
516
+ /** All catalog entries, in catalog order. */
517
+ export function listMetrics(): FirstPartyMetric[] {
518
+ return [...ENTRIES];
519
+ }
520
+
521
+ /** Look up a single metric by key. Returns undefined for unknown keys. */
522
+ export function getMetric(key: string): FirstPartyMetric | undefined {
523
+ return CATALOG.get(key);
524
+ }
525
+
526
+ export interface ComposedPanel {
527
+ id: string;
528
+ title: string;
529
+ chartType: string;
530
+ source: "first-party";
531
+ width: number;
532
+ sql: string;
533
+ config: Record<string, unknown>;
534
+ }
535
+
536
+ export interface ComposePanelOverrides {
537
+ /** Panel id / metric key override (defaults to the metric key). */
538
+ id?: string;
539
+ title?: string;
540
+ chartType?: string;
541
+ width?: number;
542
+ window?: MetricWindow;
543
+ }
544
+
545
+ /**
546
+ * Expand a metric key into a full dashboard panel, applying optional overrides.
547
+ * Returns null for unknown keys so callers can report them gracefully instead
548
+ * of throwing.
549
+ */
550
+ export function buildPanel(
551
+ key: string,
552
+ overrides: ComposePanelOverrides = {},
553
+ ): ComposedPanel | null {
554
+ const metric = CATALOG.get(key);
555
+ if (!metric) return null;
556
+ const window = overrides.window;
557
+ const width =
558
+ typeof overrides.width === "number" &&
559
+ Number.isInteger(overrides.width) &&
560
+ overrides.width >= 1 &&
561
+ overrides.width <= 6
562
+ ? overrides.width
563
+ : metric.width;
564
+ return {
565
+ id: overrides.id?.trim() || metric.key,
566
+ title: overrides.title?.trim() || metric.title,
567
+ chartType: overrides.chartType?.trim() || metric.chartType,
568
+ source: "first-party",
569
+ width,
570
+ sql: metric.buildSql(window),
571
+ // Clone so callers can't mutate the shared catalog config object.
572
+ config: { ...metric.config },
573
+ };
574
+ }
@@ -162,8 +162,14 @@ const RETRYABLE_CHUNK_UPLOAD_STATUSES = new Set([
162
162
  const SCREEN_CAPTURE_FRAME_RATE = 24;
163
163
  const SCREEN_CAPTURE_MAX_WIDTH = 1920;
164
164
  const SCREEN_CAPTURE_MAX_HEIGHT = 1080;
165
- const RECORDING_VIDEO_BITRATE_BPS = 1_200_000;
166
- const RECORDING_AUDIO_BITRATE_BPS = 96_000;
165
+ // Capture quality for the browser MediaRecorder. We no longer shrink files
166
+ // client-side (ffmpeg.wasm re-encode is disabled — see COMPRESSION_ENABLED in
167
+ // `@/lib/compress`) and the upload provider streams large files directly, so we
168
+ // capture at a crisp 1080p bitrate instead of a tight budget. 1.2 Mbps left
169
+ // dense UI (fine text, Figma, code) visibly fuzzy; 8 Mbps keeps it sharp.
170
+ // Dial down here if file sizes become a concern for a deployment.
171
+ const RECORDING_VIDEO_BITRATE_BPS = 8_000_000;
172
+ const RECORDING_AUDIO_BITRATE_BPS = 128_000;
167
173
  type CaptureSource = "screen" | "camera" | "microphone" | "unknown";
168
174
 
169
175
  const VOICE_FOCUSED_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
@@ -927,16 +927,8 @@ export default function RecordingPage() {
927
927
  <TabsTrigger value="agent" className="min-w-0 px-2 text-xs">
928
928
  Agent
929
929
  </TabsTrigger>
930
- <TabsTrigger
931
- value="comments"
932
- className="min-w-0 gap-1 px-2 text-xs"
933
- >
930
+ <TabsTrigger value="comments" className="min-w-0 px-2 text-xs">
934
931
  Activity
935
- {comments.length > 0 ? (
936
- <span className="ml-0.5 rounded-full bg-accent px-1.5 text-[10px] tabular-nums">
937
- {comments.length}
938
- </span>
939
- ) : null}
940
932
  </TabsTrigger>
941
933
  <TabsTrigger value="transcript" className="min-w-0 px-2 text-xs">
942
934
  Transcript
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Dragging the desktop camera bubble now glides to a stop at the screen edge instead of jittering or snapping back
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Screen recordings now capture at a crisp 1080p bitrate instead of a compressed, fuzzy one
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite build --watch",
8
+ "dev:hot": "tsx scripts/dev.ts",
8
9
  "build": "vite build",
9
10
  "typecheck": "tsc --noEmit",
10
11
  "package": "pnpm build && tsx scripts/package.ts"