@agent-native/core 0.90.3 → 0.90.4

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 (191) hide show
  1. package/bin/agent-native.js +25 -3
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +9 -0
  4. package/corpus/core/bin/agent-native.js +25 -3
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/engine/index.ts +1 -0
  7. package/corpus/core/src/agent/engine/registry.ts +63 -2
  8. package/corpus/core/src/cli/create.ts +35 -0
  9. package/corpus/core/src/cli/design-connect.ts +248 -0
  10. package/corpus/core/src/cli/index.ts +84 -7
  11. package/corpus/core/src/cli/workspacify.ts +34 -0
  12. package/corpus/core/src/client/history/VersionHistoryPanel.tsx +77 -47
  13. package/corpus/core/src/client/history/index.ts +3 -0
  14. package/corpus/core/src/client/history/use-history.ts +28 -0
  15. package/corpus/core/src/client/index.ts +8 -0
  16. package/corpus/core/src/client/review/index.ts +5 -0
  17. package/corpus/core/src/client/review/use-review.ts +40 -2
  18. package/corpus/core/src/history/actions/create-resource-version.ts +11 -8
  19. package/corpus/core/src/history/index.ts +0 -1
  20. package/corpus/core/src/history/registry.ts +24 -12
  21. package/corpus/core/src/history/store.ts +84 -53
  22. package/corpus/core/src/review/actions/consume-review-feedback.ts +15 -5
  23. package/corpus/core/src/review/actions/delete-review-comment.ts +15 -3
  24. package/corpus/core/src/review/actions/resolve-review-thread.ts +12 -5
  25. package/corpus/core/src/review/index.ts +0 -5
  26. package/corpus/core/src/review/registry.ts +24 -12
  27. package/corpus/core/src/review/store.ts +10 -7
  28. package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
  29. package/corpus/core/src/server/credential-provider.ts +13 -3
  30. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +11 -11
  31. package/corpus/templates/assets/actions/generate-image.ts +64 -28
  32. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +1 -1
  33. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +91 -74
  34. package/corpus/templates/assets/server/lib/generation.ts +29 -4
  35. package/corpus/templates/assets/server/lib/image-processing.ts +131 -0
  36. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +24 -2
  37. package/corpus/templates/calendar/AGENTS.md +3 -0
  38. package/corpus/templates/calendar/actions/create-event.ts +8 -34
  39. package/corpus/templates/calendar/actions/event-action-helpers.ts +52 -0
  40. package/corpus/templates/calendar/actions/manage-event-draft.ts +7 -34
  41. package/corpus/templates/calendar/actions/update-event.ts +10 -23
  42. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +47 -0
  43. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +23 -0
  44. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +59 -2
  45. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +40 -1
  46. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +34 -1
  47. package/corpus/templates/calendar/app/hooks/use-events.ts +6 -0
  48. package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
  49. package/corpus/templates/calendar/app/i18n-data.ts +40 -0
  50. package/corpus/templates/calendar/changelog/2026-07-07-you-can-mark-event-guests-as-optional-while-inviting-them-or.md +6 -0
  51. package/corpus/templates/calendar/server/handlers/events.ts +1 -0
  52. package/corpus/templates/calendar/server/lib/google-calendar.ts +4 -0
  53. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  54. package/corpus/templates/calendar/shared/api.ts +2 -0
  55. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +29 -3
  56. package/corpus/templates/clips/actions/generate-workflow.ts +19 -8
  57. package/corpus/templates/clips/actions/get-clips-ai-prefs.ts +36 -0
  58. package/corpus/templates/clips/actions/lib/clips-ai-prefs.ts +28 -0
  59. package/corpus/templates/clips/actions/regenerate-chapters.ts +21 -7
  60. package/corpus/templates/clips/actions/regenerate-summary.ts +26 -8
  61. package/corpus/templates/clips/actions/regenerate-title.ts +44 -7
  62. package/corpus/templates/clips/actions/update-clips-ai-prefs.ts +50 -0
  63. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +7 -2
  64. package/corpus/templates/clips/app/hooks/use-auto-title.ts +25 -10
  65. package/corpus/templates/clips/app/i18n/ar-SA.ts +28 -6
  66. package/corpus/templates/clips/app/i18n/de-DE.ts +28 -6
  67. package/corpus/templates/clips/app/i18n/en-US.ts +29 -6
  68. package/corpus/templates/clips/app/i18n/es-ES.ts +29 -6
  69. package/corpus/templates/clips/app/i18n/fr-FR.ts +30 -6
  70. package/corpus/templates/clips/app/i18n/hi-IN.ts +26 -6
  71. package/corpus/templates/clips/app/i18n/ja-JP.ts +27 -6
  72. package/corpus/templates/clips/app/i18n/ko-KR.ts +28 -6
  73. package/corpus/templates/clips/app/i18n/pt-BR.ts +28 -6
  74. package/corpus/templates/clips/app/i18n/zh-CN.ts +26 -6
  75. package/corpus/templates/clips/app/i18n/zh-TW.ts +25 -6
  76. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +73 -0
  77. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -0
  78. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -2
  79. package/corpus/templates/clips/changelog/2026-07-07-ai-tools-can-include-the-full-video-not-just-the-transcript-.md +6 -0
  80. package/corpus/templates/clips/changelog/2026-07-07-clips-desktop-now-surfaces-meetings-and-dictate-directly-in-.md +6 -0
  81. package/corpus/templates/clips/changelog/2026-07-07-meeting-notes-now-explain-exactly-how-to-start-granola-style.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-07-07-meeting-start-alerts-now-include-separate-start-notes-and-jo.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-07-07-recordings-start-more-predictably-and-newly-saved-clips-refr.md +6 -0
  84. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  85. package/corpus/templates/clips/desktop/src/app.tsx +435 -105
  86. package/corpus/templates/clips/desktop/src/lib/recorder.ts +27 -21
  87. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +23 -22
  88. package/corpus/templates/clips/desktop/src/styles.css +167 -2
  89. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  90. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -7
  91. package/corpus/templates/clips/desktop/src-tauri/src/tray_meetings.rs +5 -4
  92. package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +16 -8
  93. package/corpus/templates/clips/shared/clips-ai-prefs.ts +81 -0
  94. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +16 -1
  95. package/corpus/templates/design/actions/add-localhost-screens.ts +1 -0
  96. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +345 -11
  97. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +89 -14
  98. package/corpus/templates/design/app/components/design/bridge-security.ts +7 -1
  99. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -1
  100. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  101. package/corpus/templates/design/app/i18n-data.ts +41 -0
  102. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-uses-live-app-iframes-for-click-to-edit-pr.md +6 -0
  103. package/dist/agent/engine/index.d.ts +1 -1
  104. package/dist/agent/engine/index.d.ts.map +1 -1
  105. package/dist/agent/engine/index.js +1 -1
  106. package/dist/agent/engine/index.js.map +1 -1
  107. package/dist/agent/engine/registry.d.ts +14 -0
  108. package/dist/agent/engine/registry.d.ts.map +1 -1
  109. package/dist/agent/engine/registry.js +56 -2
  110. package/dist/agent/engine/registry.js.map +1 -1
  111. package/dist/cli/create.d.ts.map +1 -1
  112. package/dist/cli/create.js +31 -0
  113. package/dist/cli/create.js.map +1 -1
  114. package/dist/cli/design-connect.d.ts.map +1 -1
  115. package/dist/cli/design-connect.js +186 -0
  116. package/dist/cli/design-connect.js.map +1 -1
  117. package/dist/cli/index.js +76 -9
  118. package/dist/cli/index.js.map +1 -1
  119. package/dist/cli/workspacify.d.ts.map +1 -1
  120. package/dist/cli/workspacify.js +30 -0
  121. package/dist/cli/workspacify.js.map +1 -1
  122. package/dist/client/history/VersionHistoryPanel.d.ts +1 -1
  123. package/dist/client/history/VersionHistoryPanel.d.ts.map +1 -1
  124. package/dist/client/history/VersionHistoryPanel.js +12 -5
  125. package/dist/client/history/VersionHistoryPanel.js.map +1 -1
  126. package/dist/client/history/index.d.ts +1 -1
  127. package/dist/client/history/index.d.ts.map +1 -1
  128. package/dist/client/history/index.js +1 -1
  129. package/dist/client/history/index.js.map +1 -1
  130. package/dist/client/history/use-history.d.ts +12 -0
  131. package/dist/client/history/use-history.d.ts.map +1 -1
  132. package/dist/client/history/use-history.js +6 -0
  133. package/dist/client/history/use-history.js.map +1 -1
  134. package/dist/client/index.d.ts +2 -2
  135. package/dist/client/index.d.ts.map +1 -1
  136. package/dist/client/index.js +2 -2
  137. package/dist/client/index.js.map +1 -1
  138. package/dist/client/review/index.d.ts +1 -1
  139. package/dist/client/review/index.d.ts.map +1 -1
  140. package/dist/client/review/index.js +1 -1
  141. package/dist/client/review/index.js.map +1 -1
  142. package/dist/client/review/use-review.d.ts +23 -0
  143. package/dist/client/review/use-review.d.ts.map +1 -1
  144. package/dist/client/review/use-review.js +8 -0
  145. package/dist/client/review/use-review.js.map +1 -1
  146. package/dist/collab/awareness.d.ts +2 -2
  147. package/dist/collab/awareness.d.ts.map +1 -1
  148. package/dist/collab/routes.d.ts +1 -1
  149. package/dist/history/actions/create-resource-version.js +7 -4
  150. package/dist/history/actions/create-resource-version.js.map +1 -1
  151. package/dist/history/index.d.ts +1 -1
  152. package/dist/history/index.d.ts.map +1 -1
  153. package/dist/history/index.js +1 -1
  154. package/dist/history/index.js.map +1 -1
  155. package/dist/history/registry.d.ts.map +1 -1
  156. package/dist/history/registry.js +14 -12
  157. package/dist/history/registry.js.map +1 -1
  158. package/dist/history/store.d.ts.map +1 -1
  159. package/dist/history/store.js +74 -49
  160. package/dist/history/store.js.map +1 -1
  161. package/dist/observability/routes.d.ts +3 -3
  162. package/dist/progress/routes.d.ts +1 -1
  163. package/dist/review/actions/consume-review-feedback.d.ts +1 -0
  164. package/dist/review/actions/consume-review-feedback.js +8 -2
  165. package/dist/review/actions/consume-review-feedback.js.map +1 -1
  166. package/dist/review/actions/delete-review-comment.d.ts +2 -1
  167. package/dist/review/actions/delete-review-comment.js +11 -3
  168. package/dist/review/actions/delete-review-comment.js.map +1 -1
  169. package/dist/review/actions/resolve-review-thread.d.ts +2 -1
  170. package/dist/review/actions/resolve-review-thread.js +5 -2
  171. package/dist/review/actions/resolve-review-thread.js.map +1 -1
  172. package/dist/review/index.d.ts +1 -1
  173. package/dist/review/index.d.ts.map +1 -1
  174. package/dist/review/index.js +1 -1
  175. package/dist/review/index.js.map +1 -1
  176. package/dist/review/registry.d.ts.map +1 -1
  177. package/dist/review/registry.js +14 -12
  178. package/dist/review/registry.js.map +1 -1
  179. package/dist/review/store.d.ts +3 -3
  180. package/dist/review/store.d.ts.map +1 -1
  181. package/dist/review/store.js +7 -4
  182. package/dist/review/store.js.map +1 -1
  183. package/dist/secrets/routes.d.ts +9 -9
  184. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  185. package/dist/server/agent-chat-plugin.js +5 -0
  186. package/dist/server/agent-chat-plugin.js.map +1 -1
  187. package/dist/server/credential-provider.d.ts +2 -0
  188. package/dist/server/credential-provider.d.ts.map +1 -1
  189. package/dist/server/credential-provider.js +13 -3
  190. package/dist/server/credential-provider.js.map +1 -1
  191. package/package.json +1 -1
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  IconAlertTriangle,
3
3
  IconArrowLeft,
4
+ IconCalendarEvent,
4
5
  IconCircleCheck,
5
6
  IconDownload,
6
7
  IconExternalLink,
7
8
  IconFolderOpen,
8
9
  IconPencil,
9
10
  IconInfoCircle,
11
+ IconMicrophone2,
10
12
  IconRefresh,
11
13
  IconTrash,
12
14
  IconUpload,
@@ -16,6 +18,7 @@ import { emit, listen } from "@tauri-apps/api/event";
16
18
  import { getCurrentWindow } from "@tauri-apps/api/window";
17
19
  import { open as openExternal } from "@tauri-apps/plugin-shell";
18
20
  import {
21
+ type ReactNode,
19
22
  type RefObject,
20
23
  useCallback,
21
24
  useEffect,
@@ -26,7 +29,6 @@ import {
26
29
  import { FeedbackButton } from "./components/FeedbackButton";
27
30
  import {
28
31
  CamIcon,
29
- ClockIcon,
30
32
  CloseIcon,
31
33
  GoogleIcon,
32
34
  LibraryIcon,
@@ -93,14 +95,6 @@ import {
93
95
  type ScreenMemoryStatus,
94
96
  } from "./shared/config";
95
97
 
96
- interface RecordingSummary {
97
- id: string;
98
- title: string;
99
- durationMs: number;
100
- thumbnailUrl: string | null;
101
- updatedAt: string;
102
- }
103
-
104
98
  interface PendingNativeUpload {
105
99
  kind: "native";
106
100
  recordingId: string;
@@ -121,6 +115,18 @@ interface PendingNativeUpload {
121
115
 
122
116
  type PendingDesktopUpload = PendingNativeUpload | PendingBrowserRecordingUpload;
123
117
 
118
+ type PopoverView = "recorder" | "settings" | "meetings" | "dictation";
119
+
120
+ interface PopoverMeeting {
121
+ id: string;
122
+ title: string;
123
+ scheduledStart: string | null;
124
+ scheduledEnd: string | null;
125
+ joinUrl: string | null;
126
+ platform: string | null;
127
+ transcriptStatus: string | null;
128
+ }
129
+
124
130
  interface LocalRecordingNotice {
125
131
  folderPath?: string;
126
132
  files: LocalExportedFile[];
@@ -184,25 +190,6 @@ const DEFAULT_URL = import.meta.env.DEV
184
190
  ? "http://localhost:8094"
185
191
  : "https://clips.agent-native.com";
186
192
 
187
- function resolveDesktopThumbnailUrl(
188
- rawUrl: string | null | undefined,
189
- serverUrl: string,
190
- ): string | null {
191
- if (!rawUrl) return null;
192
- if (
193
- rawUrl.startsWith("http://") ||
194
- rawUrl.startsWith("https://") ||
195
- rawUrl.startsWith("data:") ||
196
- rawUrl.startsWith("blob:")
197
- ) {
198
- return rawUrl;
199
- }
200
- if (rawUrl.startsWith("/")) {
201
- return `${serverUrl.replace(/\/+$/, "")}${rawUrl}`;
202
- }
203
- return rawUrl;
204
- }
205
-
206
193
  function normalizeCaptureSource(value: string): CaptureSource {
207
194
  if (value === "region" && isMacPlatform()) return "region";
208
195
  return value === "window" ? "window" : "full-screen";
@@ -467,6 +454,78 @@ function formatAgo(iso: string): string {
467
454
  }
468
455
  }
469
456
 
457
+ function formatMeetingWhen(meeting: PopoverMeeting): string {
458
+ const startMs = Date.parse(meeting.scheduledStart ?? "");
459
+ if (Number.isNaN(startMs)) return "Upcoming";
460
+
461
+ const endMs = Date.parse(meeting.scheduledEnd ?? "");
462
+ const now = Date.now();
463
+ if (startMs <= now && (Number.isNaN(endMs) || endMs >= now)) {
464
+ return "Now";
465
+ }
466
+
467
+ if (startMs > now && startMs - now < 60 * 60 * 1000) {
468
+ return `in ${Math.max(1, Math.round((startMs - now) / 60000))}m`;
469
+ }
470
+
471
+ const start = new Date(startMs);
472
+ const time = start.toLocaleTimeString([], {
473
+ hour: "numeric",
474
+ minute: "2-digit",
475
+ });
476
+ const today = new Date(now);
477
+ const tomorrow = new Date(now + 24 * 60 * 60 * 1000);
478
+ if (start.toDateString() === today.toDateString()) return `Today ${time}`;
479
+ if (start.toDateString() === tomorrow.toDateString()) {
480
+ return `Tomorrow ${time}`;
481
+ }
482
+
483
+ return `${start.toLocaleDateString([], {
484
+ weekday: "short",
485
+ month: "short",
486
+ day: "numeric",
487
+ })} ${time}`;
488
+ }
489
+
490
+ function meetingCanStartNotes(meeting: PopoverMeeting): boolean {
491
+ const startMs = Date.parse(meeting.scheduledStart ?? "");
492
+ if (Number.isNaN(startMs)) return false;
493
+ const endMs = Date.parse(meeting.scheduledEnd ?? "");
494
+ const now = Date.now();
495
+ return (
496
+ startMs <= now + 10 * 60 * 1000 && (Number.isNaN(endMs) || endMs >= now)
497
+ );
498
+ }
499
+
500
+ function voiceShortcutLabel(
501
+ shortcut: VoiceShortcutPreference,
502
+ customShortcut: string,
503
+ ): string {
504
+ switch (shortcut) {
505
+ case "fn":
506
+ return "Fn";
507
+ case "cmd-shift-space":
508
+ return "Cmd+Shift+Space";
509
+ case "ctrl-shift-space":
510
+ return "Ctrl+Shift+Space";
511
+ case "custom":
512
+ return customShortcut || "Custom shortcut";
513
+ case "both":
514
+ return "Fn, Cmd+Shift+Space, or Ctrl+Shift+Space";
515
+ }
516
+ }
517
+
518
+ function voiceProviderLabel(provider: VoiceProvider): string {
519
+ if (provider === "whisper") return "Local Whisper";
520
+ if (provider === "builder" || provider === "builder-gemini") {
521
+ return "Builder.io cleanup";
522
+ }
523
+ if (provider === "gemini") return "Google Gemini";
524
+ if (provider === "groq") return "Groq";
525
+ if (provider === "macos-native") return "macOS on-device";
526
+ return "Browser speech";
527
+ }
528
+
470
529
  function formatFileSize(bytes: number): string {
471
530
  if (!Number.isFinite(bytes) || bytes <= 0) return "";
472
531
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
@@ -628,7 +687,6 @@ export function App() {
628
687
  const localRecordingMode: LocalRecordingMode =
629
688
  featureConfig?.localRecordingMode ?? "off";
630
689
 
631
- const [recordings, setRecordings] = useState<RecordingSummary[]>([]);
632
690
  const [pendingUploads, setPendingUploads] = useState<PendingDesktopUpload[]>(
633
691
  [],
634
692
  );
@@ -641,11 +699,16 @@ export function App() {
641
699
  );
642
700
  const [localRecordingNotice, setLocalRecordingNotice] =
643
701
  useState<LocalRecordingNotice | null>(null);
644
- const [showRecent, setShowRecent] = useState(false);
702
+ const [popoverView, setPopoverView] = useState<PopoverView>("recorder");
703
+ const [meetings, setMeetings] = useState<PopoverMeeting[]>([]);
704
+ const [meetingsLoading, setMeetingsLoading] = useState(false);
705
+ const [meetingsError, setMeetingsError] = useState<string | null>(null);
706
+ const [meetingStartMessage, setMeetingStartMessage] = useState<string | null>(
707
+ null,
708
+ );
645
709
  const [readinessOpen, setReadinessOpen] = useState<boolean>(
646
710
  () => !loadBool(READINESS_REVIEWED_KEY, false),
647
711
  );
648
- const [showSettings, setShowSettings] = useState(false);
649
712
  const [recorder, setRecorder] = useState<RecorderHandle | null>(null);
650
713
  const [recError, setRecError] = useState<string | null>(null);
651
714
  const [cameraError, setCameraError] = useState<string | null>(null);
@@ -1029,6 +1092,58 @@ export function App() {
1029
1092
  [serverUrl],
1030
1093
  );
1031
1094
 
1095
+ const fetchUpcomingMeetings = useCallback(async () => {
1096
+ if (authStatus !== "authed") {
1097
+ setMeetings([]);
1098
+ setMeetingsError(null);
1099
+ return;
1100
+ }
1101
+
1102
+ setMeetingsLoading(true);
1103
+ setMeetingsError(null);
1104
+ try {
1105
+ const result = await callClipsAction<{ meetings?: unknown[] }>(
1106
+ "list-meetings",
1107
+ { view: "upcoming", limit: 3, upcomingWithinMin: 24 * 60 },
1108
+ { method: "GET" },
1109
+ );
1110
+ const list = Array.isArray(result.meetings) ? result.meetings : [];
1111
+ setMeetings(
1112
+ list.slice(0, 3).map((raw) => {
1113
+ const meeting = raw as Partial<PopoverMeeting>;
1114
+ return {
1115
+ id: String(meeting.id ?? ""),
1116
+ title: meeting.title || "Untitled meeting",
1117
+ scheduledStart: meeting.scheduledStart ?? null,
1118
+ scheduledEnd: meeting.scheduledEnd ?? null,
1119
+ joinUrl: meeting.joinUrl ?? null,
1120
+ platform: meeting.platform ?? null,
1121
+ transcriptStatus: meeting.transcriptStatus ?? null,
1122
+ };
1123
+ }),
1124
+ );
1125
+ } catch (err) {
1126
+ setMeetings([]);
1127
+ setMeetingsError(
1128
+ err instanceof Error ? err.message : "Could not load meetings.",
1129
+ );
1130
+ } finally {
1131
+ setMeetingsLoading(false);
1132
+ }
1133
+ }, [authStatus, callClipsAction]);
1134
+
1135
+ const startMeetingNotes = useCallback((meeting: PopoverMeeting) => {
1136
+ setMeetingStartMessage(`Starting notes for ${meeting.title}…`);
1137
+ emit("meetings:start-transcription", {
1138
+ meetingId: meeting.id,
1139
+ joinUrl: meeting.joinUrl,
1140
+ reason: "user",
1141
+ }).catch((err) => {
1142
+ console.error("[clips-popover] start meeting notes failed:", err);
1143
+ setMeetingStartMessage("Could not start notes. Try again from Meetings.");
1144
+ });
1145
+ }, []);
1146
+
1032
1147
  useMeetingTranscription({
1033
1148
  callClipsAction,
1034
1149
  serverUrl,
@@ -1155,7 +1270,7 @@ export function App() {
1155
1270
  }
1156
1271
  clearDesktopAuthToken(serverUrl);
1157
1272
  await checkAuth();
1158
- setShowSettings(false);
1273
+ setPopoverView("recorder");
1159
1274
  }
1160
1275
 
1161
1276
  // ---- Esc closes the popover --------------------------------------------
@@ -1529,33 +1644,9 @@ export function App() {
1529
1644
  const appRef = useRef<HTMLDivElement | null>(null);
1530
1645
  usePopoverAutoSize(appRef, {
1531
1646
  disabled: !popoverVisible || isRecording || recordingFlowActive,
1532
- width: showSettings ? 440 : 360,
1647
+ width: popoverView === "settings" ? 440 : 360,
1533
1648
  });
1534
1649
 
1535
- // ---- recent list --------------------------------------------------------
1536
-
1537
- const fetchRecent = useCallback(async () => {
1538
- if (authStatus !== "authed") return; // don't bother; would just 401
1539
- try {
1540
- const url = `${serverUrl.replace(/\/+$/, "")}/_agent-native/actions/list-recordings?limit=3&sort=recent`;
1541
- const res = await fetch(url, { credentials: "include" });
1542
- if (!res.ok) return;
1543
- const json = await res.json();
1544
- const list = Array.isArray(json?.recordings) ? json.recordings : [];
1545
- setRecordings(
1546
- list.slice(0, 3).map((r: any) => ({
1547
- id: r.id,
1548
- title: r.title ?? "Untitled",
1549
- durationMs: r.durationMs ?? 0,
1550
- thumbnailUrl: r.thumbnailUrl ?? null,
1551
- updatedAt: r.updatedAt ?? r.createdAt,
1552
- })),
1553
- );
1554
- } catch {
1555
- // ignore — server may be unreachable, we still render the chrome
1556
- }
1557
- }, [serverUrl, authStatus]);
1558
-
1559
1650
  const loadPendingUploads = useCallback(async () => {
1560
1651
  try {
1561
1652
  const nativeList = await invoke<Omit<PendingNativeUpload, "kind">[]>(
@@ -1580,8 +1671,10 @@ export function App() {
1580
1671
  }, []);
1581
1672
 
1582
1673
  useEffect(() => {
1583
- fetchRecent();
1584
- }, [fetchRecent]);
1674
+ if (popoverView === "meetings" && popoverVisible) {
1675
+ void fetchUpcomingMeetings();
1676
+ }
1677
+ }, [fetchUpcomingMeetings, popoverView, popoverVisible]);
1585
1678
 
1586
1679
  useEffect(() => {
1587
1680
  loadPendingUploads();
@@ -1651,7 +1744,6 @@ export function App() {
1651
1744
  });
1652
1745
  }
1653
1746
  await loadPendingUploads();
1654
- await fetchRecent();
1655
1747
  await openExternal(`${targetServerUrl}/r/${upload.recordingId}`);
1656
1748
  getCurrentWindow()
1657
1749
  .hide()
@@ -2008,7 +2100,7 @@ export function App() {
2008
2100
  return;
2009
2101
  }
2010
2102
 
2011
- setShowSettings(false);
2103
+ setPopoverView("recorder");
2012
2104
  if (authStatus === "anon" && localRecordingMode === "off") {
2013
2105
  setRecError("Sign in to Clips before using the recording shortcut.");
2014
2106
  invoke("show_popover").catch(() => {});
@@ -2148,9 +2240,6 @@ export function App() {
2148
2240
  .catch(() => {});
2149
2241
  emit("clips:popover-visible", false).catch(() => {});
2150
2242
  }
2151
- if (!stopResult?.localOnly) {
2152
- fetchRecent();
2153
- }
2154
2243
  }
2155
2244
  }
2156
2245
  }),
@@ -2210,7 +2299,7 @@ export function App() {
2210
2299
  });
2211
2300
  unlisteners.length = 0;
2212
2301
  };
2213
- }, [recorder, fetchRecent, loadPendingUploads]);
2302
+ }, [recorder, loadPendingUploads]);
2214
2303
 
2215
2304
  // Auto-hide on blur is handled on the Rust side (tauri::WindowEvent::Focused).
2216
2305
 
@@ -2224,7 +2313,7 @@ export function App() {
2224
2313
  // we just render the normal pre-record panel so the user at least knows
2225
2314
  // where they are. No recording-only UI lives here.
2226
2315
 
2227
- if (showSettings) {
2316
+ if (popoverView === "settings") {
2228
2317
  return (
2229
2318
  <div className="app app-settings" ref={appRef}>
2230
2319
  <Setup
@@ -2250,9 +2339,48 @@ export function App() {
2250
2339
  onConnect={(url) => {
2251
2340
  saveString(STORAGE_KEY, url.replace(/\/+$/, ""));
2252
2341
  setServerUrl(url.replace(/\/+$/, ""));
2253
- setShowSettings(false);
2342
+ setPopoverView("recorder");
2254
2343
  }}
2255
- onCancel={() => setShowSettings(false)}
2344
+ onCancel={() => setPopoverView("recorder")}
2345
+ />
2346
+ </div>
2347
+ );
2348
+ }
2349
+
2350
+ if (popoverView === "meetings") {
2351
+ return (
2352
+ <div className="app app-popover-view" ref={appRef}>
2353
+ <MeetingsPopoverView
2354
+ meetings={meetings}
2355
+ loading={meetingsLoading}
2356
+ error={meetingsError}
2357
+ startMessage={meetingStartMessage}
2358
+ meetingsEnabled={featureConfig?.meetingsEnabled !== false}
2359
+ onBack={() => setPopoverView("recorder")}
2360
+ onRefresh={fetchUpcomingMeetings}
2361
+ onOpenMeetings={() => openInBrowser("/meetings")}
2362
+ onOpenMeeting={(meetingId) =>
2363
+ openInBrowser(`/meetings/${encodeURIComponent(meetingId)}`)
2364
+ }
2365
+ onOpenSettings={() => setPopoverView("settings")}
2366
+ onStartNotes={startMeetingNotes}
2367
+ />
2368
+ </div>
2369
+ );
2370
+ }
2371
+
2372
+ if (popoverView === "dictation") {
2373
+ return (
2374
+ <div className="app app-popover-view" ref={appRef}>
2375
+ <DictationPopoverView
2376
+ voiceEnabled={voiceDictationEnabled}
2377
+ voiceShortcut={voiceShortcut}
2378
+ voiceCustomShortcut={voiceCustomShortcut}
2379
+ voiceMode={voiceMode}
2380
+ voiceProvider={voiceProvider}
2381
+ onBack={() => setPopoverView("recorder")}
2382
+ onOpenDictate={() => openInBrowser("/dictate")}
2383
+ onOpenSettings={() => setPopoverView("settings")}
2256
2384
  />
2257
2385
  </div>
2258
2386
  );
@@ -2309,7 +2437,7 @@ export function App() {
2309
2437
  </>
2310
2438
  )}
2311
2439
  <div className="footer">
2312
- <a className="footer-link" onClick={() => setShowSettings(true)}>
2440
+ <a className="footer-link" onClick={() => setPopoverView("settings")}>
2313
2441
  Settings
2314
2442
  </a>
2315
2443
  </div>
@@ -2472,45 +2600,22 @@ export function App() {
2472
2600
  onClick={() => openInBrowser("/")}
2473
2601
  />
2474
2602
  <BottomButton
2475
- icon="settings"
2476
- label="Settings"
2477
- onClick={() => setShowSettings(true)}
2603
+ icon="meetings"
2604
+ label="Meetings"
2605
+ onClick={() => setPopoverView("meetings")}
2478
2606
  />
2479
2607
  <BottomButton
2480
- icon="recent"
2481
- label="Recent"
2608
+ icon="dictation"
2609
+ label="Dictate"
2482
2610
  badge={undefined}
2483
- onClick={() => setShowRecent((v) => !v)}
2611
+ onClick={() => setPopoverView("dictation")}
2612
+ />
2613
+ <BottomButton
2614
+ icon="settings"
2615
+ label="Settings"
2616
+ onClick={() => setPopoverView("settings")}
2484
2617
  />
2485
2618
  </div>
2486
-
2487
- {showRecent && recordings.length > 0 ? (
2488
- <div className="recent-list">
2489
- {recordings.map((r) => (
2490
- <button
2491
- key={r.id}
2492
- className="recent-item"
2493
- onClick={() => openInBrowser(`/r/${r.id}`)}
2494
- >
2495
- {r.thumbnailUrl ? (
2496
- <img
2497
- className="thumb"
2498
- src={
2499
- resolveDesktopThumbnailUrl(r.thumbnailUrl, serverUrl) ?? ""
2500
- }
2501
- alt=""
2502
- />
2503
- ) : (
2504
- <div className="thumb thumb-placeholder" />
2505
- )}
2506
- <div className="recent-meta">
2507
- <div className="recent-title">{r.title}</div>
2508
- <div className="recent-sub">{formatAgo(r.updatedAt)}</div>
2509
- </div>
2510
- </button>
2511
- ))}
2512
- </div>
2513
- ) : null}
2514
2619
  </div>
2515
2620
  );
2516
2621
  }
@@ -3126,13 +3231,236 @@ function SignInForm({
3126
3231
  );
3127
3232
  }
3128
3233
 
3234
+ function PopoverSubViewHeader({
3235
+ title,
3236
+ onBack,
3237
+ action,
3238
+ }: {
3239
+ title: string;
3240
+ onBack: () => void;
3241
+ action?: ReactNode;
3242
+ }) {
3243
+ return (
3244
+ <div className="setup-header popover-view-header">
3245
+ <button
3246
+ type="button"
3247
+ className="setup-back"
3248
+ onClick={onBack}
3249
+ aria-label="Back"
3250
+ >
3251
+ <IconArrowLeft size={18} stroke={1.75} />
3252
+ </button>
3253
+ <h2>{title}</h2>
3254
+ <div className="popover-view-header-spacer" />
3255
+ {action}
3256
+ </div>
3257
+ );
3258
+ }
3259
+
3260
+ function MeetingsPopoverView({
3261
+ meetings,
3262
+ loading,
3263
+ error,
3264
+ startMessage,
3265
+ meetingsEnabled,
3266
+ onBack,
3267
+ onRefresh,
3268
+ onOpenMeetings,
3269
+ onOpenMeeting,
3270
+ onOpenSettings,
3271
+ onStartNotes,
3272
+ }: {
3273
+ meetings: PopoverMeeting[];
3274
+ loading: boolean;
3275
+ error: string | null;
3276
+ startMessage: string | null;
3277
+ meetingsEnabled: boolean;
3278
+ onBack: () => void;
3279
+ onRefresh: () => void;
3280
+ onOpenMeetings: () => void;
3281
+ onOpenMeeting: (meetingId: string) => void;
3282
+ onOpenSettings: () => void;
3283
+ onStartNotes: (meeting: PopoverMeeting) => void;
3284
+ }) {
3285
+ return (
3286
+ <div className="setup popover-view">
3287
+ <PopoverSubViewHeader
3288
+ title="Meetings"
3289
+ onBack={onBack}
3290
+ action={
3291
+ <button
3292
+ type="button"
3293
+ className="link-button popover-view-link"
3294
+ onClick={onOpenMeetings}
3295
+ >
3296
+ Open web
3297
+ </button>
3298
+ }
3299
+ />
3300
+
3301
+ <div className="setup-section">
3302
+ <p className="setup-hint">
3303
+ Start Granola-style live notes from calendar meetings without hunting
3304
+ through Settings.
3305
+ </p>
3306
+ </div>
3307
+
3308
+ {!meetingsEnabled ? (
3309
+ <div className="popover-empty-card">
3310
+ <strong>Meeting notes are off</strong>
3311
+ <p>Turn them on to show reminders and start live transcription.</p>
3312
+ <button type="button" className="secondary" onClick={onOpenSettings}>
3313
+ Open meeting settings
3314
+ </button>
3315
+ </div>
3316
+ ) : error ? (
3317
+ <div className="popover-empty-card">
3318
+ <strong>Could not load meetings</strong>
3319
+ <p>{error}</p>
3320
+ <button type="button" className="secondary" onClick={onRefresh}>
3321
+ Try again
3322
+ </button>
3323
+ </div>
3324
+ ) : loading ? (
3325
+ <div className="popover-empty-card">
3326
+ <strong>Loading meetings…</strong>
3327
+ <p>Checking your connected calendar.</p>
3328
+ </div>
3329
+ ) : meetings.length === 0 ? (
3330
+ <div className="popover-empty-card">
3331
+ <strong>No meetings ready</strong>
3332
+ <p>Connect Google Calendar or open the Meetings page to see setup.</p>
3333
+ <button type="button" className="secondary" onClick={onOpenMeetings}>
3334
+ Open Meetings
3335
+ </button>
3336
+ </div>
3337
+ ) : (
3338
+ <div className="popover-list">
3339
+ {meetings.map((meeting) => {
3340
+ const canStart = meetingCanStartNotes(meeting);
3341
+ return (
3342
+ <div className="popover-list-item" key={meeting.id}>
3343
+ <div className="popover-list-icon">
3344
+ <IconCalendarEvent size={17} stroke={1.75} />
3345
+ </div>
3346
+ <div className="popover-list-main">
3347
+ <div className="popover-list-title">{meeting.title}</div>
3348
+ <div className="popover-list-sub">
3349
+ {formatMeetingWhen(meeting)}
3350
+ {meeting.platform ? ` · ${meeting.platform}` : ""}
3351
+ </div>
3352
+ </div>
3353
+ {canStart ? (
3354
+ <button
3355
+ type="button"
3356
+ className="popover-list-action popover-list-action-primary"
3357
+ onClick={() => onStartNotes(meeting)}
3358
+ >
3359
+ Start notes
3360
+ </button>
3361
+ ) : (
3362
+ <button
3363
+ type="button"
3364
+ className="popover-list-action"
3365
+ onClick={() => onOpenMeeting(meeting.id)}
3366
+ >
3367
+ Open
3368
+ </button>
3369
+ )}
3370
+ </div>
3371
+ );
3372
+ })}
3373
+ </div>
3374
+ )}
3375
+
3376
+ {startMessage ? <p className="setup-success">{startMessage}</p> : null}
3377
+ </div>
3378
+ );
3379
+ }
3380
+
3381
+ function DictationPopoverView({
3382
+ voiceEnabled,
3383
+ voiceShortcut,
3384
+ voiceCustomShortcut,
3385
+ voiceMode,
3386
+ voiceProvider,
3387
+ onBack,
3388
+ onOpenDictate,
3389
+ onOpenSettings,
3390
+ }: {
3391
+ voiceEnabled: boolean;
3392
+ voiceShortcut: VoiceShortcutPreference;
3393
+ voiceCustomShortcut: string;
3394
+ voiceMode: VoiceMode;
3395
+ voiceProvider: VoiceProvider;
3396
+ onBack: () => void;
3397
+ onOpenDictate: () => void;
3398
+ onOpenSettings: () => void;
3399
+ }) {
3400
+ const shortcut = voiceShortcutLabel(voiceShortcut, voiceCustomShortcut);
3401
+ return (
3402
+ <div className="setup popover-view">
3403
+ <PopoverSubViewHeader
3404
+ title="Dictate"
3405
+ onBack={onBack}
3406
+ action={
3407
+ <button
3408
+ type="button"
3409
+ className="link-button popover-view-link"
3410
+ onClick={onOpenDictate}
3411
+ >
3412
+ Open web
3413
+ </button>
3414
+ }
3415
+ />
3416
+
3417
+ <div className="popover-empty-card">
3418
+ <div className="popover-card-heading">
3419
+ <IconMicrophone2 size={18} stroke={1.75} />
3420
+ <strong>
3421
+ {voiceEnabled ? "Ready to dictate" : "Dictation is off"}
3422
+ </strong>
3423
+ </div>
3424
+ {voiceEnabled ? (
3425
+ <>
3426
+ <p>
3427
+ {voiceMode === "toggle"
3428
+ ? "Press once to start, then press again to stop."
3429
+ : "Hold the shortcut while speaking; release to paste."}
3430
+ </p>
3431
+ <div className="popover-kv">
3432
+ <span>Shortcut</span>
3433
+ <strong>{shortcut}</strong>
3434
+ </div>
3435
+ <div className="popover-kv">
3436
+ <span>Provider</span>
3437
+ <strong>{voiceProviderLabel(voiceProvider)}</strong>
3438
+ </div>
3439
+ </>
3440
+ ) : (
3441
+ <p>Turn on voice dictation to speak to type anywhere on your Mac.</p>
3442
+ )}
3443
+ </div>
3444
+
3445
+ <div className="setup-button-row">
3446
+ <button type="button" className="secondary" onClick={onOpenDictate}>
3447
+ Open Dictate history
3448
+ </button>
3449
+ <button type="button" className="secondary" onClick={onOpenSettings}>
3450
+ Dictation settings
3451
+ </button>
3452
+ </div>
3453
+ </div>
3454
+ );
3455
+ }
3456
+
3129
3457
  function BottomButton({
3130
3458
  icon,
3131
3459
  label,
3132
3460
  badge,
3133
3461
  onClick,
3134
3462
  }: {
3135
- icon: "library" | "settings" | "recent";
3463
+ icon: "library" | "settings" | "meetings" | "dictation";
3136
3464
  label: string;
3137
3465
  badge?: string;
3138
3466
  onClick: () => void;
@@ -3144,8 +3472,10 @@ function BottomButton({
3144
3472
  <LibraryIcon />
3145
3473
  ) : icon === "settings" ? (
3146
3474
  <SettingsIcon />
3475
+ ) : icon === "meetings" ? (
3476
+ <IconCalendarEvent size={18} stroke={1.75} />
3147
3477
  ) : (
3148
- <ClockIcon />
3478
+ <IconMicrophone2 size={18} stroke={1.75} />
3149
3479
  )}
3150
3480
  {badge ? <span className="badge">{badge}</span> : null}
3151
3481
  </span>