@agent-native/core 0.72.4 → 0.73.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 (91) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/skills-guide.md +14 -0
  4. package/corpus/core/docs/content/template-clips.md +7 -1
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/durable-background.ts +25 -12
  7. package/corpus/core/src/cli/create.ts +1 -1
  8. package/corpus/core/src/cli/skills.ts +450 -19
  9. package/corpus/core/src/server/auth.ts +7 -0
  10. package/corpus/core/src/server/onboarding-html.ts +95 -9
  11. package/corpus/core/src/templates/default/AGENTS.md +4 -0
  12. package/corpus/core/src/templates/default/package.json +2 -1
  13. package/corpus/core/src/templates/headless/AGENTS.md +4 -0
  14. package/corpus/core/src/templates/headless/package.json +1 -0
  15. package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -0
  16. package/corpus/core/src/templates/workspace-root/README.md +4 -0
  17. package/corpus/core/src/templates/workspace-root/package.json +1 -0
  18. package/corpus/templates/analytics/actions/update-dashboard.ts +3 -4
  19. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +144 -40
  20. package/corpus/templates/analytics/app/global.css +15 -13
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +9 -32
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +0 -26
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +0 -23
  24. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +65 -2
  25. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +3 -4
  26. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-charts-now-fill-sparse-template-time-series-suppor.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-rows-now-auto-fit-panels-so-one-two-or-three-chart.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-traffic-tables-now-show-top-urls-and-shared-clips-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +5 -0
  30. package/corpus/templates/analytics/changelog/2026-06-24-first-party-template-dashboards-now-include-repeat-users-ret.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-24-retention-charts-now-break-out-1-day-and-7-day-return-rates-.md +6 -0
  32. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +256 -44
  33. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +7 -2
  34. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +35 -8
  35. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +287 -42
  36. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +1 -1
  37. package/corpus/templates/calendar/server/plugins/auth.ts +12 -0
  38. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +7 -7
  39. package/corpus/templates/clips/AGENTS.md +6 -2
  40. package/corpus/templates/clips/app/components/capture-install-options.tsx +20 -4
  41. package/corpus/templates/clips/app/components/player/player-controls.tsx +62 -32
  42. package/corpus/templates/clips/app/lib/capture-install-options.ts +39 -5
  43. package/corpus/templates/clips/app/routes/download.tsx +2 -2
  44. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
  45. package/corpus/templates/clips/changelog/2026-06-24-inline-slack-playback-controls-no-longer-shift-when-adjustin.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-console-log-stream-all-le.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-fetch-xhr-network-request.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-06-24-the-chrome-extension-option-now-only-appears-on-supported-hosts.md +5 -0
  49. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +13 -8
  50. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  51. package/corpus/templates/clips/chrome-extension/scripts/package.ts +15 -2
  52. package/corpus/templates/clips/chrome-extension/src/overlay.css +4 -25
  53. package/corpus/templates/clips/chrome-extension/src/overlay.ts +1 -3
  54. package/corpus/templates/clips/server/lib/public-agent-context.ts +67 -24
  55. package/corpus/templates/mail/server/plugins/auth.ts +4 -1
  56. package/corpus/templates/plan/app/pages/PlansPage.tsx +271 -197
  57. package/corpus/templates/plan/server/plan-content.ts +45 -2
  58. package/dist/agent/durable-background.d.ts +9 -4
  59. package/dist/agent/durable-background.d.ts.map +1 -1
  60. package/dist/agent/durable-background.js +24 -11
  61. package/dist/agent/durable-background.js.map +1 -1
  62. package/dist/cli/create.js +1 -1
  63. package/dist/cli/create.js.map +1 -1
  64. package/dist/cli/skills.d.ts.map +1 -1
  65. package/dist/cli/skills.js +361 -19
  66. package/dist/cli/skills.js.map +1 -1
  67. package/dist/server/auth.d.ts +7 -0
  68. package/dist/server/auth.d.ts.map +1 -1
  69. package/dist/server/auth.js +1 -0
  70. package/dist/server/auth.js.map +1 -1
  71. package/dist/server/onboarding-html.d.ts +15 -0
  72. package/dist/server/onboarding-html.d.ts.map +1 -1
  73. package/dist/server/onboarding-html.js +73 -9
  74. package/dist/server/onboarding-html.js.map +1 -1
  75. package/dist/templates/default/AGENTS.md +4 -0
  76. package/dist/templates/default/package.json +2 -1
  77. package/dist/templates/headless/AGENTS.md +4 -0
  78. package/dist/templates/headless/package.json +1 -0
  79. package/dist/templates/workspace-root/AGENTS.md +4 -0
  80. package/dist/templates/workspace-root/README.md +4 -0
  81. package/dist/templates/workspace-root/package.json +1 -0
  82. package/docs/content/skills-guide.md +14 -0
  83. package/docs/content/template-clips.md +7 -1
  84. package/package.json +1 -1
  85. package/src/templates/default/AGENTS.md +4 -0
  86. package/src/templates/default/package.json +2 -1
  87. package/src/templates/headless/AGENTS.md +4 -0
  88. package/src/templates/headless/package.json +1 -0
  89. package/src/templates/workspace-root/AGENTS.md +4 -0
  90. package/src/templates/workspace-root/README.md +4 -0
  91. package/src/templates/workspace-root/package.json +1 -0
@@ -88,6 +88,18 @@ function fixed(sql: string): (window?: MetricWindow) => string {
88
88
  return () => sql;
89
89
  }
90
90
 
91
+ const SIGNUP_TEMPLATE_EXPR =
92
+ "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')";
93
+ const DASHBOARD_TIME_RANGE_FILTER =
94
+ "('{{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')))";
95
+ const DASHBOARD_EMAIL_FILTER =
96
+ "('{{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'))";
97
+ const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
98
+ 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, ${SIGNUP_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`;
99
+ const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${SIGNUP_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`;
100
+ const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, ${SIGNUP_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`;
101
+ const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${SIGNUP_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 ${SIGNUP_TEMPLATE_EXPR} ORDER BY count DESC`;
102
+
91
103
  /**
92
104
  * Catalog entries. Order here is the default panel order when a caller passes
93
105
  * metrics; callers can reorder by listing keys in the order they want.
@@ -96,19 +108,16 @@ const ENTRIES: FirstPartyMetric[] = [
96
108
  // --- Signups -------------------------------------------------------------
97
109
  {
98
110
  key: "total-signups",
99
- title: "Signups",
111
+ title: "Total Signups",
100
112
  chartType: "metric",
101
113
  source: "first-party",
102
114
  width: 1,
103
115
  windowed: false,
104
- buildSql: fixed(
105
- "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup'",
106
- ),
116
+ buildSql: fixed(TOTAL_SIGNUPS_SQL),
107
117
  config: {
108
- yKey: "count",
118
+ yKey: "signups",
109
119
  yFormatter: "number",
110
- description:
111
- "Better Auth user creation events from all Agent Native templates",
120
+ description: "Total signup events in the selected time range.",
112
121
  },
113
122
  },
114
123
  {
@@ -118,14 +127,18 @@ const ENTRIES: FirstPartyMetric[] = [
118
127
  source: "first-party",
119
128
  width: 2,
120
129
  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
- ),
130
+ buildSql: fixed(SIGNUPS_OVER_TIME_SQL),
124
131
  config: {
125
132
  xKey: "date",
126
133
  yKey: "count",
127
- color: "#8b5cf6",
128
- description: "Daily signup events across all Agent Native templates",
134
+ yFormatter: "number",
135
+ pivot: {
136
+ xKey: "date",
137
+ seriesKey: "template",
138
+ valueKey: "count",
139
+ },
140
+ stacked: true,
141
+ description: "Daily signup events stacked by inferred template/app.",
129
142
  },
130
143
  },
131
144
  {
@@ -133,16 +146,15 @@ const ENTRIES: FirstPartyMetric[] = [
133
146
  title: "Signups by Template",
134
147
  chartType: "bar",
135
148
  source: "first-party",
136
- width: 1,
149
+ width: 2,
137
150
  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
- ),
151
+ buildSql: fixed(SIGNUPS_BY_TEMPLATE_SQL),
141
152
  config: {
142
153
  xKey: "template",
143
154
  yKey: "count",
144
- color: "#8b5cf6",
145
- description: "Signup events grouped by inferred template/app",
155
+ yFormatter: "number",
156
+ color: "var(--brand-purple)",
157
+ description: "Signup events grouped by inferred template/app.",
146
158
  },
147
159
  },
148
160
 
@@ -253,45 +265,85 @@ const ENTRIES: FirstPartyMetric[] = [
253
265
  width: 2,
254
266
  windowed: false,
255
267
  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",
268
+ "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template",
257
269
  ),
258
- config: { xKey: "date", yKey: "count", color: "var(--brand-blue)" },
270
+ config: {
271
+ xKey: "date",
272
+ yKey: "count",
273
+ yFormatter: "number",
274
+ pivot: {
275
+ xKey: "date",
276
+ seriesKey: "template",
277
+ valueKey: "count",
278
+ },
279
+ stacked: true,
280
+ },
259
281
  },
260
282
  {
261
283
  key: "clicks-by-template",
262
- title: "Clicks by Template",
263
- chartType: "bar",
284
+ title: "Clicks by Template Over Time",
285
+ chartType: "area",
264
286
  source: "first-party",
265
- width: 1,
287
+ width: 2,
266
288
  windowed: false,
267
289
  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",
290
+ "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template",
269
291
  ),
270
- config: { xKey: "template", yKey: "count", color: "var(--brand-blue)" },
292
+ config: {
293
+ xKey: "date",
294
+ yKey: "count",
295
+ yFormatter: "number",
296
+ pivot: {
297
+ xKey: "date",
298
+ seriesKey: "template",
299
+ valueKey: "count",
300
+ },
301
+ stacked: true,
302
+ },
271
303
  },
272
304
  {
273
305
  key: "demo-clicks-by-template",
274
- title: "Try-Demo Clicks by Template",
275
- chartType: "bar",
306
+ title: "Try-Demo Clicks Over Time",
307
+ chartType: "area",
276
308
  source: "first-party",
277
- width: 1,
309
+ width: 2,
278
310
  windowed: false,
279
311
  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",
312
+ "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template",
281
313
  ),
282
- config: { xKey: "template", yKey: "count", color: "var(--brand-teal)" },
314
+ config: {
315
+ xKey: "date",
316
+ yKey: "count",
317
+ yFormatter: "number",
318
+ pivot: {
319
+ xKey: "date",
320
+ seriesKey: "template",
321
+ valueKey: "count",
322
+ },
323
+ stacked: true,
324
+ },
283
325
  },
284
326
  {
285
327
  key: "cli-copies-by-template",
286
- title: "CLI Copies by Template",
287
- chartType: "bar",
328
+ title: "CLI Copies by Template Over Time",
329
+ chartType: "area",
288
330
  source: "first-party",
289
- width: 1,
331
+ width: 2,
290
332
  windowed: false,
291
333
  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",
334
+ "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template",
293
335
  ),
294
- config: { xKey: "template", yKey: "count", color: "#06b6d4" },
336
+ config: {
337
+ xKey: "date",
338
+ yKey: "count",
339
+ yFormatter: "number",
340
+ pivot: {
341
+ xKey: "date",
342
+ seriesKey: "template",
343
+ valueKey: "count",
344
+ },
345
+ stacked: true,
346
+ },
295
347
  },
296
348
  {
297
349
  key: "cli-copies-over-time",
@@ -301,9 +353,19 @@ const ENTRIES: FirstPartyMetric[] = [
301
353
  width: 2,
302
354
  windowed: false,
303
355
  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",
356
+ "SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template",
305
357
  ),
306
- config: { xKey: "date", yKey: "count", color: "#06b6d4" },
358
+ config: {
359
+ xKey: "date",
360
+ yKey: "count",
361
+ yFormatter: "number",
362
+ pivot: {
363
+ xKey: "date",
364
+ seriesKey: "template",
365
+ valueKey: "count",
366
+ },
367
+ stacked: true,
368
+ },
307
369
  },
308
370
 
309
371
  // --- Activity / pageviews ------------------------------------------------
@@ -314,17 +376,200 @@ const ENTRIES: FirstPartyMetric[] = [
314
376
  source: "first-party",
315
377
  width: 2,
316
378
  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.
379
+ // Browser telemetry emits explicit `pageview` events with URL context, so
380
+ // keep pageview panels scoped to that event instead of all tracked events.
319
381
  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",
382
+ `SELECT substr(timestamp, 1, 10) AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY substr(timestamp, 1, 10), COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template`,
321
383
  ),
322
384
  config: {
323
385
  xKey: "date",
324
386
  yKey: "count",
325
- color: "var(--brand-blue)",
387
+ yFormatter: "number",
388
+ pivot: {
389
+ xKey: "date",
390
+ seriesKey: "template",
391
+ valueKey: "count",
392
+ },
393
+ stacked: true,
394
+ description: "Pageview events per day stacked by inferred template/app.",
395
+ },
396
+ },
397
+ {
398
+ key: "top-visited-urls",
399
+ title: "Top Visited URLs",
400
+ chartType: "table",
401
+ source: "first-party",
402
+ width: 2,
403
+ windowed: false,
404
+ buildSql: fixed(
405
+ `WITH pageviews AS (SELECT COALESCE(CASE WHEN NULLIF(hostname, '') IS NOT NULL THEN 'https://' || hostname || COALESCE(NULLIF(path, ''), '/') END, NULLIF(url, ''), NULLIF(path, ''), 'unknown') AS url, CASE WHEN NULLIF(hostname, '') IS NOT NULL THEN 'https://' || hostname || COALESCE(NULLIF(path, ''), '/') WHEN NULLIF(url, '') LIKE 'http%' THEN url ELSE COALESCE(NULLIF(url, ''), NULLIF(path, ''), '') END AS href, COUNT(*) AS views, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users, MAX(substr(timestamp, 1, 10)) AS last_seen FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY 1, 2) SELECT url, views, users, last_seen, href FROM pageviews ORDER BY views DESC LIMIT 25`,
406
+ ),
407
+ config: {
408
+ description:
409
+ "Most-viewed URLs in the selected time range. Links open in a new tab.",
410
+ sortable: true,
411
+ limit: 25,
412
+ columns: [
413
+ { key: "url", label: "URL", format: "link", linkKey: "href" },
414
+ { key: "views", label: "Views", format: "number" },
415
+ { key: "users", label: "Users", format: "number" },
416
+ { key: "last_seen", label: "Last seen", format: "date" },
417
+ { key: "href", hidden: true },
418
+ ],
419
+ },
420
+ },
421
+ {
422
+ key: "top-visited-clips",
423
+ title: "Top Visited Clips",
424
+ chartType: "table",
425
+ source: "first-party",
426
+ width: 2,
427
+ windowed: false,
428
+ buildSql: fixed(
429
+ `WITH clip_events AS (SELECT COALESCE(NULLIF(properties::jsonb ->> 'recording_id', ''), NULLIF(path, ''), NULLIF(url, ''), 'unknown') AS clip_key, CASE WHEN NULLIF(url, '') LIKE 'http%' THEN url WHEN NULLIF(hostname, '') IS NOT NULL THEN 'https://' || hostname || COALESCE(NULLIF(path, ''), '/') WHEN NULLIF(path, '') LIKE '/%' THEN 'https://clips.agent-native.com' || path ELSE COALESCE(NULLIF(url, ''), '') END AS href, user_id, anonymous_id, timestamp FROM analytics_events WHERE event_name = 'share_view' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND COALESCE(NULLIF(properties::jsonb ->> 'surface', ''), 'clip') = 'clip'), clip_views AS (SELECT clip_key, COALESCE(MAX(NULLIF(href, '')), clip_key) AS href, COUNT(*) AS views, COUNT(DISTINCT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) AS users, MAX(substr(timestamp, 1, 10)) AS last_seen FROM clip_events GROUP BY clip_key) SELECT CASE WHEN href LIKE 'http%' THEN href ELSE clip_key END AS clip, views, users, last_seen, href FROM clip_views ORDER BY views DESC LIMIT 25`,
430
+ ),
431
+ config: {
432
+ description: "Most-viewed shared Clips pages in the selected time range.",
433
+ sortable: true,
434
+ limit: 25,
435
+ columns: [
436
+ { key: "clip", label: "Clip URL", format: "link", linkKey: "href" },
437
+ { key: "views", label: "Views", format: "number" },
438
+ { key: "users", label: "Users", format: "number" },
439
+ { key: "last_seen", label: "Last seen", format: "date" },
440
+ { key: "href", hidden: true },
441
+ ],
442
+ },
443
+ },
444
+ {
445
+ key: "repeat-users",
446
+ title: "Repeat Users",
447
+ chartType: "metric",
448
+ source: "first-party",
449
+ width: 1,
450
+ windowed: false,
451
+ buildSql: fixed(
452
+ "WITH user_days AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, COUNT(DISTINCT substr(timestamp, 1, 10)) AS active_days 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 COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))) SELECT COUNT(*) AS count FROM user_days WHERE active_days >= 2",
453
+ ),
454
+ config: {
455
+ yKey: "count",
456
+ yFormatter: "number",
457
+ description:
458
+ "Distinct users with activity on at least two days in the selected time range. Uses user_id first, then anonymous_id.",
459
+ },
460
+ },
461
+ {
462
+ key: "retention-over-time",
463
+ title: "1d / 7d Retention Over Time",
464
+ chartType: "line",
465
+ source: "first-party",
466
+ width: 3,
467
+ windowed: false,
468
+ buildSql: fixed(
469
+ "WITH base AS (SELECT COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')) AS user_key, 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'))), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), cohorts AS (SELECT user_key, 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')))), retention AS (SELECT c.cohort_date AS date, '1d retention' AS period, 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 GROUP BY c.cohort_date UNION ALL SELECT c.cohort_date AS date, '7d retention' AS period, 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 GROUP BY c.cohort_date) SELECT date, period, rate FROM retention ORDER BY date, period",
470
+ ),
471
+ config: {
472
+ xKey: "date",
473
+ yKey: "rate",
474
+ yFormatter: "percent",
475
+ pivot: {
476
+ xKey: "date",
477
+ seriesKey: "period",
478
+ valueKey: "rate",
479
+ },
480
+ colors: ["#10b981", "#8b5cf6"],
481
+ description:
482
+ "First-seen daily cohorts in the selected range. Cohorts must be at least seven days old so both lines are mature.",
483
+ },
484
+ },
485
+ {
486
+ key: "one-day-retention-by-template",
487
+ title: "1d Retention by Template",
488
+ chartType: "line",
489
+ source: "first-party",
490
+ width: 2,
491
+ windowed: false,
492
+ buildSql: fixed(ONE_DAY_RETENTION_BY_TEMPLATE_SQL),
493
+ config: {
494
+ xKey: "date",
495
+ yKey: "rate",
496
+ yFormatter: "percent",
497
+ pivot: {
498
+ xKey: "date",
499
+ seriesKey: "template",
500
+ valueKey: "rate",
501
+ },
502
+ description:
503
+ "Per-template first-seen cohorts in the selected range. A user is retained when they return to the same template the next day.",
504
+ },
505
+ },
506
+ {
507
+ key: "seven-day-retention-by-template",
508
+ title: "7d Retention by Template",
509
+ chartType: "line",
510
+ source: "first-party",
511
+ width: 2,
512
+ windowed: false,
513
+ buildSql: fixed(SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL),
514
+ config: {
515
+ xKey: "date",
516
+ yKey: "rate",
517
+ yFormatter: "percent",
518
+ pivot: {
519
+ xKey: "date",
520
+ seriesKey: "template",
521
+ valueKey: "rate",
522
+ },
523
+ description:
524
+ "Per-template first-seen cohorts in the selected range. A user is retained when they return to the same template seven days later.",
525
+ },
526
+ },
527
+ {
528
+ key: "dau-over-time",
529
+ title: "DAU by Template",
530
+ chartType: "area",
531
+ source: "first-party",
532
+ width: 2,
533
+ windowed: false,
534
+ buildSql: fixed(
535
+ "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",
536
+ ),
537
+ config: {
538
+ xKey: "date",
539
+ yKey: "users",
540
+ yFormatter: "number",
541
+ pivot: {
542
+ xKey: "date",
543
+ seriesKey: "template",
544
+ valueKey: "users",
545
+ },
546
+ stacked: true,
547
+ description:
548
+ "Distinct active users per day in the selected time range, stacked by template. Uses user_id first, then anonymous_id.",
549
+ },
550
+ },
551
+ {
552
+ key: "wau-over-time",
553
+ title: "WAU by Template",
554
+ chartType: "area",
555
+ source: "first-party",
556
+ width: 2,
557
+ windowed: false,
558
+ buildSql: fixed(
559
+ "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",
560
+ ),
561
+ config: {
562
+ xKey: "date",
563
+ yKey: "users",
564
+ yFormatter: "number",
565
+ pivot: {
566
+ xKey: "date",
567
+ seriesKey: "template",
568
+ valueKey: "users",
569
+ },
570
+ stacked: true,
326
571
  description:
327
- "Total first-party tracked events per day (activity proxy there is no distinct pageview event in /track data).",
572
+ "Trailing 7-day distinct active users for each active date in the selected time range, stacked by template.",
328
573
  },
329
574
  },
330
575
 
@@ -3,4 +3,4 @@ type: added
3
3
  date: 2026-06-23
4
4
  ---
5
5
 
6
- Added a heads-up explaining Google's 'app not verified' screen when connecting your calendar, so you know it's safe to continue
6
+ Added a heads-up explaining Google's 'app not verified' screen when signing in or connecting your calendar, so you know it's safe to continue
@@ -16,6 +16,18 @@ export default createAuthPlugin({
16
16
  "Manages availability and booking links automatically",
17
17
  "Answers schedule questions and resolves conflicts instantly",
18
18
  ],
19
+ runLocalCommand:
20
+ "npx @agent-native/core@latest create my-calendar-app --template calendar",
21
+ },
22
+ googleSignInNotice: {
23
+ host: "calendar.agent-native.com",
24
+ title: "Google may show a warning",
25
+ body: [
26
+ "You'll see this screen because this demo uses Agent-Native's Google app, not a Google-reviewed public app.",
27
+ "It's safe to continue: click Advanced, then “Go to … (unsafe)” to finish signing in.",
28
+ ],
29
+ continueLabel: "Continue to Google",
30
+ cancelLabel: "Run locally",
19
31
  },
20
32
  publicPaths: [
21
33
  "/book",
@@ -57,13 +57,13 @@ the recorder page itself. The Clips Chrome extension is the active-tab path for
57
57
  browser logs: it launches `/record` with an extension capture session and passes
58
58
  `developerLogs=1/0`, then saves diagnostics with source `extension`.
59
59
 
60
- Keep the public Chrome extension option hidden until the Web Store listing is
61
- live. UI prompts that otherwise say "Download desktop app" should use the shared
62
- install-choice component so `VITE_CLIPS_CHROME_EXTENSION_ENABLED=1` can reveal
63
- two options: Chrome extension for browser logs, and desktop app for the most
64
- seamless native capture. Also set `VITE_CLIPS_CHROME_EXTENSION_URL` when the
65
- extension URL exists; leave the boolean unset while preparing/submitting the
66
- listing.
60
+ The Web Store listing is live, so the public Chrome extension option shows by
61
+ default. UI prompts that otherwise say "Download desktop app" use the shared
62
+ install-choice component (`CaptureInstallButton` / `CaptureInstallInlineLink`)
63
+ to offer two options: Chrome extension for browser logs, and desktop app for the
64
+ most seamless native capture. Set `VITE_CLIPS_CHROME_EXTENSION_ENABLED=0` to hide
65
+ the Chrome option again, or `VITE_CLIPS_CHROME_EXTENSION_URL` to point at a
66
+ different listing.
67
67
 
68
68
  ## Pause / resume
69
69
 
@@ -65,8 +65,12 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
65
65
  the recording session. `save-browser-diagnostics` is UI/internal and stores
66
66
  bounded console logs plus fetch/XHR method, URL path/query keys, status, and
67
67
  duration; it never captures headers, bodies, cookies, or query values. Use
68
- `get-recording-player-data` for full diagnostics when you have editor access;
69
- public agent context only exposes a compact issue summary.
68
+ `get-recording-player-data` for full diagnostics when you have editor access.
69
+ Public agent context exposes the redacted console stream (all levels) as
70
+ `browserDiagnostics.consoleLogs` and the fetch/XHR stream as
71
+ `browserDiagnostics.networkRequests` (method, sanitized URL with query values
72
+ redacted, status, duration), plus `consoleIssues` and `failedNetworkRequests`
73
+ highlights. All bounded; page URL, headers, bodies, and cookies stay omitted.
70
74
  - The Chrome extension lives in `chrome-extension/`. It launches `/record` with
71
75
  `clipsExtensionId` and `clipsCaptureSessionId`, then the recorder sends
72
76
  `CLIPS_CAPTURE_START/STOP/CANCEL` back to the extension. The extension uses
@@ -1,7 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { Link } from "react-router";
3
3
  import {
4
- IconBrowser,
4
+ IconBrandApple,
5
+ IconBrandChrome,
6
+ IconBrandWindows,
5
7
  IconChevronDown,
6
8
  IconDeviceDesktop,
7
9
  IconExternalLink,
@@ -35,8 +37,22 @@ type CaptureInstallInlineLinkProps = PopoverPlacement & {
35
37
  desktopHref?: string;
36
38
  };
37
39
 
40
+ /**
41
+ * The desktop-app tile shows the icon for the visitor's current OS — Apple on
42
+ * macOS, Windows on Windows — and falls back to a neutral desktop glyph on other
43
+ * platforms or during SSR. The Chrome tile always uses the Chrome brand icon.
44
+ */
45
+ function desktopOsIcon(): typeof IconDeviceDesktop {
46
+ if (typeof navigator === "undefined") return IconDeviceDesktop;
47
+ const ua = navigator.userAgent;
48
+ if (/Windows/i.test(ua)) return IconBrandWindows;
49
+ if (/Mac|iPhone|iPad/i.test(ua)) return IconBrandApple;
50
+ return IconDeviceDesktop;
51
+ }
52
+
38
53
  function InstallOptionsContent({ desktopHref = "/download" }) {
39
54
  const chromeAvailable = Boolean(clipsChromeExtensionUrl);
55
+ const DesktopIcon = desktopOsIcon();
40
56
 
41
57
  return (
42
58
  <div className="grid gap-2">
@@ -57,7 +73,7 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
57
73
  rel="noreferrer"
58
74
  className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
59
75
  >
60
- <IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
76
+ <IconBrandChrome className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
61
77
  <span className="min-w-0 flex-1">
62
78
  <span className="block text-sm font-medium">Chrome extension</span>
63
79
  <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
@@ -69,7 +85,7 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
69
85
  </a>
70
86
  ) : (
71
87
  <div className="flex items-start gap-3 rounded-md border border-dashed border-border p-3 text-left opacity-70">
72
- <IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
88
+ <IconBrandChrome className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
73
89
  <span className="min-w-0 flex-1">
74
90
  <span className="block text-sm font-medium">Chrome extension</span>
75
91
  <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
@@ -83,7 +99,7 @@ function InstallOptionsContent({ desktopHref = "/download" }) {
83
99
  to={desktopHref}
84
100
  className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
85
101
  >
86
- <IconDeviceDesktop className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
102
+ <DesktopIcon className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
87
103
  <span className="min-w-0 flex-1">
88
104
  <span className="block text-sm font-medium">Desktop app</span>
89
105
  <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">