@agent-native/core 0.167.1 → 0.168.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 (117) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/agent-native-docs/SKILL.md +11 -0
  3. package/corpus/templates/assets/.agents/skills/agent-native-docs/SKILL.md +11 -0
  4. package/corpus/templates/brain/.agents/skills/agent-native-docs/SKILL.md +11 -0
  5. package/corpus/templates/brain/actions/list-sources.ts +17 -7
  6. package/corpus/templates/brain/app/components/brain/Surface.tsx +4 -2
  7. package/corpus/templates/brain/app/lib/brain.ts +13 -3
  8. package/corpus/templates/brain/server/lib/brain-health.ts +5 -8
  9. package/corpus/templates/brain/server/lib/brain.ts +1 -0
  10. package/corpus/templates/brain/server/lib/capture-sanitization.ts +5 -0
  11. package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +3 -2
  12. package/corpus/templates/calendar/.agents/skills/agent-native-docs/SKILL.md +11 -0
  13. package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  14. package/corpus/templates/clips/.agents/skills/agent-native-docs/SKILL.md +11 -0
  15. package/corpus/templates/clips/app/routes/download.tsx +78 -131
  16. package/corpus/templates/clips/desktop/src/app.tsx +11 -10
  17. package/corpus/templates/clips/desktop/src/lib/desktop-oauth-window.ts +23 -0
  18. package/corpus/templates/content/.agents/skills/agent-native-docs/SKILL.md +11 -0
  19. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +11 -0
  20. package/corpus/templates/design/.agents/skills/agent-native-docs/SKILL.md +11 -0
  21. package/corpus/templates/dispatch/.agents/skills/agent-native-docs/SKILL.md +11 -0
  22. package/corpus/templates/factory/.agents/skills/agent-native-docs/SKILL.md +11 -0
  23. package/corpus/templates/forms/.agents/skills/agent-native-docs/SKILL.md +11 -0
  24. package/corpus/templates/macros/.agents/skills/agent-native-docs/SKILL.md +11 -0
  25. package/corpus/templates/mail/.agents/skills/agent-native-docs/SKILL.md +11 -0
  26. package/corpus/templates/plan/.agents/skills/agent-native-docs/SKILL.md +11 -0
  27. package/corpus/templates/slides/.agents/skills/agent-native-docs/SKILL.md +11 -0
  28. package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +11 -0
  29. package/dist/chat-threads/store.d.ts +7 -0
  30. package/dist/chat-threads/store.js +15 -0
  31. package/dist/client/AboutAgentNativeDialog.d.ts +5 -0
  32. package/dist/client/AboutAgentNativeDialog.js +56 -0
  33. package/dist/client/AgentPanel.d.ts +5 -1
  34. package/dist/client/AgentPanel.js +8 -8
  35. package/dist/client/AssistantChat.d.ts +3 -1
  36. package/dist/client/AssistantChat.js +22 -6
  37. package/dist/client/CommandMenu.d.ts +6 -1
  38. package/dist/client/CommandMenu.js +46 -3
  39. package/dist/client/EnvironmentBadge.js +4 -1
  40. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  41. package/dist/client/MultiTabAssistantChat.js +5 -4
  42. package/dist/client/agent-native-version.d.ts +12 -0
  43. package/dist/client/agent-native-version.js +36 -0
  44. package/dist/client/agent-page/AgentJobsTab.js +39 -6
  45. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +10 -1
  46. package/dist/client/agent-page/AutomationScheduleDialog.js +3 -2
  47. package/dist/client/agent-page/ScheduledTriggerNotice.d.ts +22 -0
  48. package/dist/client/agent-page/ScheduledTriggerNotice.js +84 -0
  49. package/dist/client/agent-page/scheduled-trigger-state.d.ts +28 -0
  50. package/dist/client/agent-page/scheduled-trigger-state.js +10 -0
  51. package/dist/client/agent-page/use-jobs.d.ts +7 -0
  52. package/dist/client/agent-page/use-jobs.js +14 -0
  53. package/dist/client/blocks/library/diagram.config.d.ts +4 -1
  54. package/dist/client/blocks/library/diagram.js +1 -1
  55. package/dist/client/chat/run-recovery.js +15 -7
  56. package/dist/client/use-chat-threads.d.ts +7 -0
  57. package/dist/client/use-chat-threads.js +144 -40
  58. package/dist/collab/routes.d.ts +1 -1
  59. package/dist/config.d.ts +3 -1
  60. package/dist/config.js +14 -9
  61. package/dist/deploy/build.d.ts +5 -0
  62. package/dist/deploy/build.js +12 -1
  63. package/dist/framework-tools.js +1 -0
  64. package/dist/jobs/actions/get-scheduled-trigger-status.d.ts +13 -0
  65. package/dist/jobs/actions/get-scheduled-trigger-status.js +21 -0
  66. package/dist/localization/core-messages/ar-SA.js +6 -0
  67. package/dist/localization/core-messages/de-DE.js +6 -0
  68. package/dist/localization/core-messages/en-US.d.ts +6 -0
  69. package/dist/localization/core-messages/en-US.js +6 -0
  70. package/dist/localization/core-messages/es-ES.js +6 -0
  71. package/dist/localization/core-messages/fr-FR.js +6 -0
  72. package/dist/localization/core-messages/hi-IN.js +6 -0
  73. package/dist/localization/core-messages/ja-JP.js +6 -0
  74. package/dist/localization/core-messages/ko-KR.js +6 -0
  75. package/dist/localization/core-messages/pt-BR.js +6 -0
  76. package/dist/localization/core-messages/zh-CN.js +6 -0
  77. package/dist/localization/core-messages/zh-TW.js +6 -0
  78. package/dist/localization/core-messages.d.ts +6 -0
  79. package/dist/localization/default-messages.d.ts +14 -0
  80. package/dist/localization/default-messages.js +14 -0
  81. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  82. package/dist/notifications/routes.d.ts +1 -1
  83. package/dist/observability/routes.d.ts +3 -3
  84. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  85. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  86. package/dist/resources/handlers.d.ts +2 -2
  87. package/dist/server/action-discovery.js +4 -0
  88. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +42 -1
  89. package/dist/server/agent-chat/recurring-jobs-runtime.js +102 -9
  90. package/dist/server/agent-chat-plugin.d.ts +3 -1
  91. package/dist/server/agent-chat-plugin.js +115 -24
  92. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  93. package/dist/server/attribution.d.ts +29 -0
  94. package/dist/server/attribution.js +68 -0
  95. package/dist/server/auth.js +58 -15
  96. package/dist/server/better-auth-instance.js +16 -4
  97. package/dist/server/identity-sso.js +14 -2
  98. package/dist/server/realtime-token.d.ts +1 -1
  99. package/dist/server/request-context.d.ts +8 -0
  100. package/dist/server/request-context.js +4 -0
  101. package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  102. package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
  103. package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
  104. package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
  105. package/dist/vite/agent-native-config-loader.js +1 -1
  106. package/dist/vite/client.js +19 -8
  107. package/dist/vite/package-versions.d.ts +1 -0
  108. package/dist/vite/package-versions.js +97 -0
  109. package/docs/content/agent-native-config.mdx +55 -31
  110. package/docs/content/deployment.mdx +3 -1
  111. package/docs/content/environment-variables.mdx +51 -14
  112. package/docs/content/recurring-jobs.mdx +6 -0
  113. package/package.json +2 -2
  114. package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +11 -0
  115. package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +11 -0
  116. package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +11 -0
  117. package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +11 -0
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8490
34
+ - template files: 8491
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.
@@ -6,6 +6,7 @@ import { z } from "zod";
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import { nextBrainSourceSyncAt } from "../server/jobs/sync-sources.js";
8
8
  import { listAccessibleAudienceIds } from "../server/lib/audiences.js";
9
+ import { sourceHealthState } from "../server/lib/brain-health.js";
9
10
  import { parseJson, serializeSource } from "../server/lib/brain.js";
10
11
  import { sourceProviderSchema } from "./_schemas.js";
11
12
 
@@ -71,14 +72,23 @@ export default defineAction({
71
72
  .from(schema.brainSources)
72
73
  .where(and(...clauses))
73
74
  .orderBy(desc(schema.brainSources.updatedAt));
75
+ const nowMs = Date.now();
74
76
  const sources = await Promise.all(
75
- rows.map(async (row) => ({
76
- ...serializeSource(row),
77
- recordCount: await sourceRecordCount(row.id),
78
- latestRun: await latestRun(row.id),
79
- nextSyncAt: nextBrainSourceSyncAt(row),
80
- })),
77
+ rows.map(async (row) => {
78
+ const latest = await latestRun(row.id);
79
+ const nextSyncAt = nextBrainSourceSyncAt(row);
80
+ return {
81
+ ...serializeSource(row),
82
+ recordCount: await sourceRecordCount(row.id),
83
+ latestRun: latest,
84
+ nextSyncAt,
85
+ health: sourceHealthState(row, latest, nextSyncAt, nowMs),
86
+ };
87
+ }),
81
88
  );
82
- return { count: rows.length, sources };
89
+ return {
90
+ count: rows.length,
91
+ sources,
92
+ };
83
93
  },
84
94
  });
@@ -117,7 +117,8 @@ export function StatusBadge({ status }: { status: string }) {
117
117
  ? IconCircleCheck
118
118
  : normalized.includes("review") ||
119
119
  normalized.includes("degraded") ||
120
- normalized.includes("stale")
120
+ normalized.includes("stale") ||
121
+ normalized.includes("needs")
121
122
  ? IconClock
122
123
  : normalized.includes("error")
123
124
  ? IconAlertTriangle
@@ -132,7 +133,8 @@ export function StatusBadge({ status }: { status: string }) {
132
133
  "border-border bg-secondary text-secondary-foreground",
133
134
  (normalized.includes("review") ||
134
135
  normalized.includes("degraded") ||
135
- normalized.includes("stale")) &&
136
+ normalized.includes("stale") ||
137
+ normalized.includes("needs")) &&
136
138
  "border-amber-500/35 bg-amber-500/10 text-amber-700 dark:text-amber-300",
137
139
  normalized.includes("error") &&
138
140
  "border-destructive/35 bg-destructive/10 text-destructive",
@@ -20,7 +20,14 @@ export type BrainView =
20
20
  | "settings";
21
21
 
22
22
  export type KnowledgeStatus = "approved" | "needs_review" | "draft" | "stale";
23
- export type SourceHealth = "healthy" | "degraded" | "paused" | "error";
23
+ export type SourceHealth =
24
+ | "healthy"
25
+ | "needs_setup"
26
+ | "needs_sync"
27
+ | "stale"
28
+ | "degraded"
29
+ | "paused"
30
+ | "error";
24
31
  export type ReviewPriority = "high" | "medium" | "low";
25
32
 
26
33
  export interface Citation {
@@ -1144,8 +1151,11 @@ export function sourceDescription(source: BrainSource) {
1144
1151
  export function sourceHealth(source: BrainSource): SourceHealth {
1145
1152
  if (source.health) return source.health;
1146
1153
  if (sourceRetryAfter(source)) return "degraded";
1147
- if (source.status === "active")
1148
- return source.lastError ? "degraded" : "healthy";
1154
+ if (source.status === "active") {
1155
+ if (source.lastError) return "degraded";
1156
+ if (sourceAutoSync(source) && !source.lastSyncedAt) return "needs_sync";
1157
+ return "healthy";
1158
+ }
1149
1159
  if (source.status === "error") return "error";
1150
1160
  if (source.status === "paused" || source.status === "archived")
1151
1161
  return "paused";
@@ -189,9 +189,9 @@ function latestIso(values: Array<string | null | undefined>) {
189
189
  return latest;
190
190
  }
191
191
 
192
- function sourceHealthState(
192
+ export function sourceHealthState(
193
193
  source: SourceRow,
194
- latestRun: SyncRunRow | null,
194
+ latestRun: Pick<SyncRunRow, "status"> | null,
195
195
  nextSyncAt: string | null,
196
196
  nowMs: number,
197
197
  ): BrainSourceHealthState {
@@ -530,12 +530,9 @@ export async function readBrainHealth() {
530
530
  };
531
531
  });
532
532
 
533
- const sourceHealthCounts = countStatuses<
534
- BrainSourceHealthState | "active" | "paused" | "error"
535
- >([
536
- ...sourceSummaries.map((source) => ({ status: source.health })),
537
- ...sourceRows.map((source) => ({ status: source.status })),
538
- ]);
533
+ const sourceHealthCounts = countStatuses<BrainSourceHealthState>(
534
+ sourceSummaries.map((source) => ({ status: source.health })),
535
+ );
539
536
  const captureCounts = countStatuses(captureRows);
540
537
  const captureDispositionCounts = countStatuses(
541
538
  captureRows.map((capture) => ({
@@ -755,6 +755,7 @@ export async function createCapture(values: {
755
755
  title: source.title,
756
756
  provider: source.provider as BrainSourceProvider,
757
757
  ownerEmail: source.ownerEmail,
758
+ orgId: source.orgId,
758
759
  },
759
760
  sourceConfig: parseJson<Record<string, unknown>>(source.configJson, {}),
760
761
  settings,
@@ -71,6 +71,7 @@ export interface CaptureSanitizationInput {
71
71
  title: string;
72
72
  provider: BrainSourceProvider;
73
73
  ownerEmail: string;
74
+ orgId?: string | null;
74
75
  };
75
76
  sourceConfig?: Record<string, unknown>;
76
77
  settings: BrainSettings;
@@ -440,6 +441,10 @@ async function classifyWithApprovedModel(
440
441
  apiKeyEnvVar: userApiKey.apiKeyEnvVar,
441
442
  appId: "brain",
442
443
  engineOption: engineName,
444
+ credentialIdentity: {
445
+ userEmail: input.source.ownerEmail,
446
+ orgId: input.source.orgId ?? undefined,
447
+ },
443
448
  });
444
449
 
445
450
  const maxChars = Math.min(
@@ -10,7 +10,8 @@ declare global {
10
10
  var __AGENT_NATIVE_BRAIN_SCHEDULED_RUNTIME__: boolean | undefined;
11
11
  }
12
12
 
13
- export const BRAIN_QUEUE_SWEEP_LIMIT = 10;
13
+ export const BRAIN_QUEUE_SWEEP_LIMIT = 25;
14
+ export const BRAIN_MAX_DISTILLATIONS_PER_SWEEP = 2;
14
15
 
15
16
  function productionLike() {
16
17
  return (
@@ -53,7 +54,7 @@ export default defineEventHandler(async (event) => {
53
54
  processBrainIngestQueueOnce({
54
55
  limit: BRAIN_QUEUE_SWEEP_LIMIT,
55
56
  runDistillation: true,
56
- maxDistillations: 1,
57
+ maxDistillations: BRAIN_MAX_DISTILLATIONS_PER_SWEEP,
57
58
  }),
58
59
  expireSensitivityQuarantines(),
59
60
  ]);
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.
@@ -4,20 +4,12 @@ import {
4
4
  IconBrandChrome,
5
5
  IconBrandApple,
6
6
  IconBrandWindows,
7
- IconChevronDown,
8
- IconDeviceDesktop,
9
7
  IconExternalLink,
8
+ IconTerminal2,
10
9
  } from "@tabler/icons-react";
11
10
  import { useEffect, useState } from "react";
12
11
 
13
12
  import { Button } from "@/components/ui/button";
14
- import {
15
- DropdownMenu,
16
- DropdownMenuContent,
17
- DropdownMenuRadioGroup,
18
- DropdownMenuRadioItem,
19
- DropdownMenuTrigger,
20
- } from "@/components/ui/dropdown-menu";
21
13
  import { Skeleton } from "@/components/ui/skeleton";
22
14
  import enMessages from "@/i18n/en-US";
23
15
  import {
@@ -45,7 +37,6 @@ type PlatformId = "mac" | "windows" | "linux";
45
37
  interface PlatformVariant {
46
38
  id: PlatformId;
47
39
  label: string;
48
- sublabel: string;
49
40
  assetKinds: readonly (
50
41
  | "mac-universal"
51
42
  | "mac-arm64"
@@ -64,23 +55,20 @@ const VARIANTS: PlatformVariant[] = [
64
55
  {
65
56
  id: "mac",
66
57
  label: "macOS",
67
- sublabel: "Universal (Apple Silicon + Intel)",
68
58
  assetKinds: ["mac-universal", "mac-arm64", "mac-x64"],
69
59
  icon: IconBrandApple,
70
60
  },
71
61
  {
72
62
  id: "windows",
73
63
  label: "Windows",
74
- sublabel: "64-bit MSI installer",
75
64
  assetKinds: ["windows-msi"],
76
65
  icon: IconBrandWindows,
77
66
  },
78
67
  {
79
68
  id: "linux",
80
69
  label: "Linux",
81
- sublabel: "AppImage, Debian, and RPM packages",
82
70
  assetKinds: ["linux-appimage", "linux-deb", "linux-rpm"],
83
- icon: IconDeviceDesktop,
71
+ icon: IconTerminal2,
84
72
  },
85
73
  ];
86
74
 
@@ -171,47 +159,6 @@ function primaryDownloadButton(
171
159
  );
172
160
  }
173
161
 
174
- function secondaryDownloadButton(
175
- variant: PlatformVariant,
176
- manifest: Manifest | null,
177
- manifestError: boolean,
178
- downloadLabel: string,
179
- retryLabel: string,
180
- onRetry: () => void,
181
- ) {
182
- const asset = pickAsset(manifest, variant);
183
- const Icon = variant.icon;
184
- const className =
185
- "h-auto gap-1.5 px-2 py-1 text-sm font-normal text-muted-foreground hover:bg-transparent hover:text-foreground";
186
- if (asset) {
187
- return (
188
- <Button asChild variant="ghost" className={className}>
189
- <a href={asset.url} download onClick={markDesktopAppDownloaded}>
190
- <Icon className="h-4 w-4" />
191
- {downloadLabel}
192
- </a>
193
- </Button>
194
- );
195
- }
196
- if (manifest === null && !manifestError) {
197
- return <Skeleton className="h-7 w-[208px] rounded-md" />;
198
- }
199
- if (manifestError) {
200
- return (
201
- <Button variant="ghost" className={className} onClick={onRetry}>
202
- <Icon className="h-4 w-4" />
203
- {retryLabel}
204
- </Button>
205
- );
206
- }
207
- return (
208
- <Button variant="ghost" className={className} disabled>
209
- <Icon className="h-4 w-4" />
210
- {downloadLabel}
211
- </Button>
212
- );
213
- }
214
-
215
162
  export default function DownloadPage() {
216
163
  const chromeExtensionEnabled = useClipsChromeExtensionEnabled();
217
164
  const t = useT();
@@ -267,7 +214,11 @@ export default function DownloadPage() {
267
214
  };
268
215
 
269
216
  const primary = VARIANTS.find((v) => v.id === detected) ?? VARIANTS[0];
270
- const secondary = VARIANTS.filter((v) => v.id !== primary.id);
217
+
218
+ const handlePlatformChange = (nextPlatform: PlatformId) => {
219
+ if (nextPlatform === detected) return;
220
+ setDetected(nextPlatform);
221
+ };
271
222
 
272
223
  return (
273
224
  <div className="min-h-screen bg-background text-foreground">
@@ -317,87 +268,83 @@ export default function DownloadPage() {
317
268
  {t("downloadRoute.heroDescription")}
318
269
  </p>
319
270
 
320
- <div className="mt-10 flex flex-col items-center gap-3">
321
- {primaryDownloadButton(
322
- primary,
323
- manifest,
324
- manifestError,
325
- t("downloadRoute.downloadFor", { platform: primary.label }),
326
- t("downloadRoute.retry"),
327
- retryManifest,
328
- )}
329
- {secondary.map((variant) => (
330
- <div key={variant.id}>
331
- {secondaryDownloadButton(
332
- variant,
333
- manifest,
334
- manifestError,
335
- t("downloadRoute.alsoFor", { platform: variant.label }),
336
- t("downloadRoute.retry"),
337
- retryManifest,
338
- )}
339
- </div>
340
- ))}
341
- <div className="text-xs text-muted-foreground">
342
- {manifest ? (
343
- <>
344
- {manifest.pub_date
345
- ? t("downloadRoute.versionReleased", {
346
- version: manifest.version,
347
- date: new Date(manifest.pub_date).toLocaleDateString(),
348
- })
349
- : t("downloadRoute.version", {
350
- version: manifest.version,
351
- })}
352
- </>
353
- ) : manifestError ? (
354
- <>{t("downloadRoute.manifestError")}</>
355
- ) : (
356
- <>{t("downloadRoute.loadingRelease")}</>
357
- )}
271
+ <div className="mt-10 flex flex-col items-center">
272
+ <div className="mb-2 flex justify-center gap-2">
273
+ {VARIANTS.map((variant) => {
274
+ const Icon = variant.icon;
275
+ const active = primary.id === variant.id;
276
+ return (
277
+ <button
278
+ key={variant.id}
279
+ type="button"
280
+ aria-label={variant.label}
281
+ aria-pressed={active}
282
+ onClick={() => handlePlatformChange(variant.id)}
283
+ className={`group flex items-center justify-center rounded-lg p-4 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
284
+ active
285
+ ? "text-foreground"
286
+ : "text-muted-foreground opacity-40 hover:opacity-65"
287
+ }`}
288
+ >
289
+ <Icon className="size-6" aria-hidden="true" />
290
+ </button>
291
+ );
292
+ })}
358
293
  </div>
359
- <div className="mt-2 flex items-center justify-center text-xs text-muted-foreground">
360
- <DropdownMenu>
361
- <DropdownMenuTrigger asChild>
362
- <Button
294
+
295
+ <div className="mx-auto mt-8 max-w-2xl text-center">
296
+ {primaryDownloadButton(
297
+ primary,
298
+ manifest,
299
+ manifestError,
300
+ t("downloadRoute.downloadFor", { platform: primary.label }),
301
+ t("downloadRoute.retry"),
302
+ retryManifest,
303
+ )}
304
+
305
+ <div className="mt-5 flex justify-center">
306
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
307
+ <span>{t("downloadRoute.stable")}</span>
308
+ <button
363
309
  type="button"
364
- variant="ghost"
365
- size="sm"
366
- className="h-7 gap-1.5 px-2 font-normal text-muted-foreground hover:bg-muted/50 hover:text-foreground"
367
- data-release-channel={channel}
310
+ role="switch"
311
+ aria-checked={channel === "nightly"}
312
+ aria-label={t(
313
+ channel === "nightly"
314
+ ? "downloadRoute.switchToStable"
315
+ : "downloadRoute.switchToNightly",
316
+ )}
317
+ onClick={() =>
318
+ handleChannelChange(
319
+ channel === "nightly" ? "production" : "nightly",
320
+ )
321
+ }
322
+ className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
323
+ channel === "nightly"
324
+ ? "bg-foreground"
325
+ : "bg-muted-foreground/20"
326
+ }`}
368
327
  >
369
328
  <span
370
- className={
329
+ aria-hidden="true"
330
+ className={`block size-3.5 rounded-full bg-primary-foreground shadow-sm transition-transform ${
371
331
  channel === "nightly"
372
- ? "font-medium text-foreground"
373
- : undefined
374
- }
375
- >
376
- {channel === "nightly"
377
- ? t("downloadRoute.nightly")
378
- : t("downloadRoute.stable")}
379
- </span>
380
- <IconChevronDown className="size-3.5" aria-hidden="true" />
381
- </Button>
382
- </DropdownMenuTrigger>
383
- <DropdownMenuContent align="center" className="w-44">
384
- <DropdownMenuRadioGroup
385
- value={channel}
386
- onValueChange={(value) => {
387
- if (value === "production" || value === "nightly") {
388
- handleChannelChange(value);
389
- }
390
- }}
332
+ ? "translate-x-[18px]"
333
+ : "translate-x-[2px]"
334
+ }`}
335
+ />
336
+ </button>
337
+ <span
338
+ className={
339
+ channel === "nightly"
340
+ ? "font-medium text-foreground"
341
+ : "text-muted-foreground"
342
+ }
391
343
  >
392
- <DropdownMenuRadioItem value="production">
393
- {t("downloadRoute.stable")}
394
- </DropdownMenuRadioItem>
395
- <DropdownMenuRadioItem value="nightly">
396
- {t("downloadRoute.nightly")}
397
- </DropdownMenuRadioItem>
398
- </DropdownMenuRadioGroup>
399
- </DropdownMenuContent>
400
- </DropdownMenu>
344
+ {t("downloadRoute.nightly")}
345
+ </span>
346
+ </div>
347
+ </div>
401
348
  </div>
402
349
  </div>
403
350
 
@@ -67,6 +67,7 @@ import {
67
67
  startBubbleWebrtc,
68
68
  type BubbleWebrtcHandle,
69
69
  } from "./lib/bubble-webrtc";
70
+ import { openBoundOAuthWindow } from "./lib/desktop-oauth-window";
70
71
  import {
71
72
  getCameraStreamWithFallback,
72
73
  isMediaConstraintFailure,
@@ -2126,15 +2127,17 @@ export function App() {
2126
2127
  void tick();
2127
2128
  }
2128
2129
 
2129
- // Google verification stays in the bound Tauri WebView so the callback sees
2130
- // the same browser-binding cookie. Magic-link verification still completes
2131
- // in the system browser through its own exchange flow.
2130
+ // Google verification stays in a child of the bound Tauri WebView so the
2131
+ // callback sees the same browser-binding cookie. Magic-link verification
2132
+ // still completes in the system browser through its own exchange flow.
2132
2133
  async function signInExternal() {
2133
2134
  if (signInInflightRef.current) return;
2134
2135
  signInInflightRef.current = true;
2136
+ let oauthWindow: ReturnType<typeof openBoundOAuthWindow> | null = null;
2135
2137
 
2136
2138
  try {
2137
2139
  setSignInError(null);
2140
+ oauthWindow = openBoundOAuthWindow();
2138
2141
  const flowId = crypto.randomUUID?.() ?? null;
2139
2142
  const verifier = (() => {
2140
2143
  const randomUuid = crypto.randomUUID;
@@ -2163,7 +2166,6 @@ export function App() {
2163
2166
  const authParams = new URLSearchParams({
2164
2167
  desktop: "1",
2165
2168
  flow_id: flowId,
2166
- webview: "1",
2167
2169
  });
2168
2170
  const authResponse = await fetch(
2169
2171
  `${base}/_agent-native/google/auth-url?${authParams.toString()}`,
@@ -2194,13 +2196,12 @@ export function App() {
2194
2196
  : "Could not start Google sign-in.";
2195
2197
  throw new Error(message);
2196
2198
  }
2199
+ oauthWindow.location.href = authPayload.url;
2197
2200
  setSignInPending("google");
2198
- // Stay in this exact WebView: the auth bootstrap set the HttpOnly
2199
- // browser-binding cookie here, and the callback returns this page with
2200
- // the staged session cookie after Google completes.
2201
- window.location.href = authPayload.url;
2201
+ startDesktopAuthExchange(flowId, "google", verifier);
2202
2202
  } catch (err) {
2203
2203
  console.error("[clips-tray] signInExternal failed:", err);
2204
+ oauthWindow?.close();
2204
2205
  signInInflightRef.current = false;
2205
2206
  setSignInPending(null);
2206
2207
  setSignInError(
@@ -3962,8 +3963,8 @@ export function App() {
3962
3963
  // (not a separate Tauri window). This avoids Tauri 2's separate-WebKit-
3963
3964
  // data-store-per-WebviewWindow cookie-jar issue — the cookie is set in
3964
3965
  // the same webview that reads it on the next /auth/session poll.
3965
- // Google verification uses the bound WebView, while magic-link verification
3966
- // uses the system browser and password stays inline here.
3966
+ // Google verification uses a popup in the bound WebView, while magic-link
3967
+ // verification uses the system browser and password stays inline here.
3967
3968
  if (authStatus === "anon") {
3968
3969
  return (
3969
3970
  <div className="app" ref={appRef}>
@@ -0,0 +1,23 @@
1
+ type OAuthWindow = Pick<Window, "close"> & {
2
+ location: Pick<Location, "href">;
3
+ };
4
+
5
+ type OpenOAuthWindow = (url: string, target: string) => OAuthWindow | null;
6
+
7
+ /**
8
+ * Open a same-WebView OAuth child before awaiting the auth URL.
9
+ *
10
+ * The parent keeps polling the exchange while the child completes Google
11
+ * OAuth. Using window.open keeps both pages in the Tauri WebView cookie
12
+ * partition; the system-browser shell opener would lose the binding cookie.
13
+ */
14
+ export function openBoundOAuthWindow(
15
+ openWindow: OpenOAuthWindow = (url, target) =>
16
+ window.open(url, target) as OAuthWindow | null,
17
+ ): OAuthWindow {
18
+ const oauthWindow = openWindow("about:blank", "_blank");
19
+ if (!oauthWindow) {
20
+ throw new Error("Could not open the Google sign-in window.");
21
+ }
22
+ return oauthWindow;
23
+ }
@@ -134,3 +134,14 @@ hand-duplicating framework logic.
134
134
  `agent-surfaces`.
135
135
  - Do not copy framework runtime internals when a public API or narrow UI copy
136
136
  will do; read `customizing-agent-native` for the supported override ladder.
137
+
138
+ ## Authoring visual docs blocks
139
+
140
+ Treat the shared `Diagram` block as a renderer, not as a reason to style every
141
+ docs UI as a sketch. Use the hand-drawn font and Rough.js only for actual
142
+ diagrams or wireframes that communicate structure, flow, or a design draft.
143
+ For polished UI-like content - cards, logo walls, tables, controls, or product
144
+ shells — set `renderMode="design"` (and usually `frame="hide"` when the block
145
+ is the content itself). Design mode preserves normal docs typography and
146
+ disables the Rough.js overlay. Keep `data-rough` only on elements intentionally
147
+ sketched inside a real diagram.