@agent-native/core 0.151.2 → 0.152.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 (250) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +1 -1
  3. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +24 -0
  4. package/corpus/templates/analytics/actions/bigquery-query.ts +21 -0
  5. package/corpus/templates/analytics/app/components/ConnectionTestStatus.tsx +65 -0
  6. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -3
  7. package/corpus/templates/analytics/app/pages/DataSources.tsx +15 -37
  8. package/corpus/templates/analytics/app/root.tsx +0 -1
  9. package/corpus/templates/analytics/changelog/2026-08-11-analytics-custom-blocks-keep-loading-with-legacy-bigquery-action.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-11-analytics-routes-company-knowledge-and-slack-context-questio.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  12. package/corpus/templates/analytics/public/manifest.json +0 -1
  13. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +5 -0
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
  15. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +1 -1
  16. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +24 -0
  17. package/corpus/templates/assets/app/root.tsx +0 -1
  18. package/corpus/templates/assets/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  19. package/corpus/templates/assets/public/manifest.json +0 -1
  20. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +1 -1
  21. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +24 -0
  22. package/corpus/templates/brain/app/root.tsx +0 -1
  23. package/corpus/templates/brain/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  24. package/corpus/templates/brain/public/manifest.json +0 -1
  25. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +1 -1
  26. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +24 -0
  27. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +0 -53
  28. package/corpus/templates/calendar/app/i18n-data.ts +0 -80
  29. package/corpus/templates/calendar/app/root.tsx +0 -1
  30. package/corpus/templates/calendar/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-11-google-calendar-connection-buttons-now-open-the-sign-in-flow.md +6 -0
  32. package/corpus/templates/calendar/public/manifest.json +0 -1
  33. package/corpus/templates/calendar/server/plugins/auth.ts +0 -10
  34. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
  35. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
  36. package/corpus/templates/chat/app/root.tsx +0 -1
  37. package/corpus/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  38. package/corpus/templates/chat/public/manifest.json +0 -1
  39. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +24 -0
  40. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +1 -1
  41. package/corpus/templates/clips/app/components/player/share-dialog.tsx +6 -0
  42. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +20 -3
  43. package/corpus/templates/clips/app/i18n/en-US.ts +4 -4
  44. package/corpus/templates/clips/app/root.tsx +0 -1
  45. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -3
  46. package/corpus/templates/clips/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-08-11-clips-desktop-settings-are-now-easier-to-scan-with-a-wider-t.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-08-11-google-calendar-connections-from-meetings-now-open-directly.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-08-11-recording-viewers-can-comment-and-react.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-08-11-system-audio-recording-now-lives-inside-the-microphone-menu-.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-08-11-timestamped-comments-stay-on-the-video-for-three-seconds-wh.md +6 -0
  52. package/corpus/templates/clips/desktop/src/app.tsx +773 -1001
  53. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +15 -27
  54. package/corpus/templates/clips/desktop/src/styles.css +364 -5
  55. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
  56. package/corpus/templates/clips/public/manifest.json +0 -1
  57. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +1 -1
  58. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +24 -0
  59. package/corpus/templates/content/AGENTS.md +3 -0
  60. package/corpus/templates/content/actions/resolve-content-landing.ts +120 -0
  61. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +9 -0
  62. package/corpus/templates/content/app/i18n-data.ts +58 -0
  63. package/corpus/templates/content/app/lib/content-landing.ts +22 -0
  64. package/corpus/templates/content/app/root.tsx +0 -1
  65. package/corpus/templates/content/app/routes/_app._index.tsx +36 -26
  66. package/corpus/templates/content/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  67. package/corpus/templates/content/changelog/2026-08-11-content-opens-your-last-page-on-return-and-gives-new-users-a.md +6 -0
  68. package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +7 -1
  69. package/corpus/templates/content/parity/matrix.md +1 -0
  70. package/corpus/templates/content/parity/matrix.ts +22 -0
  71. package/corpus/templates/content/public/manifest.json +0 -1
  72. package/corpus/templates/content/shared/content-landing.ts +17 -0
  73. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +1 -1
  74. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +24 -0
  75. package/corpus/templates/crm/app/root.tsx +0 -1
  76. package/corpus/templates/crm/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  77. package/corpus/templates/crm/public/manifest.json +0 -1
  78. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +1 -1
  79. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +24 -0
  80. package/corpus/templates/design/app/i18n/en-US.ts +5 -0
  81. package/corpus/templates/design/app/root.tsx +0 -1
  82. package/corpus/templates/design/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  83. package/corpus/templates/design/changelog/2026-08-11-design-sharing-now-identifies-viewers-who-can-add-review-com.md +6 -0
  84. package/corpus/templates/design/public/manifest.json +0 -1
  85. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +1 -1
  86. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +24 -0
  87. package/corpus/templates/dispatch/AGENTS.md +9 -0
  88. package/corpus/templates/dispatch/actions/list-dispatch-usage-metrics.ts +5 -19
  89. package/corpus/templates/dispatch/actions/view-screen.ts +10 -3
  90. package/corpus/templates/dispatch/app/root.tsx +0 -1
  91. package/corpus/templates/dispatch/app/routes/settings.tsx +13 -1
  92. package/corpus/templates/dispatch/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  93. package/corpus/templates/dispatch/changelog/2026-08-11-clicking-the-dispatch-logo-returns-to-the-overview-page.md +6 -0
  94. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-links-to-itself-from-the-app-switcher.md +6 -0
  95. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-settings-now-link-directly-to-admin.md +6 -0
  96. package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-shows-usage-trends-recent-prompts-and-an-agent-revi.md +6 -0
  97. package/corpus/templates/dispatch/changelog/2026-08-11-human-friendly-app-names.md +6 -0
  98. package/corpus/templates/dispatch/public/manifest.json +0 -1
  99. package/corpus/templates/dispatch/server/lib/identity-sso.ts +432 -158
  100. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +204 -123
  101. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +1 -0
  102. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +1 -1
  103. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +24 -0
  104. package/corpus/templates/factory/app/root.tsx +0 -1
  105. package/corpus/templates/factory/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  106. package/corpus/templates/factory/public/manifest.json +0 -1
  107. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +1 -1
  108. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +24 -0
  109. package/corpus/templates/forms/app/root.tsx +0 -1
  110. package/corpus/templates/forms/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  111. package/corpus/templates/forms/public/manifest.json +0 -1
  112. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +1 -1
  113. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +24 -0
  114. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +1 -1
  115. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +24 -0
  116. package/corpus/templates/mail/app/root.tsx +0 -1
  117. package/corpus/templates/mail/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  118. package/corpus/templates/mail/changelog/2026-08-11-google-sign-in-now-opens-the-gmail-connection-flow-directly.md +6 -0
  119. package/corpus/templates/mail/public/manifest.json +0 -1
  120. package/corpus/templates/mail/server/plugins/auth.ts +0 -10
  121. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +1 -1
  122. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +24 -0
  123. package/corpus/templates/plan/app/i18n/en-US.ts +2 -0
  124. package/corpus/templates/plan/app/pages/PlansPage.tsx +6 -0
  125. package/corpus/templates/plan/app/root.tsx +0 -1
  126. package/corpus/templates/plan/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  127. package/corpus/templates/plan/changelog/2026-08-11-plan-sharing-now-identifies-viewers-who-can-add-comments.md +6 -0
  128. package/corpus/templates/plan/public/manifest.json +0 -1
  129. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +1 -1
  130. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +24 -0
  131. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
  132. package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
  133. package/corpus/templates/slides/app/root.tsx +0 -1
  134. package/corpus/templates/slides/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  135. package/corpus/templates/slides/changelog/2026-08-11-share-dialog-uses-commenter-terminology.md +6 -0
  136. package/corpus/templates/slides/public/manifest.json +0 -1
  137. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +1 -1
  138. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +24 -0
  139. package/corpus/templates/tasks/app/root.tsx +0 -1
  140. package/corpus/templates/tasks/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  141. package/corpus/templates/tasks/public/manifest.json +0 -1
  142. package/dist/agent/production-agent.js +1 -22
  143. package/dist/agent/run-manager.d.ts +7 -0
  144. package/dist/agent/run-manager.js +46 -3
  145. package/dist/agent/tool-approval-migrations.d.ts +40 -0
  146. package/dist/agent/tool-approval-migrations.js +51 -0
  147. package/dist/agent/tool-approval-store.js +5 -20
  148. package/dist/cli/templates-meta.js +2 -2
  149. package/dist/client/AgentPanel.js +10 -1
  150. package/dist/client/AssistantChat.d.ts +3 -0
  151. package/dist/client/AssistantChat.js +148 -140
  152. package/dist/client/agent-page/AgentTabsPage.js +2 -2
  153. package/dist/client/chat/attachment-adapters.js +8 -2
  154. package/dist/client/chat/tool-call-display.d.ts +1 -0
  155. package/dist/client/chat/tool-call-display.js +3 -1
  156. package/dist/client/chat-first.js +11 -5
  157. package/dist/client/hooks/index.d.ts +1 -1
  158. package/dist/client/hooks/index.js +1 -1
  159. package/dist/client/org/OrgSwitcher.d.ts +3 -1
  160. package/dist/client/org/OrgSwitcher.js +22 -16
  161. package/dist/client/org/WorkspaceNotice.js +3 -1
  162. package/dist/client/resources/index.d.ts +1 -1
  163. package/dist/client/resources/index.js +1 -1
  164. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  165. package/dist/client/resources/use-mcp-servers.js +14 -0
  166. package/dist/client/settings/AccountSettingsCard.js +8 -4
  167. package/dist/client/settings/SettingsPanel.js +46 -13
  168. package/dist/client/settings/SettingsTabsPage.js +8 -3
  169. package/dist/client/sharing/ShareButton.d.ts +7 -1
  170. package/dist/client/sharing/ShareButton.js +14 -7
  171. package/dist/client/sharing/ShareDialog.js +5 -1
  172. package/dist/client/sharing/useShareButtonController.d.ts +4 -0
  173. package/dist/client/sharing/useShareButtonController.js +15 -0
  174. package/dist/client/sharing/useShareDialogController.d.ts +7 -0
  175. package/dist/client/sharing/useShareDialogController.js +14 -0
  176. package/dist/client/sse-event-processor.js +37 -0
  177. package/dist/client/use-session.d.ts +2 -0
  178. package/dist/client/use-session.js +69 -5
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/deploy/build.js +1 -1
  181. package/dist/file-upload/pre-upload-attachments.d.ts +3 -4
  182. package/dist/file-upload/pre-upload-attachments.js +65 -17
  183. package/dist/ingestion/index.d.ts +1 -0
  184. package/dist/ingestion/index.js +1 -0
  185. package/dist/ingestion/office.d.ts +3 -3
  186. package/dist/ingestion/office.js +24 -2
  187. package/dist/ingestion/spreadsheet.d.ts +41 -0
  188. package/dist/ingestion/spreadsheet.js +172 -0
  189. package/dist/integrations/adapters/slack.js +7 -3
  190. package/dist/localization/default-messages.d.ts +3 -0
  191. package/dist/localization/default-messages.js +3 -0
  192. package/dist/mcp/embed-app.js +20 -9
  193. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  194. package/dist/notifications/routes.d.ts +6 -6
  195. package/dist/oauth-tokens/migrations.d.ts +8 -0
  196. package/dist/oauth-tokens/migrations.js +36 -0
  197. package/dist/org/workspace-url.d.ts +8 -0
  198. package/dist/org/workspace-url.js +26 -2
  199. package/dist/secrets/routes.d.ts +3 -3
  200. package/dist/server/agent-chat-plugin.js +16 -8
  201. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  202. package/dist/server/auth.d.ts +0 -11
  203. package/dist/server/auth.js +37 -20
  204. package/dist/server/better-auth-migrations.js +23 -0
  205. package/dist/server/email-template.d.ts +5 -0
  206. package/dist/server/email-template.js +9 -0
  207. package/dist/server/embed-route.js +17 -5
  208. package/dist/server/identity-sso-store.d.ts +40 -66
  209. package/dist/server/identity-sso-store.js +187 -150
  210. package/dist/server/identity-sso.d.ts +17 -73
  211. package/dist/server/identity-sso.js +294 -297
  212. package/dist/server/onboarding-html.d.ts +0 -12
  213. package/dist/server/onboarding-html.js +46 -269
  214. package/dist/server/release-migrations.js +8 -0
  215. package/dist/shared/index.d.ts +1 -1
  216. package/dist/shared/index.js +1 -1
  217. package/dist/shared/workspace-app-id.d.ts +2 -0
  218. package/dist/shared/workspace-app-id.js +10 -0
  219. package/dist/sharing/actions/share-resource.d.ts +1 -0
  220. package/dist/sharing/actions/share-resource.js +20 -5
  221. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
  222. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
  223. package/dist/templates/chat/app/root.tsx +0 -1
  224. package/dist/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  225. package/dist/templates/chat/public/manifest.json +0 -1
  226. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +24 -0
  227. package/dist/templates/default/app/root.tsx +0 -1
  228. package/dist/templates/default/public/manifest.json +0 -1
  229. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +24 -0
  230. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +22 -11
  231. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +1 -1
  232. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +24 -0
  233. package/docs/content/cross-app-sso.mdx +85 -27
  234. package/docs/content/template-mail.mdx +0 -6
  235. package/package.json +4 -3
  236. package/src/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
  237. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
  238. package/src/templates/chat/app/root.tsx +0 -1
  239. package/src/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
  240. package/src/templates/chat/public/manifest.json +0 -1
  241. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +24 -0
  242. package/src/templates/default/app/root.tsx +0 -1
  243. package/src/templates/default/public/manifest.json +0 -1
  244. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +24 -0
  245. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +22 -11
  246. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +1 -1
  247. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +24 -0
  248. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +0 -6
  249. package/corpus/templates/clips/app/components/meetings/google-oauth-identity-notice.tsx +0 -51
  250. package/corpus/templates/clips/changelog/2026-07-30-google-calendar-warning.md +0 -6
@@ -164,6 +164,8 @@ type PopoverView =
164
164
  | "meetings"
165
165
  | "dictation";
166
166
 
167
+ type SettingsTabId = "general" | "recording" | "meetings" | "dictation";
168
+
167
169
  interface PopoverMeeting {
168
170
  id: string;
169
171
  title: string;
@@ -2622,8 +2624,12 @@ export function App() {
2622
2624
  // and we call `resize_popover` to match.
2623
2625
  const appRef = useRef<HTMLDivElement | null>(null);
2624
2626
  usePopoverAutoSize(appRef, {
2625
- disabled: !popoverVisible || isRecording || recordingFlowActive,
2626
- width: popoverView === "settings" || popoverView === "memory" ? 440 : 360,
2627
+ disabled:
2628
+ (popoverView !== "settings" && !popoverVisible) ||
2629
+ isRecording ||
2630
+ recordingFlowActive,
2631
+ width:
2632
+ popoverView === "settings" ? 920 : popoverView === "memory" ? 440 : 360,
2627
2633
  });
2628
2634
 
2629
2635
  const loadPendingUploads = useCallback(async () => {
@@ -5315,6 +5321,7 @@ function Setup({
5315
5321
  }) {
5316
5322
  const [url, setUrl] = useState(initial ?? DEFAULT_URL);
5317
5323
  const [readinessOpen, setReadinessOpen] = useState(false);
5324
+ const [settingsTab, setSettingsTab] = useState<SettingsTabId>("general");
5318
5325
  const featureConfig = useFeatureConfig();
5319
5326
  const updateStatus = useUpdateStatus();
5320
5327
  const voiceEnabled = featureConfig?.voiceEnabled !== false;
@@ -6069,13 +6076,6 @@ function Setup({
6069
6076
  updateStatus.state === "available" ||
6070
6077
  updateStatus.state === "downloading";
6071
6078
  const updateReady = updateStatus.state === "downloaded";
6072
- const updateStatusClass =
6073
- updateStatus.state === "error"
6074
- ? "setup-warning"
6075
- : updateStatus.state === "downloaded" ||
6076
- updateStatus.state === "not-available"
6077
- ? "setup-success"
6078
- : "setup-hint";
6079
6079
  const updateCheckLabel = !updateChecksSupported
6080
6080
  ? "Release builds only"
6081
6081
  : updateStatus.state === "checking"
@@ -6808,1071 +6808,843 @@ function Setup({
6808
6808
  );
6809
6809
  }
6810
6810
 
6811
- return (
6812
- <div className="setup">
6813
- <div className="setup-header" onMouseDown={handlePopoverHeaderMouseDown}>
6814
- {onCancel ? (
6815
- <button
6816
- type="button"
6817
- className="setup-back"
6818
- onClick={onCancel}
6819
- aria-label="Back"
6820
- >
6821
- <IconArrowLeft size={18} stroke={1.75} />
6822
- </button>
6823
- ) : null}
6824
- <h2>Settings</h2>
6825
- </div>
6826
-
6827
- <div className="setup-section-heading">General</div>
6828
-
6829
- <div className="setup-section">
6830
- <SettingLabel
6831
- label="Clips server URL"
6832
- hint="The URL of the Clips backend this tray app connects to."
6833
- htmlFor="clips-url"
6834
- />
6835
- <input
6836
- id="clips-url"
6837
- type="url"
6838
- value={url}
6839
- onChange={(e) => setUrl(e.target.value)}
6840
- placeholder="http://localhost:8080"
6841
- />
6842
- <button
6843
- className="secondary setup-connect-button"
6844
- type="button"
6845
- onClick={handleConnect}
6811
+ function renderGeneralSettings() {
6812
+ return (
6813
+ <div className="settings-tab-content">
6814
+ <DesktopSettingsGroup
6815
+ title="Connection"
6816
+ description="Choose the Clips backend used by this desktop recorder."
6846
6817
  >
6847
- Connect
6848
- </button>
6849
- </div>
6818
+ <DesktopSettingsRow
6819
+ label="Clips server URL"
6820
+ description="The backend this tray app connects to."
6821
+ control={
6822
+ <div className="settings-control-group settings-control-group--wide">
6823
+ <input
6824
+ id="clips-url"
6825
+ className="settings-url-input"
6826
+ type="url"
6827
+ value={url}
6828
+ onChange={(event) => setUrl(event.target.value)}
6829
+ placeholder="http://localhost:8080"
6830
+ />
6831
+ <button
6832
+ className="secondary"
6833
+ type="button"
6834
+ onClick={handleConnect}
6835
+ >
6836
+ Connect
6837
+ </button>
6838
+ </div>
6839
+ }
6840
+ />
6841
+ </DesktopSettingsGroup>
6850
6842
 
6851
- <div className="setup-section">
6852
- <div className="setup-toggle-row">
6853
- <SettingLabel
6843
+ <DesktopSettingsGroup
6844
+ title="App behavior"
6845
+ description="Keep Clips ready when you need it and out of the way when you do not."
6846
+ >
6847
+ <DesktopSettingsRow
6854
6848
  label="Open at login"
6855
- hint="Start Clips automatically when you sign in so recording, meetings, and dictation shortcuts are ready."
6856
- />
6857
- <Switch
6858
- on={launchAtLoginEnabled}
6859
- onChange={setLaunchAtLoginEnabled}
6860
- label="Open Clips at login"
6849
+ description="Start Clips when you sign in."
6850
+ control={
6851
+ <Switch
6852
+ on={launchAtLoginEnabled}
6853
+ onChange={setLaunchAtLoginEnabled}
6854
+ label="Open Clips at login"
6855
+ />
6856
+ }
6861
6857
  />
6862
- </div>
6863
- </div>
6864
-
6865
- <div className="setup-section">
6866
- <div className="setup-toggle-row">
6867
- <SettingLabel
6858
+ <DesktopSettingsRow
6868
6859
  label="Hide when inactive"
6869
- hint="When off, the Clips window stays open until you close it."
6870
- />
6871
- <Switch
6872
- on={autoHidePopoverEnabled}
6873
- onChange={setAutoHidePopoverEnabled}
6874
- label="Hide Clips when focus leaves"
6875
- />
6876
- </div>
6877
- </div>
6878
-
6879
- <div className="setup-section">
6880
- <div className="setup-toggle-row">
6881
- <SettingLabel
6882
- label="Show Clips in screen captures"
6883
- hint="When off, Clips windows and recording overlays stay out of screenshots, normal screen recordings, and Rewind. Turn on only for debugging or demos."
6884
- />
6885
- <Switch
6886
- on={showInScreenCapture}
6887
- onChange={setShowInScreenCapture}
6888
- label="Show Clips in screen captures"
6860
+ description="Hide the tray window when it loses focus."
6861
+ control={
6862
+ <Switch
6863
+ on={autoHidePopoverEnabled}
6864
+ onChange={setAutoHidePopoverEnabled}
6865
+ label="Hide Clips when focus leaves"
6866
+ />
6867
+ }
6889
6868
  />
6890
- </div>
6891
- </div>
6892
-
6893
- <div className="setup-section">
6894
- <SettingLabel
6895
- label="Desktop updates"
6896
- hint="Check the signed Clips desktop release channel and download any available update."
6897
- />
6898
- <div className="setup-button-row">
6899
- <button
6900
- type="button"
6901
- className="secondary"
6902
- onClick={checkForDesktopUpdate}
6903
- disabled={!updateChecksSupported || updateBusy || updateReady}
6904
- >
6905
- <IconRefresh
6906
- size={15}
6907
- stroke={1.9}
6908
- className={updateBusy ? "update-spinner" : undefined}
6909
- />
6910
- {updateCheckLabel}
6911
- </button>
6912
- {updateReady ? (
6913
- <button
6914
- type="button"
6915
- className="secondary"
6916
- onClick={() => {
6917
- installAndRestart().catch((err) => {
6918
- console.error("[clips-updater] relaunch failed:", err);
6919
- });
6920
- }}
6921
- >
6922
- Restart to install
6923
- </button>
6924
- ) : null}
6925
- </div>
6926
- <p className={updateStatusClass}>
6927
- {updateChecksSupported
6928
- ? desktopUpdateStatusText(updateStatus)
6929
- : "Update checks are available in signed release builds."}
6930
- </p>
6931
- </div>
6932
-
6933
- <div className="setup-section-heading">Permissions</div>
6934
-
6935
- <div className="setup-section">
6936
- <ReadinessPanel
6937
- mode="screen-camera"
6938
- cameraOn={true}
6939
- micOn={true}
6940
- includeVoicePaste={voiceEnabled}
6941
- includeFnMonitoring={fnShortcutSelected}
6942
- open={readinessOpen}
6943
- onOpenChange={setReadinessOpen}
6944
- onOpenPermission={openPrivacySettings}
6945
- />
6946
- </div>
6947
-
6948
- <div className="setup-section-heading">Recording</div>
6949
-
6950
- <div className="setup-section rewind-settings-entry">
6951
- <div className="rewind-settings-entry-main">
6952
- <span
6953
- className={`rewind-home-dot ${rewindStatusPresentation.isLive ? "is-live" : ""}`}
6869
+ <DesktopSettingsRow
6870
+ label="Show in screen captures"
6871
+ description="Include Clips windows in screenshots and recordings."
6872
+ control={
6873
+ <Switch
6874
+ on={showInScreenCapture}
6875
+ onChange={setShowInScreenCapture}
6876
+ label="Show Clips in screen captures"
6877
+ />
6878
+ }
6954
6879
  />
6955
- <div>
6956
- <strong>Rewind</strong>
6957
- <p className="setup-hint">{rewindStatusPresentation.title}</p>
6958
- </div>
6959
- </div>
6960
- <button type="button" className="secondary" onClick={onOpenRewind}>
6961
- Rewind settings
6962
- </button>
6963
- </div>
6880
+ </DesktopSettingsGroup>
6964
6881
 
6965
- <div className="setup-section">
6966
- <SettingLabel
6967
- label="Clip Drafts"
6968
- hint="Only clips you dismiss from the saved-upload card appear in Movies/Clips/Drafts. To retry a failed upload, return to the Clips popover and use Retry."
6969
- />
6970
- <button
6971
- type="button"
6972
- className="secondary"
6973
- onClick={openClipDraftsFolder}
6882
+ <DesktopSettingsGroup
6883
+ title="Updates"
6884
+ description="Keep the signed Clips desktop release current."
6974
6885
  >
6975
- <IconFolderOpen size={15} stroke={1.9} />
6976
- Open Clip Drafts
6977
- </button>
6978
- {clipDraftsError ? (
6979
- <p className="setup-warning">{clipDraftsError}</p>
6980
- ) : null}
6981
- </div>
6982
-
6983
- <div className="setup-section setup-rewind-legacy">
6984
- <div className="setup-toggle-row">
6985
- <SettingLabel
6986
- label="Rewind"
6987
- hint="A disabled-by-default rolling local archive for recent screen and app context. It never becomes a shared Clip on its own."
6988
- />
6989
- <Switch
6990
- on={screenMemory.enabled}
6991
- onChange={(enabled) =>
6992
- void setScreenMemoryConfig({ enabled, paused: false })
6886
+ <DesktopSettingsRow
6887
+ label="Desktop updates"
6888
+ description={
6889
+ updateChecksSupported
6890
+ ? desktopUpdateStatusText(updateStatus)
6891
+ : "Update checks are available in signed release builds."
6993
6892
  }
6994
- label="Enable Rewind"
6995
- disabled={screenMemoryConfigBusy || captureControlsLocked}
6996
- />
6997
- </div>
6998
- <p className="setup-hint">
6999
- Local-only by default. Media uploads and sharing happen only when you
7000
- make a normal Clip.
7001
- </p>
7002
- {screenMemory.enabled ? (
7003
- <>
7004
- <div className="setup-toggle-row">
7005
- <SettingLabel
7006
- label="Pause capture"
7007
- hint="Stop retaining new Rewind segments without clearing the local archive."
7008
- />
7009
- <Switch
7010
- on={screenMemory.paused}
7011
- onChange={(paused) => void setScreenMemoryConfig({ paused })}
7012
- label="Pause Rewind"
7013
- disabled={screenMemoryConfigBusy || captureControlsLocked}
7014
- />
7015
- </div>
7016
- <div className="setup-grid">
7017
- <label className="setup-mini-field">
7018
- <span>Retention</span>
7019
- <select
7020
- className="setup-select"
7021
- disabled={screenMemoryConfigBusy || captureControlsLocked}
7022
- value={screenMemory.retentionHours}
7023
- onChange={(event) =>
7024
- setScreenMemoryConfig({
7025
- retentionHours: Number(event.target.value),
7026
- })
7027
- }
7028
- >
7029
- <option value={8}>8 hours</option>
7030
- <option value={24}>24 hours</option>
7031
- </select>
7032
- </label>
7033
- <label className="setup-mini-field">
7034
- <span>Disk cap</span>
7035
- <select
7036
- className="setup-select"
7037
- disabled={screenMemoryConfigBusy}
7038
- value={screenMemory.maxBytes}
7039
- onChange={(event) =>
7040
- setScreenMemoryConfig({
7041
- maxBytes: Number(event.target.value),
7042
- })
7043
- }
7044
- >
7045
- <option value={5 * 1024 * 1024 * 1024}>5 GB</option>
7046
- <option value={20 * 1024 * 1024 * 1024}>20 GB</option>
7047
- <option value={50 * 1024 * 1024 * 1024}>50 GB</option>
7048
- </select>
7049
- </label>
7050
- </div>
7051
- <div className="setup-grid">
7052
- <label className="setup-mini-field">
7053
- <span>Capture mode</span>
7054
- <select
7055
- className="setup-select"
7056
- disabled={screenMemoryConfigBusy}
7057
- value={screenMemory.captureMode}
7058
- onChange={(event) =>
7059
- setScreenMemoryConfig({
7060
- captureMode: event.target.value as
7061
- | "visuals"
7062
- | "visuals-audio",
7063
- })
7064
- }
7065
- >
7066
- <option value="visuals">Visuals</option>
7067
- <option value="visuals-audio">Visuals + audio</option>
7068
- </select>
7069
- </label>
7070
- <label className="setup-mini-field">
7071
- <span>Agent handoff review</span>
7072
- <select
7073
- className="setup-select"
7074
- disabled={screenMemoryConfigBusy}
7075
- value={screenMemory.reviewBeforeSending ? "review" : "direct"}
7076
- onChange={(event) =>
7077
- setScreenMemoryConfig({
7078
- reviewBeforeSending: event.target.value === "review",
7079
- })
7080
- }
7081
- >
7082
- <option value="review">Review before sending</option>
7083
- <option value="direct">Send requested range directly</option>
7084
- </select>
7085
- </label>
7086
- </div>
7087
- <p className="setup-hint">
7088
- {screenMemory.captureMode === "visuals-audio"
7089
- ? "Visuals + audio is configured to retain microphone and system audio as separate local tracks."
7090
- : "Visuals retains screen and app context without selecting audio capture."}
7091
- </p>
7092
- <p className="setup-hint">
7093
- Agents receive bounded matching text when you ask. Raw Rewind
7094
- media remains local unless a bounded range becomes a private Clip.
7095
- </p>
7096
- <label className="setup-mini-field">
7097
- <span>Excluded apps</span>
7098
- <input
7099
- value={excludedBundleIdsInput}
7100
- onChange={(event) =>
7101
- setExcludedBundleIdsInput(event.target.value)
7102
- }
7103
- onBlur={() =>
7104
- setScreenMemoryConfig({
7105
- excludedBundleIds: parseExcludedBundleIds(
7106
- excludedBundleIdsInput,
7107
- ),
7108
- })
7109
- }
7110
- placeholder="com.example.private-app, com.example.vault"
7111
- aria-describedby="rewind-excluded-apps-hint"
7112
- />
7113
- </label>
7114
- <button
7115
- type="button"
7116
- className="secondary"
7117
- disabled={screenMemoryConfigBusy}
7118
- onClick={() =>
7119
- void setScreenMemoryConfig({
7120
- excludedBundleIds: parseExcludedBundleIds(
7121
- excludedBundleIdsInput,
7122
- ),
7123
- })
7124
- }
7125
- >
7126
- Apply exclusions
7127
- </button>
7128
- <p id="rewind-excluded-apps-hint" className="setup-hint">
7129
- Bundle IDs, comma-separated. Password managers are excluded by
7130
- default. Recognized bundle IDs stop media capture and discard the
7131
- entire in-flight segment; apps that do not expose a recognized
7132
- bundle ID cannot be detected.
7133
- </p>
7134
- <div className="whisper-status">
7135
- {rewindStatusPresentation.isLive ? (
7136
- <IconCircleCheck size={13} className="whisper-status-icon" />
7137
- ) : (
7138
- <IconAlertTriangle size={13} className="whisper-status-icon" />
7139
- )}
7140
- <span>
7141
- {rewindStatusPresentation.kind === "recording" &&
7142
- !rewindStatusPresentation.hasError
7143
- ? `Rewind is retaining local coverage: ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.`
7144
- : rewindStatusPresentation.detail}
7145
- </span>
7146
- </div>
7147
- {screenMemorySegments[0] ? (
7148
- <p className="setup-hint">
7149
- Latest local coverage:{" "}
7150
- {new Date(screenMemorySegments[0].endedAt).toLocaleTimeString()}
7151
- .
7152
- </p>
7153
- ) : null}
7154
- <div className="setup-advanced-body">
7155
- <SettingLabel
7156
- label="Ask Rewind"
7157
- hint="Searches app context, local transcripts, and local visual text on this Mac. Private mode allows this because no model or network service is called."
7158
- />
7159
- <div className="setup-button-row">
7160
- <input
7161
- value={rewindLocalQuery}
7162
- onChange={(event) => setRewindLocalQuery(event.target.value)}
7163
- placeholder="What was Lilian saying about the audience?"
7164
- aria-label="Ask Rewind locally"
7165
- maxLength={500}
7166
- onKeyDown={(event) => {
7167
- if (event.key !== "Enter") return;
7168
- event.preventDefault();
7169
- event.stopPropagation();
7170
- void askRewindLocally();
7171
- }}
7172
- />
6893
+ control={
6894
+ <div className="settings-control-group">
7173
6895
  <button
7174
6896
  type="button"
7175
6897
  className="secondary"
7176
- disabled={rewindLocalBusy || !rewindLocalQuery.trim()}
7177
- onClick={() => void askRewindLocally()}
6898
+ onClick={checkForDesktopUpdate}
6899
+ disabled={!updateChecksSupported || updateBusy || updateReady}
7178
6900
  >
7179
- {rewindLocalBusy ? "Searching…" : "Search locally"}
6901
+ <IconRefresh
6902
+ size={15}
6903
+ stroke={1.9}
6904
+ className={updateBusy ? "update-spinner" : undefined}
6905
+ />
6906
+ {updateCheckLabel}
7180
6907
  </button>
6908
+ {updateReady ? (
6909
+ <button
6910
+ type="button"
6911
+ className="secondary"
6912
+ onClick={() => {
6913
+ installAndRestart().catch((err) => {
6914
+ console.error("[clips-updater] relaunch failed:", err);
6915
+ });
6916
+ }}
6917
+ >
6918
+ Restart to install
6919
+ </button>
6920
+ ) : null}
7181
6921
  </div>
7182
- {rewindLocalError ? (
7183
- <p className="setup-warning">{rewindLocalError}</p>
7184
- ) : null}
7185
- {rewindLocalResult ? (
7186
- <div aria-live="polite">
7187
- <p className="setup-hint">
7188
- <strong>Local answer:</strong>{" "}
7189
- {rewindLocalResult.answerSummary}
7190
- </p>
7191
- <p className="setup-hint">
7192
- <strong>Confidence:</strong> {rewindLocalResult.confidence}
7193
- </p>
7194
- <p className="setup-hint">
7195
- <strong>Coverage:</strong>{" "}
7196
- {rewindLocalResult.coverage.segmentsConsidered} retained
7197
- segment
7198
- {rewindLocalResult.coverage.segmentsConsidered === 1
7199
- ? ""
7200
- : "s"}
7201
- ; {rewindLocalResult.coverage.transcriptIndexesReady}{" "}
7202
- transcript and {rewindLocalResult.coverage.ocrIndexesReady}{" "}
7203
- visual indexes ready.
7204
- {rewindLocalResult.coverage.gaps.length > 0
7205
- ? ` ${rewindLocalResult.coverage.gaps.length} capture or index gap${rewindLocalResult.coverage.gaps.length === 1 ? "" : "s"} may hide matches.`
7206
- : " No known capture or index gaps."}
7207
- </p>
7208
- {rewindLocalResult.coverage.gaps.length > 0 ? (
7209
- <details className="setup-advanced">
7210
- <summary className="setup-advanced-summary">
7211
- Coverage gaps
7212
- </summary>
7213
- <div className="setup-advanced-body">
7214
- {rewindLocalResult.coverage.gaps
7215
- .slice(0, 10)
7216
- .map((gap, index) => (
7217
- <p
7218
- className="setup-hint"
7219
- key={`${gap.kind}-${gap.source}-${gap.startedAt ?? index}`}
7220
- >
7221
- <strong>{gap.source}</strong> · {gap.detail}
7222
- </p>
7223
- ))}
7224
- </div>
7225
- </details>
7226
- ) : null}
7227
- {rewindLocalResult.evidence.length === 0 ? (
7228
- <p className="setup-hint">No matching evidence to show.</p>
7229
- ) : (
7230
- rewindLocalResult.evidence.map((evidence) => (
7231
- <div className="setup-section" key={evidence.id}>
7232
- <p className="setup-hint">
7233
- <strong>{evidence.sourceType}</strong> ·{" "}
7234
- {new Date(evidence.capturedAt).toLocaleString()}
7235
- {typeof evidence.confidence === "number"
7236
- ? ` · ${Math.round(evidence.confidence * 100)}% OCR confidence`
7237
- : ""}
7238
- <br />
7239
- {evidence.excerpt}
7240
- </p>
7241
- <button
7242
- type="button"
7243
- className="secondary"
7244
- onClick={() => void replayRewindMoment(evidence)}
7245
- disabled={rewindReplayId === evidence.id}
7246
- >
7247
- <IconExternalLink size={14} stroke={1.9} />
7248
- {rewindReplayId === evidence.id
7249
- ? "Preparing replay…"
7250
- : "Replay moment"}
7251
- </button>
7252
- </div>
7253
- ))
7254
- )}
7255
- {rewindLocalResult.truncated ? (
7256
- <p className="setup-hint">
7257
- More local matches exist; results are bounded to the
7258
- strongest 12.
7259
- </p>
7260
- ) : null}
7261
- </div>
7262
- ) : null}
7263
- </div>
7264
- <div className="setup-button-row">
6922
+ }
6923
+ />
6924
+ </DesktopSettingsGroup>
6925
+
6926
+ <DesktopSettingsGroup
6927
+ title="Account & diagnostics"
6928
+ description="Manage the local desktop session and find troubleshooting information."
6929
+ >
6930
+ {signedInAs && onSignOut ? (
6931
+ <DesktopSettingsRow
6932
+ label="Signed in"
6933
+ description={signedInAs}
6934
+ control={
6935
+ <button
6936
+ type="button"
6937
+ className="link-button"
6938
+ onClick={onSignOut}
6939
+ >
6940
+ Sign out
6941
+ </button>
6942
+ }
6943
+ />
6944
+ ) : null}
6945
+ <DesktopSettingsRow
6946
+ label="Diagnostic logs"
6947
+ description="Open the local Clips logs folder."
6948
+ control={
7265
6949
  <button
7266
6950
  type="button"
7267
6951
  className="secondary"
7268
- onClick={exportScreenMemoryRecent}
7269
- disabled={screenMemoryBusy || screenMemorySegments.length === 0}
6952
+ onClick={() => {
6953
+ invoke("open_logs").catch((err) => {
6954
+ console.error("[clips-tray] open logs failed:", err);
6955
+ });
6956
+ }}
7270
6957
  >
7271
- <IconDownload size={15} stroke={1.9} />
7272
- Export 5 min
6958
+ <IconFolderOpen size={15} stroke={1.9} />
6959
+ Open logs
7273
6960
  </button>
6961
+ }
6962
+ />
6963
+ </DesktopSettingsGroup>
6964
+ </div>
6965
+ );
6966
+ }
6967
+
6968
+ function renderRecordingSettings() {
6969
+ return (
6970
+ <div className="settings-tab-content">
6971
+ <DesktopSettingsGroup
6972
+ title="Permissions"
6973
+ description="Clips needs access to capture your screen, camera, and microphone."
6974
+ >
6975
+ <div className="settings-group-inset">
6976
+ <ReadinessPanel
6977
+ mode="screen-camera"
6978
+ cameraOn={true}
6979
+ micOn={true}
6980
+ includeVoicePaste={voiceEnabled}
6981
+ includeFnMonitoring={fnShortcutSelected}
6982
+ open={readinessOpen}
6983
+ onOpenChange={setReadinessOpen}
6984
+ onOpenPermission={openPrivacySettings}
6985
+ />
6986
+ </div>
6987
+ </DesktopSettingsGroup>
6988
+
6989
+ <DesktopSettingsGroup
6990
+ title="Capture"
6991
+ description="Choose how recordings are saved and how the tray opens them."
6992
+ >
6993
+ <DesktopSettingsRow
6994
+ label="Rewind"
6995
+ description={rewindStatusPresentation.title}
6996
+ control={
7274
6997
  <button
7275
6998
  type="button"
7276
6999
  className="secondary"
7277
- onClick={openScreenMemoryFolder}
7000
+ onClick={() => onOpenRewind?.()}
7278
7001
  >
7279
- <IconFolderOpen size={15} stroke={1.9} />
7280
- Open folder
7002
+ Open Rewind settings
7281
7003
  </button>
7004
+ }
7005
+ />
7006
+ <DesktopSettingsRow
7007
+ label="Clip Drafts"
7008
+ description="Open local recordings that were kept after an upload issue."
7009
+ control={
7282
7010
  <button
7283
7011
  type="button"
7284
7012
  className="secondary"
7285
- onClick={clearScreenMemory}
7286
- disabled={screenMemoryBusy || screenMemorySegments.length === 0}
7013
+ onClick={openClipDraftsFolder}
7287
7014
  >
7288
- <IconTrash size={15} stroke={1.9} />
7289
- Clear Rewind
7015
+ <IconFolderOpen size={15} stroke={1.9} />
7016
+ Open Clip Drafts
7290
7017
  </button>
7291
- </div>
7292
- <details
7293
- className="setup-advanced"
7294
- open={rewindEgressOpen}
7295
- onToggle={(event) => {
7296
- const open = event.currentTarget.open;
7297
- setRewindEgressOpen(open);
7298
- if (open) refreshRewindEgressLog();
7299
- }}
7300
- >
7301
- <summary className="setup-advanced-summary">
7302
- Agent access log
7303
- </summary>
7304
- <div className="setup-advanced-body">
7305
- <p className="setup-hint">
7306
- Every bounded Rewind evidence request is recorded on this Mac
7307
- before matching text is returned. Raw media appears only as a
7308
- separate private Clip handoff.
7309
- </p>
7310
- {rewindEgressEvents.length === 0 ? (
7311
- <p className="setup-hint">No evidence requests yet.</p>
7312
- ) : (
7313
- rewindEgressEvents.slice(0, 10).map((event) => (
7314
- <details
7315
- className="setup-advanced"
7316
- key={`${event.requestId}-${event.state}`}
7317
- >
7318
- <summary className="popover-kv">
7319
- <span>
7320
- {new Date(event.occurredAt).toLocaleString()} ·{" "}
7321
- {event.state}
7322
- </span>
7323
- <strong>
7324
- {event.evidenceCount} item
7325
- {event.evidenceCount === 1 ? "" : "s"}
7326
- </strong>
7327
- </summary>
7328
- <div className="setup-advanced-body">
7329
- <p className="setup-hint">
7330
- <strong>{event.operation ?? "Agent access"}</strong>
7331
- {" · "}Request {event.requestId}
7332
- </p>
7333
- {event.receipt?.evidence?.map((evidence) => (
7334
- <p className="setup-hint" key={evidence.id}>
7335
- <strong>{evidence.sourceType}</strong>
7336
- {evidence.capturedAt
7337
- ? ` · ${new Date(evidence.capturedAt).toLocaleTimeString()}`
7338
- : ""}
7339
- <br />
7340
- Evidence {evidence.id} · moment {evidence.momentId}
7341
- </p>
7342
- ))}
7343
- {event.receipt?.frames?.map((frame) => (
7344
- <p className="setup-hint" key={frame.timestamp}>
7345
- <strong>Local frame</strong>
7346
- {` · ${new Date(frame.timestamp).toLocaleTimeString()}`}
7347
- <br />
7348
- Segment {frame.segmentId}
7349
- </p>
7350
- ))}
7351
- {event.receipt?.mediaInterval ? (
7352
- <p className="setup-hint">
7353
- <strong>Private Clip range</strong>
7354
- {` · ${new Date(event.receipt.mediaInterval.startAt).toLocaleTimeString()}–${new Date(event.receipt.mediaInterval.endAt).toLocaleTimeString()}`}
7355
- </p>
7356
- ) : null}
7357
- {!event.receipt ? (
7358
- <p className="setup-hint">
7359
- This completion record refers to the prepared
7360
- receipt with request ID {event.requestId}.
7361
- </p>
7362
- ) : null}
7363
- {event.error ? (
7364
- <p className="setup-warning">{event.error}</p>
7365
- ) : null}
7366
- </div>
7367
- </details>
7368
- ))
7369
- )}
7370
- </div>
7371
- </details>
7372
- {screenMemoryMessage ? (
7373
- <p
7374
- className={
7375
- screenMemoryMessage.kind === "ok"
7376
- ? "setup-success"
7377
- : "setup-warning"
7018
+ }
7019
+ >
7020
+ {clipDraftsError ? (
7021
+ <p className="setup-warning">{clipDraftsError}</p>
7022
+ ) : null}
7023
+ </DesktopSettingsRow>
7024
+ <DesktopSettingsRow
7025
+ label="Save recordings locally"
7026
+ description="Keep recordings on this Mac instead of uploading them."
7027
+ control={
7028
+ <select
7029
+ id="local-recording-mode"
7030
+ className="setup-select"
7031
+ value={localRecordingMode}
7032
+ onChange={(event) =>
7033
+ setLocalRecordingMode(
7034
+ event.target.value as LocalRecordingMode,
7035
+ )
7378
7036
  }
7379
7037
  >
7380
- {screenMemoryMessage.text}
7381
- </p>
7382
- ) : null}
7383
- </>
7384
- ) : null}
7385
- </div>
7386
-
7387
- <details className="setup-advanced">
7388
- <summary className="setup-advanced-summary">Advanced recording</summary>
7389
- <div className="setup-advanced-body">
7390
- <div className="setup-section">
7391
- <SettingLabel
7392
- label="Save recordings locally"
7393
- hint="Advanced local-only mode. Local recordings save to Movies/Clips and do not upload or create a Clip."
7394
- htmlFor="local-recording-mode"
7395
- />
7396
- <select
7397
- id="local-recording-mode"
7398
- className="setup-select"
7399
- value={localRecordingMode}
7400
- onChange={(event) =>
7401
- setLocalRecordingMode(event.target.value as LocalRecordingMode)
7402
- }
7403
- >
7404
- <option value="off">Cloud Clips (default)</option>
7405
- <option value="composed">One local composed video</option>
7406
- <option value="separate">
7407
- Two local files: desktop + camera
7408
- </option>
7409
- </select>
7410
- <p className="setup-hint">
7411
- Two-file mode records desktop with audio and a raw rectangular
7412
- camera video with no audio.
7413
- </p>
7414
- </div>
7415
-
7416
- <div className="setup-section">
7417
- <div className="setup-toggle-row">
7418
- <SettingLabel
7419
- label="Screen region guides"
7420
- hint="Show private rectangle guides over your screen while recording. They stay out of the saved Clip."
7421
- />
7422
- <Switch
7423
- on={regionGuides.enabled}
7424
- onChange={setRegionGuidesEnabled}
7425
- label="Show screen region guides while recording"
7426
- />
7427
- </div>
7428
- {regionGuides.enabled && (
7429
- <div className="setup-toggle-row">
7430
- <SettingLabel
7431
- label="Keep guides on screen even when not recording"
7432
- hint="Stays visible at all times so you can frame recordings made with other tools like OBS or QuickTime. Still excluded from every screen recording."
7038
+ <option value="off">Cloud Clips (default)</option>
7039
+ <option value="composed">One local composed video</option>
7040
+ <option value="separate">
7041
+ Two local files: desktop + camera
7042
+ </option>
7043
+ </select>
7044
+ }
7045
+ />
7046
+ <DesktopSettingsRow
7047
+ label="Screen region guides"
7048
+ description={
7049
+ regionGuideCount === 0
7050
+ ? "Frame recordings with private guides that never enter the saved Clip."
7051
+ : `${regionGuideCount} ${regionGuideCount === 1 ? "rectangle" : "rectangles"} saved.`
7052
+ }
7053
+ extraClassName="settings-row-extra--inline"
7054
+ control={
7055
+ <div className="settings-control-group">
7056
+ <Switch
7057
+ on={regionGuides.enabled}
7058
+ onChange={setRegionGuidesEnabled}
7059
+ label="Show screen region guides while recording"
7433
7060
  />
7061
+ <button
7062
+ type="button"
7063
+ className="secondary"
7064
+ onClick={openRegionGuideEditor}
7065
+ >
7066
+ <IconPencil size={15} stroke={1.9} />
7067
+ Edit preset
7068
+ </button>
7069
+ </div>
7070
+ }
7071
+ >
7072
+ {regionGuides.enabled ? (
7073
+ <>
7074
+ <span>Keep guides visible when you are not recording.</span>
7434
7075
  <Switch
7435
7076
  on={regionGuidesAlwaysVisible}
7436
7077
  onChange={setRegionGuidesAlwaysVisible}
7437
7078
  label="Keep region guides on screen even when not recording"
7438
7079
  />
7439
- </div>
7440
- )}
7441
- <div className="setup-button-row">
7442
- <button
7443
- type="button"
7444
- className="secondary"
7445
- onClick={openRegionGuideEditor}
7446
- >
7447
- <IconPencil size={15} stroke={1.9} />
7448
- Edit preset
7449
- </button>
7450
- <button
7451
- type="button"
7452
- className="secondary"
7453
- onClick={clearRegionGuidePreset}
7454
- disabled={regionGuideCount === 0}
7455
- >
7456
- <IconTrash size={15} stroke={1.9} />
7457
- Clear
7458
- </button>
7459
- </div>
7460
- <p className="setup-hint">
7461
- {regionGuideCount === 0
7462
- ? "No guide preset saved yet."
7463
- : `${regionGuideCount} ${regionGuideCount === 1 ? "rectangle" : "rectangles"} saved.`}
7464
- </p>
7465
- </div>
7466
- </div>
7467
- </details>
7468
-
7469
- <div className="setup-section">
7470
- <SettingLabel
7471
- label="Start/stop recording shortcut"
7472
- hint="Optional global shortcut for starting full-screen, region, or camera recordings and stopping the active recording."
7473
- />
7474
- <ShortcutRecorder
7475
- value={recordCustomShortcut}
7476
- placeholder="Record shortcut"
7477
- onChange={onRecordCustomShortcutChange}
7478
- />
7479
- <p className="setup-hint">
7480
- Window and browser-tab sources still open Clips first so the picker
7481
- can use a click.
7482
- </p>
7483
- </div>
7080
+ {regionGuideCount > 0 ? (
7081
+ <button
7082
+ type="button"
7083
+ className="settings-text-button"
7084
+ onClick={clearRegionGuidePreset}
7085
+ >
7086
+ Clear saved preset
7087
+ </button>
7088
+ ) : null}
7089
+ </>
7090
+ ) : null}
7091
+ </DesktopSettingsRow>
7092
+ </DesktopSettingsGroup>
7484
7093
 
7485
- <div className="setup-section">
7486
- <SettingLabel
7487
- label="Open Clips shortcut"
7488
- hint="Optional extra global shortcut for opening the tray popover. Cmd+Shift+L remains available."
7489
- />
7490
- <ShortcutRecorder
7491
- value={popoverCustomShortcut}
7492
- placeholder="Record shortcut"
7493
- onChange={onPopoverCustomShortcutChange}
7494
- />
7495
- <p className="setup-hint">
7496
- Use a modifier combination like Cmd+Shift+K. Leave empty to use only
7497
- Cmd+Shift+L.
7498
- </p>
7499
- {shortcutRegistrationError ? (
7500
- <p className="setup-warning">{shortcutRegistrationError}</p>
7501
- ) : null}
7094
+ <DesktopSettingsGroup
7095
+ title="Keyboard shortcuts"
7096
+ description="Optional global shortcuts for recording and opening the tray."
7097
+ >
7098
+ <DesktopSettingsRow
7099
+ label="Start/stop recording"
7100
+ description="Start full-screen, region, or camera recordings and stop the active recording."
7101
+ control={
7102
+ <ShortcutRecorder
7103
+ value={recordCustomShortcut}
7104
+ placeholder="Record shortcut"
7105
+ onChange={onRecordCustomShortcutChange}
7106
+ />
7107
+ }
7108
+ />
7109
+ <DesktopSettingsRow
7110
+ label="Open Clips"
7111
+ description="Open the tray popover; Cmd+Shift+L remains available."
7112
+ control={
7113
+ <ShortcutRecorder
7114
+ value={popoverCustomShortcut}
7115
+ placeholder="Record shortcut"
7116
+ onChange={onPopoverCustomShortcutChange}
7117
+ />
7118
+ }
7119
+ >
7120
+ <>
7121
+ <span>Leave this empty to use only Cmd+Shift+L.</span>
7122
+ {shortcutRegistrationError ? (
7123
+ <span className="setup-warning">
7124
+ {shortcutRegistrationError}
7125
+ </span>
7126
+ ) : null}
7127
+ </>
7128
+ </DesktopSettingsRow>
7129
+ </DesktopSettingsGroup>
7502
7130
  </div>
7131
+ );
7132
+ }
7503
7133
 
7504
- <div className="setup-section-heading">Meetings</div>
7505
-
7506
- <div className="setup-section">
7507
- <div className="setup-toggle-row">
7508
- <SettingLabel
7134
+ function renderMeetingSettings() {
7135
+ return (
7136
+ <div className="settings-tab-content">
7137
+ <DesktopSettingsGroup
7138
+ title="Meeting notes"
7139
+ description="Use calendar meetings to show reminders and start live transcription."
7140
+ >
7141
+ <DesktopSettingsRow
7509
7142
  label="Meeting notes"
7510
- hint="Use calendar meetings to show a notes widget and start live transcription."
7511
- />
7512
- <Switch
7513
- on={meetingsEnabled}
7514
- onChange={setMeetingsEnabled}
7515
- label="Enable meeting notes"
7143
+ description="Show the meeting widget and enable live notes."
7144
+ control={
7145
+ <Switch
7146
+ on={meetingsEnabled}
7147
+ onChange={setMeetingsEnabled}
7148
+ label="Enable meeting notes"
7149
+ />
7150
+ }
7516
7151
  />
7517
- </div>
7518
- </div>
7519
-
7520
- {meetingsEnabled ? (
7521
- <>
7522
- <div className="setup-section">
7523
- <SettingLabel
7524
- label="Meeting transcription"
7525
- hint="Choose whether Clips asks first, starts automatically, or waits for manual start."
7526
- htmlFor="meeting-transcription-mode"
7527
- />
7528
- <select
7529
- id="meeting-transcription-mode"
7530
- className="setup-select"
7531
- value={meetingTranscriptionMode}
7532
- onChange={(event) =>
7533
- setMeetingTranscriptionMode(
7534
- event.target.value as MeetingTranscriptionMode,
7535
- )
7536
- }
7537
- >
7538
- <option value="ask">Ask at meeting time</option>
7539
- <option value="auto">Auto-start during meeting times</option>
7540
- <option value="manual">Manual only</option>
7541
- </select>
7542
- <p className="setup-hint">
7543
- Auto-start still shows the notes pill while transcription is
7544
- active.
7545
- </p>
7546
- </div>
7547
-
7548
- <div className="setup-section">
7549
- <div className="setup-toggle-row">
7550
- <SettingLabel
7551
- label="Meeting widget"
7552
- hint="Show the on-screen meeting widget near calendar start times, even when macOS notifications are hidden."
7152
+ {meetingsEnabled ? (
7153
+ <>
7154
+ <DesktopSettingsRow
7155
+ label="Meeting transcription"
7156
+ description="Ask first, start automatically, or wait for a manual start."
7157
+ control={
7158
+ <select
7159
+ id="meeting-transcription-mode"
7160
+ className="setup-select"
7161
+ value={meetingTranscriptionMode}
7162
+ onChange={(event) =>
7163
+ setMeetingTranscriptionMode(
7164
+ event.target.value as MeetingTranscriptionMode,
7165
+ )
7166
+ }
7167
+ >
7168
+ <option value="ask">Ask at meeting time</option>
7169
+ <option value="auto">
7170
+ Auto-start during meeting times
7171
+ </option>
7172
+ <option value="manual">Manual only</option>
7173
+ </select>
7174
+ }
7553
7175
  />
7554
- <Switch
7555
- on={showMeetingWidgetEnabled}
7556
- onChange={setShowMeetingWidgetEnabled}
7557
- label="Show meeting widget"
7176
+ <DesktopSettingsRow
7177
+ label="Meeting widget"
7178
+ description="Show the on-screen meeting widget near calendar start times."
7179
+ control={
7180
+ <Switch
7181
+ on={showMeetingWidgetEnabled}
7182
+ onChange={setShowMeetingWidgetEnabled}
7183
+ label="Show meeting widget"
7184
+ />
7185
+ }
7558
7186
  />
7559
- </div>
7560
- </div>
7561
- </>
7562
- ) : null}
7563
-
7564
- <div className="setup-section-heading">Whisper</div>
7187
+ </>
7188
+ ) : null}
7189
+ </DesktopSettingsGroup>
7190
+ </div>
7191
+ );
7192
+ }
7565
7193
 
7566
- <div className="setup-section">
7567
- <div className="setup-toggle-row">
7568
- <SettingLabel
7194
+ function renderDictationSettings() {
7195
+ return (
7196
+ <div className="settings-tab-content">
7197
+ <DesktopSettingsGroup
7198
+ title="Local transcription"
7199
+ description="Use Whisper for offline transcription without an API key."
7200
+ >
7201
+ <DesktopSettingsRow
7569
7202
  label="Whisper model"
7570
- hint="Local AI model for offline transcription (dictation and meetings). No API key required."
7571
- />
7572
- <Switch
7573
- on={whisperModelEnabled}
7574
- onChange={whisper.setEnabled}
7575
- label="Enable Whisper model"
7203
+ description="Enable the local model for dictation and meetings."
7204
+ control={
7205
+ <Switch
7206
+ on={whisperModelEnabled}
7207
+ onChange={whisper.setEnabled}
7208
+ label="Enable Whisper model"
7209
+ />
7210
+ }
7576
7211
  />
7577
- </div>
7578
- <SettingLabel
7579
- label="Model"
7580
- hint="Larger models can improve transcription accuracy but use more storage and may run more slowly."
7581
- htmlFor="whisper-model"
7582
- />
7583
- <select
7584
- id="whisper-model"
7585
- className="setup-select"
7586
- value={whisperModelId}
7587
- onChange={(event) => whisper.setModelId(event.target.value)}
7588
- disabled={
7589
- whisperModels.length === 0 || whisperStatus?.state === "downloading"
7590
- }
7591
- >
7592
- {whisperModels.map((model) => (
7593
- <option key={model.id} value={model.id}>
7594
- {model.title} · {model.sizeMb} MB — {model.description}
7595
- </option>
7596
- ))}
7597
- </select>
7598
- {selectedWhisperModel ? (
7599
- <p className="setup-hint">{selectedWhisperModel.description}</p>
7600
- ) : null}
7601
- <WhisperModelStatusRow
7602
- status={whisperStatus}
7603
- enabled={whisperModelEnabled}
7604
- onDownload={whisper.triggerDownload}
7605
- />
7606
- {deletableModels.length > 0 ? (
7607
- <div className="whisper-other-models">
7608
- <p className="setup-hint">Other downloaded models</p>
7609
- {deletableModels.map((model) => (
7610
- <div key={model.id} className="whisper-other-model-row">
7611
- <span className="whisper-other-model-name">
7612
- {model.title} &middot; {model.sizeMb} MB
7613
- </span>
7614
- <button
7615
- type="button"
7616
- className="whisper-delete-btn"
7617
- onClick={() => whisper.deleteModel(model.id)}
7618
- >
7619
- <IconTrash size={13} />
7620
- Delete
7621
- </button>
7622
- </div>
7623
- ))}
7624
- </div>
7625
- ) : null}
7626
- </div>
7627
-
7628
- <div className="setup-section-heading">Dictation</div>
7212
+ <DesktopSettingsRow
7213
+ label="Model"
7214
+ description={
7215
+ selectedWhisperModel?.description ??
7216
+ "Choose the downloaded model used for offline transcription."
7217
+ }
7218
+ control={
7219
+ <select
7220
+ id="whisper-model"
7221
+ className="setup-select"
7222
+ value={whisperModelId}
7223
+ onChange={(event) => whisper.setModelId(event.target.value)}
7224
+ disabled={
7225
+ whisperModels.length === 0 ||
7226
+ whisperStatus?.state === "downloading"
7227
+ }
7228
+ >
7229
+ {whisperModels.map((model) => (
7230
+ <option key={model.id} value={model.id}>
7231
+ {model.title} · {model.sizeMb} MB — {model.description}
7232
+ </option>
7233
+ ))}
7234
+ </select>
7235
+ }
7236
+ >
7237
+ <>
7238
+ <WhisperModelStatusRow
7239
+ status={whisperStatus}
7240
+ enabled={whisperModelEnabled}
7241
+ onDownload={whisper.triggerDownload}
7242
+ />
7243
+ {deletableModels.length > 0 ? (
7244
+ <div className="whisper-other-models">
7245
+ <p className="setup-hint">Other downloaded models</p>
7246
+ {deletableModels.map((model) => (
7247
+ <div key={model.id} className="whisper-other-model-row">
7248
+ <span className="whisper-other-model-name">
7249
+ {model.title} &middot; {model.sizeMb} MB
7250
+ </span>
7251
+ <button
7252
+ type="button"
7253
+ className="whisper-delete-btn"
7254
+ onClick={() => whisper.deleteModel(model.id)}
7255
+ >
7256
+ <IconTrash size={13} />
7257
+ Delete
7258
+ </button>
7259
+ </div>
7260
+ ))}
7261
+ </div>
7262
+ ) : null}
7263
+ </>
7264
+ </DesktopSettingsRow>
7265
+ </DesktopSettingsGroup>
7629
7266
 
7630
- <div className="setup-section">
7631
- <div className="setup-toggle-row">
7632
- <SettingLabel
7267
+ <DesktopSettingsGroup
7268
+ title="Voice dictation"
7269
+ description="Speak to type anywhere on your Mac."
7270
+ >
7271
+ <DesktopSettingsRow
7633
7272
  label="Voice dictation"
7634
- hint="Speak to type anywhere on your Mac. Turn off to disable globally and remove the keyboard shortcuts."
7635
- />
7636
- <Switch
7637
- on={voiceEnabled}
7638
- onChange={setVoiceEnabled}
7639
- label="Enable voice dictation"
7273
+ description="Enable the global dictation shortcut."
7274
+ control={
7275
+ <Switch
7276
+ on={voiceEnabled}
7277
+ onChange={setVoiceEnabled}
7278
+ label="Enable voice dictation"
7279
+ />
7280
+ }
7640
7281
  />
7641
- </div>
7642
- </div>
7643
-
7644
- {voiceEnabled ? (
7645
- <>
7646
- <div className="setup-section">
7647
- <SettingLabel
7648
- label="Provider"
7649
- hint="Choose free on-device dictation, Builder.io cleanup, or a provider key you own."
7650
- htmlFor="voice-provider"
7651
- />
7652
- <select
7653
- id="voice-provider"
7654
- className="setup-select"
7655
- value={selectedMode}
7656
- onChange={(event) =>
7657
- selectProviderMode(event.target.value as VoiceProviderMode)
7658
- }
7659
- >
7660
- <option value="native">On-device (free, fast)</option>
7661
- <option value="whisper" disabled={!whisperModelEnabled}>
7662
- {whisperModelEnabled
7663
- ? "Local Whisper (offline AI)"
7664
- : "Local Whisper — enable Whisper model first"}
7665
- </option>
7666
- <option value="builder">Builder.io</option>
7667
- <option value="byok">Add your own key</option>
7668
- </select>
7669
- <p className="setup-hint">{providerHint[selectedMode]}</p>
7670
- {selectedMode === "whisper" && !whisperModelEnabled ? (
7671
- <p className="setup-warning">
7672
- Whisper model is disabled. Enable it in the Whisper section
7673
- above.
7674
- </p>
7675
- ) : null}
7676
- {providerWarning ? (
7677
- <p className="setup-warning">{providerWarning}</p>
7678
- ) : null}
7679
- {selectedMode === "builder" && !providerStatus?.builder ? (
7680
- <button
7681
- type="button"
7682
- className="secondary"
7683
- onClick={connectBuilder}
7282
+ {voiceEnabled ? (
7283
+ <>
7284
+ <DesktopSettingsRow
7285
+ label="Provider"
7286
+ description={providerHint[selectedMode]}
7287
+ control={
7288
+ <div className="settings-control-group">
7289
+ <select
7290
+ id="voice-provider"
7291
+ className="setup-select"
7292
+ value={selectedMode}
7293
+ onChange={(event) =>
7294
+ selectProviderMode(
7295
+ event.target.value as VoiceProviderMode,
7296
+ )
7297
+ }
7298
+ >
7299
+ <option value="native">On-device (free, fast)</option>
7300
+ <option value="whisper" disabled={!whisperModelEnabled}>
7301
+ {whisperModelEnabled
7302
+ ? "Local Whisper (offline AI)"
7303
+ : "Local Whisper — enable Whisper model first"}
7304
+ </option>
7305
+ <option value="builder">Builder.io</option>
7306
+ <option value="byok">Add your own key</option>
7307
+ </select>
7308
+ {selectedMode === "builder" && !providerStatus?.builder ? (
7309
+ <button
7310
+ type="button"
7311
+ className="secondary"
7312
+ onClick={connectBuilder}
7313
+ >
7314
+ Use Builder.io
7315
+ </button>
7316
+ ) : null}
7317
+ </div>
7318
+ }
7684
7319
  >
7685
- Use Builder.io
7686
- </button>
7687
- ) : null}
7688
- </div>
7320
+ {providerWarning ||
7321
+ (selectedMode === "whisper" && !whisperModelEnabled) ? (
7322
+ <p className="setup-warning">
7323
+ {providerWarning ??
7324
+ "Whisper model is disabled. Enable it in Local transcription."}
7325
+ </p>
7326
+ ) : null}
7327
+ </DesktopSettingsRow>
7328
+
7329
+ {selectedMode === "byok" ? (
7330
+ <>
7331
+ <DesktopSettingsRow
7332
+ label="Key provider"
7333
+ description="Choose the provider key used for cleanup."
7334
+ control={
7335
+ <select
7336
+ id="voice-byok-provider"
7337
+ className="setup-select"
7338
+ value={byokProvider}
7339
+ onChange={(event) => {
7340
+ setApiKeyMessage(null);
7341
+ onVoiceProviderChange(
7342
+ event.target.value as ByokVoiceProvider,
7343
+ );
7344
+ }}
7345
+ >
7346
+ <option value="gemini">
7347
+ Google Gemini (recommended)
7348
+ </option>
7349
+ <option value="groq">Groq</option>
7350
+ </select>
7351
+ }
7352
+ />
7353
+ <DesktopSettingsRow
7354
+ label="API key"
7355
+ description={
7356
+ providerStatus?.[byokProvider]
7357
+ ? `${labelForByokProvider(byokProvider)} key is set. Paste a new key to rotate it.`
7358
+ : `Save a ${keyForByokProvider(byokProvider)} key for cleanup.`
7359
+ }
7360
+ control={
7361
+ <div className="settings-control-group settings-control-group--wide">
7362
+ <input
7363
+ type="password"
7364
+ value={apiKeyValue}
7365
+ onChange={(event) =>
7366
+ setApiKeyValue(event.target.value)
7367
+ }
7368
+ onKeyDown={(event) => {
7369
+ if (event.key === "Enter") {
7370
+ event.preventDefault();
7371
+ saveApiKey();
7372
+ }
7373
+ }}
7374
+ placeholder={
7375
+ providerStatus?.[byokProvider]
7376
+ ? "Paste to rotate"
7377
+ : `Paste ${keyForByokProvider(byokProvider)}`
7378
+ }
7379
+ className="settings-url-input"
7380
+ />
7381
+ <button
7382
+ type="button"
7383
+ className="secondary"
7384
+ onClick={saveApiKey}
7385
+ disabled={!apiKeyValue.trim() || apiKeySaving}
7386
+ >
7387
+ {apiKeySaving
7388
+ ? "Saving..."
7389
+ : providerStatus?.[byokProvider]
7390
+ ? "Rotate"
7391
+ : "Save"}
7392
+ </button>
7393
+ </div>
7394
+ }
7395
+ >
7396
+ {apiKeyMessage ? (
7397
+ <p
7398
+ className={
7399
+ apiKeyMessage.kind === "ok"
7400
+ ? "setup-success"
7401
+ : "setup-warning"
7402
+ }
7403
+ >
7404
+ {apiKeyMessage.text}
7405
+ </p>
7406
+ ) : null}
7407
+ </DesktopSettingsRow>
7408
+ </>
7409
+ ) : null}
7689
7410
 
7690
- {selectedMode === "byok" ? (
7691
- <div className="setup-section">
7692
- <SettingLabel
7693
- label="Key provider"
7694
- hint="Choose which provider key to use for cleanup."
7695
- htmlFor="voice-byok-provider"
7696
- />
7697
- <select
7698
- id="voice-byok-provider"
7699
- className="setup-select"
7700
- value={byokProvider}
7701
- onChange={(event) => {
7702
- setApiKeyMessage(null);
7703
- onVoiceProviderChange(
7704
- event.target.value as ByokVoiceProvider,
7705
- );
7706
- }}
7707
- >
7708
- <option value="gemini">Google Gemini (recommended)</option>
7709
- <option value="groq">Groq</option>
7710
- </select>
7711
- <div className="setup-key-row">
7712
- <input
7713
- type="password"
7714
- value={apiKeyValue}
7715
- onChange={(event) => setApiKeyValue(event.target.value)}
7716
- onKeyDown={(event) => {
7717
- if (event.key === "Enter") {
7718
- event.preventDefault();
7719
- saveApiKey();
7411
+ {selectedMode !== "native" && selectedMode !== "whisper" ? (
7412
+ <DesktopSettingsRow
7413
+ label="Custom instructions"
7414
+ description="Guide casing, names, punctuation, and terms of art for LLM cleanup."
7415
+ stacked
7416
+ control={
7417
+ <textarea
7418
+ id="voice-instructions"
7419
+ className="setup-textarea"
7420
+ rows={4}
7421
+ value={voiceInstructions}
7422
+ onChange={(event) =>
7423
+ onVoiceInstructionsChange(event.target.value)
7424
+ }
7425
+ placeholder="Example: keep it casual and preserve technical terms exactly."
7426
+ />
7427
+ }
7428
+ />
7429
+ ) : null}
7430
+
7431
+ <DesktopSettingsRow
7432
+ label="Shortcut"
7433
+ description={shortcutHint[voiceShortcut]}
7434
+ control={
7435
+ <select
7436
+ id="voice-shortcut"
7437
+ className="setup-select"
7438
+ value={voiceShortcut}
7439
+ onChange={(event) =>
7440
+ onVoiceShortcutChange(
7441
+ event.target.value as VoiceShortcutPreference,
7442
+ )
7720
7443
  }
7721
- }}
7722
- placeholder={
7723
- providerStatus?.[byokProvider]
7724
- ? "Key is saved — paste to rotate"
7725
- : `Paste ${keyForByokProvider(byokProvider)} here`
7444
+ >
7445
+ <option value="cmd-shift-space">Cmd+Shift+Space</option>
7446
+ <option value="ctrl-shift-space">Ctrl+Shift+Space</option>
7447
+ <option value="custom">Custom shortcut</option>
7448
+ <option value="fn">
7449
+ Fn (globe, needs Input Monitoring)
7450
+ </option>
7451
+ <option value="both">All shortcuts (includes Fn)</option>
7452
+ </select>
7453
+ }
7454
+ />
7455
+ {voiceShortcut === "custom" ? (
7456
+ <DesktopSettingsRow
7457
+ label="Custom shortcut"
7458
+ description="Record the key combination used for dictation."
7459
+ control={
7460
+ <ShortcutRecorder
7461
+ value={voiceCustomShortcut}
7462
+ placeholder="Record voice shortcut"
7463
+ onChange={onVoiceCustomShortcutChange}
7464
+ />
7726
7465
  }
7727
- className="setup-key-input"
7728
7466
  />
7729
- <button
7730
- type="button"
7731
- className="secondary setup-key-save"
7732
- onClick={saveApiKey}
7733
- disabled={!apiKeyValue.trim() || apiKeySaving}
7734
- >
7735
- {apiKeySaving
7736
- ? "Saving..."
7737
- : providerStatus?.[byokProvider]
7738
- ? "Rotate"
7739
- : "Save"}
7740
- </button>
7741
- </div>
7742
- {providerStatus?.[byokProvider] ? (
7743
- <p className="setup-hint">
7744
- {labelForByokProvider(byokProvider)} key is set.
7745
- </p>
7746
7467
  ) : null}
7747
- {apiKeyMessage ? (
7748
- <p
7749
- className={
7750
- apiKeyMessage.kind === "ok"
7751
- ? "setup-success"
7752
- : "setup-warning"
7468
+ {isMacPlatform() && fnShortcutSelected ? (
7469
+ <DesktopSettingsRow
7470
+ label="Input Monitoring"
7471
+ description="Required when the Fn / globe shortcut is enabled."
7472
+ control={
7473
+ <button
7474
+ type="button"
7475
+ className="secondary"
7476
+ onClick={() => openPrivacySettings("input-monitoring")}
7477
+ >
7478
+ Open settings
7479
+ </button>
7753
7480
  }
7754
- >
7755
- {apiKeyMessage.text}
7756
- </p>
7481
+ />
7757
7482
  ) : null}
7758
- </div>
7759
- ) : null}
7760
-
7761
- {selectedMode !== "native" && selectedMode !== "whisper" ? (
7762
- <div className="setup-section">
7763
- <SettingLabel
7764
- label="Custom instructions"
7765
- hint="Included with LLM cleanup/transcription. Use this for casing, names, punctuation, tone, or terms of art."
7766
- htmlFor="voice-instructions"
7767
- />
7768
- <textarea
7769
- id="voice-instructions"
7770
- className="setup-textarea"
7771
- rows={4}
7772
- value={voiceInstructions}
7773
- onChange={(event) =>
7774
- onVoiceInstructionsChange(event.target.value)
7483
+ <DesktopSettingsRow
7484
+ label="Mode"
7485
+ description={modeHint[voiceMode]}
7486
+ control={
7487
+ <select
7488
+ id="voice-mode"
7489
+ className="setup-select"
7490
+ value={voiceMode}
7491
+ onChange={(event) =>
7492
+ onVoiceModeChange(event.target.value as VoiceMode)
7493
+ }
7494
+ >
7495
+ <option value="push-to-talk">Hold to dictate</option>
7496
+ <option value="toggle">
7497
+ Press to start, press to stop
7498
+ </option>
7499
+ </select>
7775
7500
  }
7776
- placeholder="Example: keep it casual, spell Builder.io with a dot, and preserve technical terms exactly."
7777
7501
  />
7778
- <p className="setup-hint">
7779
- These instructions are sent only when an LLM-based provider is
7780
- selected.
7781
- </p>
7782
- </div>
7502
+ </>
7783
7503
  ) : null}
7504
+ </DesktopSettingsGroup>
7505
+ </div>
7506
+ );
7507
+ }
7784
7508
 
7785
- <div className="setup-section">
7786
- <SettingLabel
7787
- label="Shortcut"
7788
- hint="The key combination that triggers voice dictation."
7789
- htmlFor="voice-shortcut"
7790
- />
7791
- <select
7792
- id="voice-shortcut"
7793
- className="setup-select"
7794
- value={voiceShortcut}
7795
- onChange={(event) =>
7796
- onVoiceShortcutChange(
7797
- event.target.value as VoiceShortcutPreference,
7798
- )
7799
- }
7509
+ const settingsTabs: Array<{
7510
+ id: SettingsTabId;
7511
+ label: string;
7512
+ group: string;
7513
+ }> = [
7514
+ { id: "general", label: "General", group: "Personal" },
7515
+ { id: "recording", label: "Recording", group: "Personal" },
7516
+ { id: "meetings", label: "Meetings", group: "Workflow" },
7517
+ { id: "dictation", label: "Dictation", group: "Workflow" },
7518
+ ];
7519
+ const activeSettingsTab =
7520
+ settingsTabs.find((tab) => tab.id === settingsTab) ?? settingsTabs[0];
7521
+
7522
+ function renderSettingsTab() {
7523
+ switch (settingsTab) {
7524
+ case "recording":
7525
+ return renderRecordingSettings();
7526
+ case "meetings":
7527
+ return renderMeetingSettings();
7528
+ case "dictation":
7529
+ return renderDictationSettings();
7530
+ case "general":
7531
+ default:
7532
+ return renderGeneralSettings();
7533
+ }
7534
+ }
7535
+
7536
+ return (
7537
+ <div className="setup settings-page">
7538
+ <div
7539
+ className="setup-header settings-page-header"
7540
+ onMouseDown={handlePopoverHeaderMouseDown}
7541
+ >
7542
+ {onCancel ? (
7543
+ <button
7544
+ type="button"
7545
+ className="setup-back"
7546
+ onClick={onCancel}
7547
+ aria-label="Back"
7548
+ >
7549
+ <IconArrowLeft size={18} stroke={1.75} />
7550
+ </button>
7551
+ ) : null}
7552
+ <div className="settings-page-title">
7553
+ <h2>Settings</h2>
7554
+ <p>Clips desktop preferences</p>
7555
+ </div>
7556
+ </div>
7557
+
7558
+ <div className="settings-page-layout">
7559
+ <nav className="settings-nav" aria-label="Settings sections">
7560
+ {settingsTabs.map((tab, index) => (
7561
+ <div
7562
+ key={tab.id}
7563
+ className={`settings-nav-group ${
7564
+ index === 0 || settingsTabs[index - 1]?.group !== tab.group
7565
+ ? "settings-nav-group--start"
7566
+ : ""
7567
+ }`}
7800
7568
  >
7801
- <option value="cmd-shift-space">Cmd+Shift+Space</option>
7802
- <option value="ctrl-shift-space">Ctrl+Shift+Space</option>
7803
- <option value="custom">Custom shortcut</option>
7804
- <option value="fn">Fn (globe, needs Input Monitoring)</option>
7805
- <option value="both">All shortcuts (includes Fn)</option>
7806
- </select>
7807
- {voiceShortcut === "custom" ? (
7808
- <ShortcutRecorder
7809
- value={voiceCustomShortcut}
7810
- placeholder="Record voice shortcut"
7811
- onChange={onVoiceCustomShortcutChange}
7812
- />
7813
- ) : null}
7814
- <p className="setup-hint">{shortcutHint[voiceShortcut]}</p>
7815
- {isMacPlatform() && fnShortcutSelected ? (
7569
+ {index === 0 || settingsTabs[index - 1]?.group !== tab.group ? (
7570
+ <div className="settings-nav-group-label">{tab.group}</div>
7571
+ ) : null}
7816
7572
  <button
7817
7573
  type="button"
7818
- className="secondary"
7819
- onClick={() => openPrivacySettings("input-monitoring")}
7574
+ className={`settings-nav-button ${settingsTab === tab.id ? "is-active" : ""}`}
7575
+ aria-current={settingsTab === tab.id ? "page" : undefined}
7576
+ onClick={() => setSettingsTab(tab.id)}
7820
7577
  >
7821
- Open Input Monitoring
7578
+ {tab.label}
7822
7579
  </button>
7823
- ) : null}
7824
- </div>
7825
-
7826
- <div className="setup-section">
7827
- <SettingLabel
7828
- label="Mode"
7829
- hint="Whether you hold the shortcut while speaking or toggle it on and off."
7830
- htmlFor="voice-mode"
7831
- />
7832
- <select
7833
- id="voice-mode"
7834
- className="setup-select"
7835
- value={voiceMode}
7836
- onChange={(event) =>
7837
- onVoiceModeChange(event.target.value as VoiceMode)
7838
- }
7839
- >
7840
- <option value="push-to-talk">Hold to dictate</option>
7841
- <option value="toggle">Press to start, press to stop</option>
7842
- </select>
7843
- <p className="setup-hint">{modeHint[voiceMode]}</p>
7580
+ </div>
7581
+ ))}
7582
+ </nav>
7583
+ <main className="settings-page-content" tabIndex={-1}>
7584
+ <div className="settings-tab-heading">
7585
+ <h3>{activeSettingsTab.label}</h3>
7586
+ <p>
7587
+ {settingsTab === "general"
7588
+ ? "Connection, app behavior, updates, and diagnostics."
7589
+ : settingsTab === "recording"
7590
+ ? "Capture behavior, permissions, drafts, and shortcuts."
7591
+ : settingsTab === "meetings"
7592
+ ? "Meeting reminders and live transcription behavior."
7593
+ : "Local transcription and voice dictation controls."}
7594
+ </p>
7844
7595
  </div>
7845
- </>
7846
- ) : null}
7596
+ {renderSettingsTab()}
7597
+ </main>
7598
+ </div>
7599
+ </div>
7600
+ );
7601
+ }
7847
7602
 
7848
- <div className="setup-section-heading">Debug</div>
7603
+ function DesktopSettingsGroup({
7604
+ title,
7605
+ description,
7606
+ children,
7607
+ }: {
7608
+ title: string;
7609
+ description?: string;
7610
+ children: ReactNode;
7611
+ }) {
7612
+ return (
7613
+ <section className="settings-group">
7614
+ <header className="settings-group-header">
7615
+ <h4>{title}</h4>
7616
+ {description ? <p>{description}</p> : null}
7617
+ </header>
7618
+ <div className="settings-group-body">{children}</div>
7619
+ </section>
7620
+ );
7621
+ }
7849
7622
 
7850
- <div className="setup-account setup-account--no-border">
7851
- <button
7852
- type="button"
7853
- className="link-button"
7854
- onClick={() => {
7855
- invoke("open_logs").catch((err) => {
7856
- console.error("[clips-tray] open logs failed:", err);
7857
- });
7858
- }}
7859
- style={{ display: "flex", alignItems: "center", gap: 6 }}
7860
- >
7861
- <IconFolderOpen size={14} />
7862
- Open logs
7863
- </button>
7623
+ function DesktopSettingsRow({
7624
+ label,
7625
+ description,
7626
+ control,
7627
+ children,
7628
+ extraClassName,
7629
+ stacked = false,
7630
+ }: {
7631
+ label: string;
7632
+ description: ReactNode;
7633
+ control?: ReactNode;
7634
+ children?: ReactNode;
7635
+ extraClassName?: string;
7636
+ stacked?: boolean;
7637
+ }) {
7638
+ return (
7639
+ <div className={`settings-row ${stacked ? "settings-row--stacked" : ""}`}>
7640
+ <div className="settings-row-copy">
7641
+ <strong>{label}</strong>
7642
+ <span>{description}</span>
7864
7643
  </div>
7865
- {signedInAs && onSignOut ? (
7866
- <div className="setup-account">
7867
- <span className="setup-account-email">{signedInAs}</span>
7868
- <button
7869
- type="button"
7870
- className="link-button"
7871
- onClick={onSignOut}
7872
- style={{ background: "transparent", border: "none" }}
7873
- >
7874
- Sign out
7875
- </button>
7644
+ {control ? <div className="settings-row-control">{control}</div> : null}
7645
+ {children ? (
7646
+ <div className={`settings-row-extra ${extraClassName ?? ""}`}>
7647
+ {children}
7876
7648
  </div>
7877
7649
  ) : null}
7878
7650
  </div>