@agent-native/core 0.68.1 → 0.68.3

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 (129) hide show
  1. package/README.md +4 -4
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +16 -0
  4. package/corpus/core/docs/content/authentication.md +16 -2
  5. package/corpus/core/docs/content/template-analytics.md +14 -9
  6. package/corpus/core/docs/content/template-clips.md +35 -2
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  9. package/corpus/core/src/client/AgentPanel.tsx +2 -4
  10. package/corpus/core/src/client/CommandMenu.tsx +98 -3
  11. package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
  12. package/corpus/core/src/client/index.ts +2 -0
  13. package/corpus/core/src/extensions/content-patch.ts +12 -2
  14. package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
  15. package/corpus/core/src/provider-api/index.ts +37 -17
  16. package/corpus/core/src/server/auth.ts +6 -8
  17. package/corpus/core/src/server/better-auth-instance.ts +19 -4
  18. package/corpus/core/src/server/core-routes-plugin.ts +157 -5
  19. package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
  20. package/corpus/core/src/server/index.ts +8 -0
  21. package/corpus/core/src/server/onboarding-html.ts +2 -1
  22. package/corpus/core/src/server/social-og-image.ts +6 -1
  23. package/corpus/core/src/vite/client.ts +83 -1
  24. package/corpus/templates/analytics/app/routes/$.tsx +9 -0
  25. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
  27. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
  28. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
  29. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
  30. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
  31. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
  32. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
  33. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
  34. package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
  35. package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
  36. package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
  37. package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
  39. package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
  40. package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
  41. package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
  42. package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
  43. package/corpus/templates/calendar/shared/api.ts +4 -0
  44. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
  45. package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
  46. package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
  47. package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
  48. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
  49. package/corpus/templates/clips/app/lib/capture-install-options.ts +14 -0
  50. package/corpus/templates/clips/app/root.tsx +54 -2
  51. package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
  52. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
  53. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
  54. package/corpus/templates/clips/app/routes/download.tsx +49 -0
  55. package/corpus/templates/clips/app/routes/record.tsx +4 -4
  56. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
  57. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +29 -0
  58. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
  59. package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
  60. package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
  61. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
  62. package/corpus/templates/content/vite.config.ts +37 -1
  63. package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
  64. package/corpus/templates/design/app/routes/$.tsx +9 -0
  65. package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
  66. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  67. package/corpus/templates/plan/app/global.css +2 -2
  68. package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
  69. package/corpus/templates/slides/app/routes/$.tsx +9 -0
  70. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
  71. package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
  72. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  73. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  74. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  75. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  76. package/dist/client/AgentPanel.d.ts +1 -1
  77. package/dist/client/AgentPanel.d.ts.map +1 -1
  78. package/dist/client/AgentPanel.js +1 -1
  79. package/dist/client/AgentPanel.js.map +1 -1
  80. package/dist/client/CommandMenu.d.ts +15 -2
  81. package/dist/client/CommandMenu.d.ts.map +1 -1
  82. package/dist/client/CommandMenu.js +46 -3
  83. package/dist/client/CommandMenu.js.map +1 -1
  84. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  85. package/dist/client/ConnectBuilderCard.js +11 -2
  86. package/dist/client/ConnectBuilderCard.js.map +1 -1
  87. package/dist/client/index.d.ts +1 -1
  88. package/dist/client/index.d.ts.map +1 -1
  89. package/dist/client/index.js.map +1 -1
  90. package/dist/extensions/content-patch.d.ts.map +1 -1
  91. package/dist/extensions/content-patch.js +10 -2
  92. package/dist/extensions/content-patch.js.map +1 -1
  93. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
  94. package/dist/oauth-tokens/google-refresh.js +29 -17
  95. package/dist/oauth-tokens/google-refresh.js.map +1 -1
  96. package/dist/provider-api/index.d.ts.map +1 -1
  97. package/dist/provider-api/index.js +28 -16
  98. package/dist/provider-api/index.js.map +1 -1
  99. package/dist/server/auth.d.ts.map +1 -1
  100. package/dist/server/auth.js +6 -6
  101. package/dist/server/auth.js.map +1 -1
  102. package/dist/server/better-auth-instance.d.ts.map +1 -1
  103. package/dist/server/better-auth-instance.js +18 -4
  104. package/dist/server/better-auth-instance.js.map +1 -1
  105. package/dist/server/core-routes-plugin.d.ts +5 -0
  106. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  107. package/dist/server/core-routes-plugin.js +107 -5
  108. package/dist/server/core-routes-plugin.js.map +1 -1
  109. package/dist/server/google-oauth-credentials.d.ts +15 -0
  110. package/dist/server/google-oauth-credentials.d.ts.map +1 -0
  111. package/dist/server/google-oauth-credentials.js +34 -0
  112. package/dist/server/google-oauth-credentials.js.map +1 -0
  113. package/dist/server/index.d.ts +1 -0
  114. package/dist/server/index.d.ts.map +1 -1
  115. package/dist/server/index.js +1 -0
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/onboarding-html.d.ts.map +1 -1
  118. package/dist/server/onboarding-html.js +2 -1
  119. package/dist/server/onboarding-html.js.map +1 -1
  120. package/dist/server/social-og-image.d.ts.map +1 -1
  121. package/dist/server/social-og-image.js +5 -1
  122. package/dist/server/social-og-image.js.map +1 -1
  123. package/dist/vite/client.d.ts.map +1 -1
  124. package/dist/vite/client.js +83 -1
  125. package/dist/vite/client.js.map +1 -1
  126. package/docs/content/authentication.md +16 -2
  127. package/docs/content/template-analytics.md +14 -9
  128. package/docs/content/template-clips.md +35 -2
  129. package/package.json +1 -1
@@ -1,28 +1,12 @@
1
1
  import { createAuthPlugin } from "@agent-native/core/server";
2
2
 
3
- // Calendar requires a Google connection to read/write events, so the
4
- // onboarding page only offers "Sign in with Google" — no email/password
5
- // account creation, since that path can't be used to access the calendar.
6
- //
7
- // Calendar/Contacts/Directory scopes are requested up front during the
8
- // primary "Sign in with Google" flow. Tokens land in the framework's
9
- // `oauth_tokens` table automatically (via a Better Auth account hook)
10
- // so the existing `templates/calendar/server/lib/google-calendar.ts`
11
- // client works on first sign-in — no separate "Connect Google" page
12
- // needed. The template-specific routes under `/_agent-native/google/*`
13
- // remain available for "add another account" flows.
3
+ // Calendar keeps Google as the primary auth surface, but the first sign-in is
4
+ // identity-only. The template-owned `/_agent-native/google/*` routes request
5
+ // Calendar/Contacts/Directory scopes only after there is a signed-in owner, so
6
+ // basic login stays isolated from product API verification/blocking issues.
14
7
  export default createAuthPlugin({
15
8
  googleOnly: true,
16
9
  mountGoogleOAuthRoutes: false,
17
- googleScopes: [
18
- "https://www.googleapis.com/auth/calendar.readonly",
19
- "https://www.googleapis.com/auth/calendar.events",
20
- "https://www.googleapis.com/auth/userinfo.email",
21
- "https://www.googleapis.com/auth/userinfo.profile",
22
- "https://www.googleapis.com/auth/directory.readonly",
23
- "https://www.googleapis.com/auth/contacts.readonly",
24
- "https://www.googleapis.com/auth/contacts.other.readonly",
25
- ],
26
10
  marketing: {
27
11
  appName: "Agent-Native Calendar",
28
12
  tagline:
@@ -17,6 +17,10 @@ export interface CalendarEvent {
17
17
  accountEmail?: string;
18
18
  /** Set when this event belongs to an overlaid person's calendar */
19
19
  overlayEmail?: string;
20
+ /** Client-only marker for overlaid calendar ownership */
21
+ ownerColor?: string;
22
+ /** Client-only display name for the overlaid calendar owner */
23
+ ownerName?: string;
20
24
  color?: string;
21
25
  /** Google Calendar event color id (1-11). */
22
26
  colorId?: string;
@@ -48,6 +48,23 @@ Loom iframe and the native Clips editor is hidden for those recordings. If the
48
48
  user needs Clips-native trimming, exports, frame extraction, or upload-based
49
49
  transcription, ask them to upload the original video file instead.
50
50
 
51
+ ## Browser diagnostics and recorder install options
52
+
53
+ Browser recordings can save bounded, redacted diagnostics through
54
+ `createBrowserDiagnosticsCapture`: console messages plus fetch/XHR method, URL,
55
+ status, duration, and errors. The browser recorder only captures activity from
56
+ the recorder page itself. The Clips Chrome extension is the active-tab path for
57
+ browser logs: it launches `/record` with an extension capture session and passes
58
+ `developerLogs=1/0`, then saves diagnostics with source `extension`.
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.
67
+
51
68
  ## Pause / resume
52
69
 
53
70
  `MediaRecorder.pause()` / `.resume()` are supported in all evergreen browsers. Keep a single `MediaRecorder` instance across pauses — don't tear down the stream, or the permission prompt will fire again. While paused, the upload worker keeps draining its buffer so we catch up before the user stops.
@@ -21,7 +21,8 @@ import { accessFilter } from "@agent-native/core/sharing";
21
21
  import { readAppSecret, writeAppSecret } from "@agent-native/core/secrets";
22
22
  import {
23
23
  listEvents,
24
- refreshAccessToken,
24
+ resolveGoogleOAuthCredentialCandidates,
25
+ refreshAccessTokenWithFallback,
25
26
  pickJoinUrl,
26
27
  type CalendarEvent,
27
28
  } from "../server/lib/google-calendar-client.js";
@@ -55,9 +56,8 @@ async function resolveAccessToken(args: {
55
56
  accessRef: string | null;
56
57
  refreshRef: string | null;
57
58
  }): Promise<string | null> {
58
- const clientId = process.env.GOOGLE_CLIENT_ID;
59
- const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
60
- if (!clientId || !clientSecret) return null;
59
+ const credentialCandidates = resolveGoogleOAuthCredentialCandidates();
60
+ if (!credentialCandidates.length) return null;
61
61
 
62
62
  let bundle: AccessTokenBundle | null = null;
63
63
  if (args.accessRef) {
@@ -93,10 +93,9 @@ async function resolveAccessToken(args: {
93
93
 
94
94
  let refreshed;
95
95
  try {
96
- refreshed = await refreshAccessToken({
96
+ refreshed = await refreshAccessTokenWithFallback({
97
97
  refreshToken: refreshSecret.value,
98
- clientId,
99
- clientSecret,
98
+ credentials: credentialCandidates,
100
99
  });
101
100
  } catch {
102
101
  return null;
@@ -0,0 +1,157 @@
1
+ import { type ReactNode } from "react";
2
+ import { Link } from "react-router";
3
+ import {
4
+ IconBrowser,
5
+ IconChevronDown,
6
+ IconDeviceDesktop,
7
+ IconExternalLink,
8
+ } from "@tabler/icons-react";
9
+ import { Button, type ButtonProps } from "@/components/ui/button";
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "@/components/ui/popover";
15
+ import { cn } from "@/lib/utils";
16
+ import {
17
+ clipsChromeExtensionEnabled,
18
+ clipsChromeExtensionUrl,
19
+ } from "@/lib/capture-install-options";
20
+
21
+ type PopoverPlacement = {
22
+ align?: "start" | "center" | "end";
23
+ side?: "top" | "right" | "bottom" | "left";
24
+ };
25
+
26
+ type CaptureInstallButtonProps = Omit<ButtonProps, "asChild"> &
27
+ PopoverPlacement & {
28
+ children: ReactNode;
29
+ desktopHref?: string;
30
+ };
31
+
32
+ type CaptureInstallInlineLinkProps = PopoverPlacement & {
33
+ children: ReactNode;
34
+ className?: string;
35
+ desktopHref?: string;
36
+ };
37
+
38
+ function InstallOptionsContent({ desktopHref = "/download" }) {
39
+ const chromeAvailable = Boolean(clipsChromeExtensionUrl);
40
+
41
+ return (
42
+ <div className="grid gap-2">
43
+ <div className="px-1 pb-1">
44
+ <div className="text-sm font-medium text-popover-foreground">
45
+ Choose your recorder
46
+ </div>
47
+ <p className="mt-1 text-xs leading-relaxed text-muted-foreground">
48
+ Use Chrome when you need browser logs, or desktop for the smoothest
49
+ everyday capture.
50
+ </p>
51
+ </div>
52
+
53
+ {chromeAvailable ? (
54
+ <a
55
+ href={clipsChromeExtensionUrl ?? undefined}
56
+ target="_blank"
57
+ rel="noreferrer"
58
+ className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
59
+ >
60
+ <IconBrowser className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
61
+ <span className="min-w-0 flex-1">
62
+ <span className="block text-sm font-medium">Chrome extension</span>
63
+ <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
64
+ Best when you want redacted console and network diagnostics from
65
+ the browser tab.
66
+ </span>
67
+ </span>
68
+ <IconExternalLink className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
69
+ </a>
70
+ ) : (
71
+ <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" />
73
+ <span className="min-w-0 flex-1">
74
+ <span className="block text-sm font-medium">Chrome extension</span>
75
+ <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
76
+ Browser logs option is ready, pending the Chrome Web Store URL.
77
+ </span>
78
+ </span>
79
+ </div>
80
+ )}
81
+
82
+ <Link
83
+ to={desktopHref}
84
+ className="flex items-start gap-3 rounded-md border border-border p-3 text-left transition hover:bg-accent"
85
+ >
86
+ <IconDeviceDesktop className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
87
+ <span className="min-w-0 flex-1">
88
+ <span className="block text-sm font-medium">Desktop app</span>
89
+ <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
90
+ Most seamless for global shortcuts, menu-bar recording, meetings,
91
+ and repeat captures.
92
+ </span>
93
+ </span>
94
+ </Link>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ export function CaptureInstallButton({
100
+ children,
101
+ className,
102
+ desktopHref = "/download",
103
+ align = "end",
104
+ side = "bottom",
105
+ ...buttonProps
106
+ }: CaptureInstallButtonProps) {
107
+ if (!clipsChromeExtensionEnabled) {
108
+ return (
109
+ <Button asChild className={className} {...buttonProps}>
110
+ <Link to={desktopHref}>{children}</Link>
111
+ </Button>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <Popover>
117
+ <PopoverTrigger asChild>
118
+ <Button className={className} {...buttonProps}>
119
+ {children}
120
+ <IconChevronDown className="h-3.5 w-3.5" />
121
+ </Button>
122
+ </PopoverTrigger>
123
+ <PopoverContent align={align} side={side} className="w-80 p-3">
124
+ <InstallOptionsContent desktopHref={desktopHref} />
125
+ </PopoverContent>
126
+ </Popover>
127
+ );
128
+ }
129
+
130
+ export function CaptureInstallInlineLink({
131
+ children,
132
+ className,
133
+ desktopHref = "/download",
134
+ align = "start",
135
+ side = "bottom",
136
+ }: CaptureInstallInlineLinkProps) {
137
+ if (!clipsChromeExtensionEnabled) {
138
+ return (
139
+ <Link to={desktopHref} className={className}>
140
+ {children}
141
+ </Link>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <Popover>
147
+ <PopoverTrigger asChild>
148
+ <button type="button" className={cn("cursor-pointer", className)}>
149
+ {children}
150
+ </button>
151
+ </PopoverTrigger>
152
+ <PopoverContent align={align} side={side} className="w-80 p-3">
153
+ <InstallOptionsContent desktopHref={desktopHref} />
154
+ </PopoverContent>
155
+ </Popover>
156
+ );
157
+ }
@@ -53,6 +53,10 @@ import { FolderTree, type FolderNode } from "./folder-tree";
53
53
  import { SearchBar } from "./search-bar";
54
54
  import { PageHeaderSlotProvider } from "./page-header";
55
55
  import { CreateSpaceDialog } from "./create-space-dialog";
56
+ import {
57
+ CaptureInstallButton,
58
+ CaptureInstallInlineLink,
59
+ } from "@/components/capture-install-options";
56
60
  import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
57
61
  import { toast } from "sonner";
58
62
  import {
@@ -477,13 +481,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
477
481
  <>
478
482
  <div className="shrink-0 space-y-1.5 border-t border-border px-2 py-1.5">
479
483
  {shouldShowSidebarLink && (
480
- <NavLink
481
- to="/download"
482
- className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60"
483
- >
484
+ <CaptureInstallInlineLink className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60">
484
485
  <IconAppWindow className="h-4 w-4" />
485
486
  Get desktop app
486
- </NavLink>
487
+ </CaptureInstallInlineLink>
487
488
  )}
488
489
  <SearchBar />
489
490
  </div>
@@ -532,9 +533,9 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
532
533
  Record from the menu bar, global shortcut, auto-updates.
533
534
  </span>
534
535
  </div>
535
- <Button asChild size="sm" className="shrink-0">
536
- <NavLink to="/download">Download</NavLink>
537
- </Button>
536
+ <CaptureInstallButton size="sm" className="shrink-0">
537
+ Download
538
+ </CaptureInstallButton>
538
539
  <Tooltip>
539
540
  <TooltipTrigger asChild>
540
541
  <Button
@@ -18,9 +18,10 @@ import {
18
18
  IconUpload,
19
19
  IconVideo,
20
20
  } from "@tabler/icons-react";
21
- import { agentNativePath, appPath } from "@agent-native/core/client";
21
+ import { agentNativePath } from "@agent-native/core/client";
22
22
  import { Button } from "@/components/ui/button";
23
23
  import { Input } from "@/components/ui/input";
24
+ import { CaptureInstallInlineLink } from "@/components/capture-install-options";
24
25
  import {
25
26
  Popover,
26
27
  PopoverContent,
@@ -724,12 +725,9 @@ export function PreRecordPanel({
724
725
  {micAccessError ? (
725
726
  <p className="text-[11px] leading-snug text-muted-foreground">
726
727
  {micAccessError}{" "}
727
- <a
728
- href={appPath("/download")}
729
- className="text-foreground underline-offset-4 hover:underline"
730
- >
728
+ <CaptureInstallInlineLink className="text-foreground underline-offset-4 hover:underline">
731
729
  Try Clips Desktop.
732
- </a>
730
+ </CaptureInstallInlineLink>
733
731
  </p>
734
732
  ) : null}
735
733
  </div>
@@ -0,0 +1,14 @@
1
+ function isTruthy(value: unknown): boolean {
2
+ if (typeof value !== "string") return false;
3
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
4
+ }
5
+
6
+ const chromeExtensionUrl =
7
+ import.meta.env.VITE_CLIPS_CHROME_EXTENSION_URL?.trim() ??
8
+ "https://chromewebstore.google.com/detail/baoipacpchggcdigagnajakiidcgcffn";
9
+
10
+ export const clipsChromeExtensionEnabled = isTruthy(
11
+ import.meta.env.VITE_CLIPS_CHROME_EXTENSION_ENABLED,
12
+ );
13
+
14
+ export const clipsChromeExtensionUrl = chromeExtensionUrl || null;
@@ -19,9 +19,18 @@ import {
19
19
  getThemeInitScript,
20
20
  useCommandMenuShortcut,
21
21
  } from "@agent-native/core/client";
22
- import { IconSun, IconMoon } from "@tabler/icons-react";
22
+ import { IconCheck, IconSun, IconMoon } from "@tabler/icons-react";
23
23
  import { useTheme } from "next-themes";
24
24
  import { Toaster } from "@/components/ui/sonner";
25
+ import { Button } from "@/components/ui/button";
26
+ import {
27
+ Dialog,
28
+ DialogContent,
29
+ DialogDescription,
30
+ DialogFooter,
31
+ DialogHeader,
32
+ DialogTitle,
33
+ } from "@/components/ui/dialog";
25
34
  import type { LinksFunction } from "react-router";
26
35
  import stylesheet from "./global.css?url";
27
36
  import { configureTracking } from "@agent-native/core/client";
@@ -117,8 +126,30 @@ type ExternalChromeRuntime = {
117
126
  ) => void;
118
127
  };
119
128
 
129
+ const CLIPS_COMMAND_DOCS = [
130
+ {
131
+ title: "Use the Chrome extension for browser logs",
132
+ description:
133
+ "Record a browser tab with redacted console logs, JavaScript exceptions, and fetch/XHR diagnostics.",
134
+ href: "https://www.agent-native.com/docs/template-clips#browser-logs-and-developer-diagnostics",
135
+ keywords: [
136
+ "logs",
137
+ "browser logs",
138
+ "developer logs",
139
+ "console logs",
140
+ "network logs",
141
+ "fetch",
142
+ "xhr",
143
+ "diagnostics",
144
+ "chrome extension",
145
+ "recording",
146
+ ],
147
+ },
148
+ ] satisfies React.ComponentProps<typeof CommandMenu.DocsGroup>["docs"];
149
+
120
150
  function ClipsExtensionAuthBridge() {
121
151
  const location = useLocation();
152
+ const [showAuthSuccess, setShowAuthSuccess] = useState(false);
122
153
 
123
154
  useEffect(() => {
124
155
  const params = new URLSearchParams(location.search);
@@ -163,6 +194,7 @@ function ClipsExtensionAuthBridge() {
163
194
  cleaned.searchParams.delete("clipsExtensionAuth");
164
195
  cleaned.searchParams.delete("clipsExtensionId");
165
196
  window.history.replaceState(window.history.state, "", cleaned);
197
+ setShowAuthSuccess(true);
166
198
  },
167
199
  );
168
200
  }
@@ -173,7 +205,26 @@ function ClipsExtensionAuthBridge() {
173
205
  };
174
206
  }, [location.search]);
175
207
 
176
- return null;
208
+ return (
209
+ <Dialog open={showAuthSuccess} onOpenChange={setShowAuthSuccess}>
210
+ <DialogContent className="max-w-sm text-center sm:text-center">
211
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500">
212
+ <IconCheck className="h-9 w-9" strokeWidth={2.5} />
213
+ </div>
214
+ <DialogHeader className="items-center text-center sm:text-center">
215
+ <DialogTitle>Signed in</DialogTitle>
216
+ <DialogDescription className="max-w-xs">
217
+ Open the Clips extension again to start recording.
218
+ </DialogDescription>
219
+ </DialogHeader>
220
+ <DialogFooter className="sm:justify-center">
221
+ <Button type="button" onClick={() => setShowAuthSuccess(false)}>
222
+ Got it
223
+ </Button>
224
+ </DialogFooter>
225
+ </DialogContent>
226
+ </Dialog>
227
+ );
177
228
  }
178
229
 
179
230
  /**
@@ -211,6 +262,7 @@ function AppContent() {
211
262
  <CommandMenu.Group heading="Actions">
212
263
  <CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
213
264
  </CommandMenu.Group>
265
+ <CommandMenu.DocsGroup docs={CLIPS_COMMAND_DOCS} />
214
266
  <CommandMenu.Group heading="Appearance">
215
267
  <ThemeToggleItem />
216
268
  </CommandMenu.Group>
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Link } from "react-router";
3
2
  import { toast } from "sonner";
4
3
  import {
5
4
  IconArrowsExchange,
@@ -34,6 +33,7 @@ import {
34
33
  TooltipTrigger,
35
34
  } from "@/components/ui/tooltip";
36
35
  import { useDesktopPromo } from "@/hooks/use-desktop-promo";
36
+ import { CaptureInstallButton } from "@/components/capture-install-options";
37
37
 
38
38
  export function meta() {
39
39
  return [{ title: "Dictate · Clips" }];
@@ -609,12 +609,10 @@ function EmptyState({ isDesktopApp }: { isDesktopApp: boolean }) {
609
609
  work in any app — Slack, your editor, anywhere.
610
610
  </p>
611
611
  <div className="mt-5 flex items-center justify-center">
612
- <Button asChild size="sm" className="gap-1.5">
613
- <Link to="/download">
614
- <IconDownload className="h-3.5 w-3.5" />
615
- Download Clips desktop app
616
- </Link>
617
- </Button>
612
+ <CaptureInstallButton size="sm" className="gap-1.5">
613
+ <IconDownload className="h-3.5 w-3.5" />
614
+ Download Clips desktop app
615
+ </CaptureInstallButton>
618
616
  </div>
619
617
  <div className="mt-3 flex items-center justify-center gap-2 text-xs text-muted-foreground">
620
618
  <Kbd>Fn</Kbd>
@@ -646,12 +644,10 @@ function DownloadDesktopAppCard() {
646
644
  desktop app is the way to use this.
647
645
  </p>
648
646
  </div>
649
- <Button asChild size="sm" className="gap-1.5">
650
- <Link to="/download">
651
- <IconDownload className="h-3.5 w-3.5" />
652
- Download
653
- </Link>
654
- </Button>
647
+ <CaptureInstallButton size="sm" className="gap-1.5">
648
+ <IconDownload className="h-3.5 w-3.5" />
649
+ Download
650
+ </CaptureInstallButton>
655
651
  </div>
656
652
  );
657
653
  }
@@ -61,6 +61,7 @@ import {
61
61
  TooltipContent,
62
62
  TooltipTrigger,
63
63
  } from "@/components/ui/tooltip";
64
+ import { CaptureInstallButton } from "@/components/capture-install-options";
64
65
 
65
66
  export function meta() {
66
67
  return [{ title: "Meeting · Clips" }];
@@ -699,17 +700,14 @@ export default function MeetingDetailRoute() {
699
700
  transcript and AI notes will appear here automatically.
700
701
  </span>
701
702
  {!isDesktopApp && (
702
- <Button
703
- asChild
703
+ <CaptureInstallButton
704
704
  size="sm"
705
705
  variant="secondary"
706
706
  className="ml-auto h-8 gap-1.5 cursor-pointer"
707
707
  >
708
- <NavLink to="/download">
709
- <IconExternalLink className="h-3.5 w-3.5" />
710
- Get desktop app
711
- </NavLink>
712
- </Button>
708
+ <IconExternalLink className="h-3.5 w-3.5" />
709
+ Get desktop app
710
+ </CaptureInstallButton>
713
711
  )}
714
712
  </div>
715
713
  )}
@@ -45,6 +45,7 @@ import { Input } from "@/components/ui/input";
45
45
  import { DayHeader, formatDayLabel } from "@/components/meetings/day-header";
46
46
  import type { AttendeeStackParticipant } from "@/components/meetings/attendee-stack";
47
47
  import { PageHeader } from "@/components/library/page-header";
48
+ import { CaptureInstallButton } from "@/components/capture-install-options";
48
49
 
49
50
  export function meta() {
50
51
  return [{ title: "Meetings · Clips" }];
@@ -668,17 +669,14 @@ function MeetingsHeader({
668
669
  </div>
669
670
  {showDesktopCta && (
670
671
  <div className="flex w-fit shrink-0 flex-col items-start gap-1 sm:items-end">
671
- <Button
672
- asChild
672
+ <CaptureInstallButton
673
673
  size="sm"
674
674
  variant="secondary"
675
675
  className="h-8 w-fit gap-1.5 cursor-pointer"
676
676
  >
677
- <NavLink to="/download">
678
- <IconAppWindow className="h-4 w-4" />
679
- Get desktop app
680
- </NavLink>
681
- </Button>
677
+ <IconAppWindow className="h-4 w-4" />
678
+ Get desktop app
679
+ </CaptureInstallButton>
682
680
  <p className="max-w-56 text-[11px] leading-snug text-muted-foreground">
683
681
  Required for meeting reminders and transcription.
684
682
  </p>
@@ -1,12 +1,18 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import {
3
+ IconBrowser,
3
4
  IconBrandApple,
4
5
  IconBrandWindows,
6
+ IconExternalLink,
5
7
  IconPlayerRecord,
6
8
  } from "@tabler/icons-react";
7
9
  import { appBasePath, appPath } from "@agent-native/core/client";
8
10
  import { Button } from "@/components/ui/button";
9
11
  import { Skeleton } from "@/components/ui/skeleton";
12
+ import {
13
+ clipsChromeExtensionEnabled,
14
+ clipsChromeExtensionUrl,
15
+ } from "@/lib/capture-install-options";
10
16
 
11
17
  export function meta() {
12
18
  return [
@@ -237,6 +243,49 @@ export default function DownloadPage() {
237
243
  )}
238
244
  </div>
239
245
  </div>
246
+
247
+ {clipsChromeExtensionEnabled && (
248
+ <section className="mt-10 w-full max-w-xl rounded-2xl border border-border bg-card p-4 text-left shadow-sm">
249
+ <div className="flex items-start gap-3">
250
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
251
+ <IconBrowser className="h-4 w-4" />
252
+ </div>
253
+ <div className="min-w-0 flex-1">
254
+ <h2 className="text-sm font-semibold text-foreground">
255
+ Chrome extension for browser logs
256
+ </h2>
257
+ <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
258
+ Use the extension when you want a recording plus redacted
259
+ console and fetch/XHR diagnostics from the tab you launch
260
+ from. The desktop app is still the smoothest everyday
261
+ recorder.
262
+ </p>
263
+ </div>
264
+ </div>
265
+ <Button
266
+ asChild={Boolean(clipsChromeExtensionUrl)}
267
+ disabled={!clipsChromeExtensionUrl}
268
+ variant="outline"
269
+ className="mt-4 w-full gap-2"
270
+ >
271
+ {clipsChromeExtensionUrl ? (
272
+ <a
273
+ href={clipsChromeExtensionUrl}
274
+ target="_blank"
275
+ rel="noreferrer"
276
+ >
277
+ <IconExternalLink className="h-4 w-4" />
278
+ Install Chrome extension
279
+ </a>
280
+ ) : (
281
+ <>
282
+ <IconExternalLink className="h-4 w-4" />
283
+ Chrome Web Store URL pending
284
+ </>
285
+ )}
286
+ </Button>
287
+ </section>
288
+ )}
240
289
  </div>
241
290
  </main>
242
291
  </div>
@@ -71,6 +71,7 @@ async function writeAppState(key: string, value: unknown): Promise<void> {
71
71
  }
72
72
  import { Button } from "@/components/ui/button";
73
73
  import { Spinner } from "@/components/ui/spinner";
74
+ import { CaptureInstallButton } from "@/components/capture-install-options";
74
75
  import { toast } from "sonner";
75
76
 
76
77
  import { PreRecordPanel } from "@/components/recorder/pre-record-panel";
@@ -542,13 +543,12 @@ function DesktopRecorderCallout() {
542
543
  </p>
543
544
  </div>
544
545
  </div>
545
- <Button
546
- asChild
546
+ <CaptureInstallButton
547
547
  size="sm"
548
548
  className="mt-4 w-full bg-primary text-primary-foreground hover:bg-primary/90"
549
549
  >
550
- <Link to="/download">Download desktop app</Link>
551
- </Button>
550
+ Download desktop app
551
+ </CaptureInstallButton>
552
552
  </aside>
553
553
  );
554
554
  }