@agent-native/core 0.120.0 → 0.120.2

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 (207) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/docs/content/extensions.mdx +23 -4
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +4 -0
  6. package/corpus/core/src/agent/run-manager.ts +36 -10
  7. package/corpus/core/src/agent/types.ts +2 -0
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
  9. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +16 -3
  10. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  11. package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
  12. package/corpus/core/src/client/use-chat-models.ts +45 -0
  13. package/corpus/core/src/deploy/build.ts +3 -0
  14. package/corpus/core/src/server/analytics.ts +164 -20
  15. package/corpus/core/src/server/auth.ts +17 -13
  16. package/corpus/core/src/server/better-auth-instance.ts +13 -1
  17. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  18. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  19. package/corpus/core/src/templates/chat/netlify.toml +1 -0
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  21. package/corpus/core/src/vite/client.ts +6 -0
  22. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
  23. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
  24. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
  25. package/corpus/templates/analytics/AGENTS.md +20 -8
  26. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
  27. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
  28. package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
  29. package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
  30. package/corpus/templates/analytics/actions/view-screen.ts +1 -1
  31. package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
  32. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  33. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  34. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +103 -502
  35. package/corpus/templates/analytics/app/global.css +30 -0
  36. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
  37. package/corpus/templates/analytics/app/i18n-data.ts +80 -0
  38. package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
  39. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -53
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +23 -25
  42. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  43. package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
  44. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  45. package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
  49. package/corpus/templates/analytics/netlify.toml +4 -0
  50. package/corpus/templates/analytics/server/db/schema.ts +4 -0
  51. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
  52. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
  53. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
  54. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
  55. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
  56. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  57. package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
  58. package/corpus/templates/assets/actions/generate-image.ts +28 -1
  59. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +47 -9
  60. package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
  61. package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
  62. package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
  63. package/corpus/templates/assets/netlify.toml +1 -0
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -8
  65. package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
  66. package/corpus/templates/brain/netlify.toml +1 -0
  67. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +43 -5
  68. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
  69. package/corpus/templates/calendar/netlify.toml +1 -0
  70. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  71. package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
  72. package/corpus/templates/chat/netlify.toml +1 -0
  73. package/corpus/templates/clips/app/components/library/library-layout.tsx +52 -6
  74. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
  75. package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  76. package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
  77. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
  78. package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
  79. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
  80. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
  81. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
  82. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
  83. package/corpus/templates/clips/desktop/src/styles.css +35 -3
  84. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
  85. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
  86. package/corpus/templates/clips/netlify.toml +1 -0
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +3 -7
  88. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
  89. package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  90. package/corpus/templates/content/netlify.toml +1 -0
  91. package/corpus/templates/design/app/components/layout/Sidebar.tsx +55 -17
  92. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  93. package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  94. package/corpus/templates/design/netlify.toml +1 -0
  95. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
  96. package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  97. package/corpus/templates/dispatch/netlify.toml +1 -0
  98. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
  99. package/corpus/templates/forms/AGENTS.md +11 -1
  100. package/corpus/templates/forms/README.md +24 -1
  101. package/corpus/templates/forms/actions/update-form.ts +12 -5
  102. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
  103. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +41 -45
  104. package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
  105. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
  106. package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
  107. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
  108. package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  109. package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
  110. package/corpus/templates/forms/netlify.toml +1 -0
  111. package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
  112. package/corpus/templates/forms/server/lib/integrations.ts +11 -2
  113. package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
  114. package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
  115. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  116. package/corpus/templates/forms/shared/conditional.ts +67 -0
  117. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +36 -5
  118. package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
  119. package/corpus/templates/macros/netlify.toml +1 -0
  120. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -9
  121. package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
  122. package/corpus/templates/mail/netlify.toml +1 -0
  123. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +38 -5
  124. package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
  125. package/corpus/templates/plan/netlify.toml +1 -0
  126. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +47 -4
  127. package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
  128. package/corpus/templates/slides/netlify.toml +1 -0
  129. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
  130. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
  131. package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
  132. package/corpus/templates/tasks/netlify.toml +1 -0
  133. package/dist/agent/production-agent.d.ts.map +1 -1
  134. package/dist/agent/production-agent.js +4 -0
  135. package/dist/agent/production-agent.js.map +1 -1
  136. package/dist/agent/run-manager.d.ts +9 -0
  137. package/dist/agent/run-manager.d.ts.map +1 -1
  138. package/dist/agent/run-manager.js +25 -10
  139. package/dist/agent/run-manager.js.map +1 -1
  140. package/dist/agent/types.d.ts +2 -2
  141. package/dist/agent/types.d.ts.map +1 -1
  142. package/dist/agent/types.js.map +1 -1
  143. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  144. package/dist/client/MultiTabAssistantChat.js +35 -0
  145. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  146. package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
  147. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  148. package/dist/client/extensions/ExtensionsListPage.js +5 -3
  149. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  150. package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
  151. package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
  152. package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
  153. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
  154. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  155. package/dist/client/settings/SettingsPanel.js +10 -1
  156. package/dist/client/settings/SettingsPanel.js.map +1 -1
  157. package/dist/client/use-chat-models.d.ts +1 -0
  158. package/dist/client/use-chat-models.d.ts.map +1 -1
  159. package/dist/client/use-chat-models.js +33 -0
  160. package/dist/client/use-chat-models.js.map +1 -1
  161. package/dist/collab/awareness.d.ts +2 -2
  162. package/dist/collab/awareness.d.ts.map +1 -1
  163. package/dist/deploy/build.js +1 -0
  164. package/dist/deploy/build.js.map +1 -1
  165. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  166. package/dist/observability/routes.d.ts +3 -3
  167. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  168. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  169. package/dist/server/analytics.d.ts +17 -4
  170. package/dist/server/analytics.d.ts.map +1 -1
  171. package/dist/server/analytics.js +122 -17
  172. package/dist/server/analytics.js.map +1 -1
  173. package/dist/server/auth.d.ts.map +1 -1
  174. package/dist/server/auth.js +5 -4
  175. package/dist/server/auth.js.map +1 -1
  176. package/dist/server/better-auth-instance.d.ts +8 -0
  177. package/dist/server/better-auth-instance.d.ts.map +1 -1
  178. package/dist/server/better-auth-instance.js +10 -1
  179. package/dist/server/better-auth-instance.js.map +1 -1
  180. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  181. package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
  182. package/dist/templates/chat/netlify.toml +1 -0
  183. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  184. package/dist/vite/client.d.ts.map +1 -1
  185. package/dist/vite/client.js +2 -0
  186. package/dist/vite/client.js.map +1 -1
  187. package/docs/content/extensions.mdx +23 -4
  188. package/package.json +1 -1
  189. package/src/agent/production-agent.ts +4 -0
  190. package/src/agent/run-manager.ts +36 -10
  191. package/src/agent/types.ts +2 -0
  192. package/src/client/MultiTabAssistantChat.tsx +45 -0
  193. package/src/client/extensions/ExtensionsListPage.tsx +16 -3
  194. package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  195. package/src/client/settings/SettingsPanel.tsx +10 -0
  196. package/src/client/use-chat-models.ts +45 -0
  197. package/src/deploy/build.ts +3 -0
  198. package/src/server/analytics.ts +164 -20
  199. package/src/server/auth.ts +17 -13
  200. package/src/server/better-auth-instance.ts +13 -1
  201. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  202. package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  203. package/src/templates/chat/netlify.toml +1 -0
  204. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  205. package/src/vite/client.ts +6 -0
  206. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
  207. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +0 -175
@@ -4,7 +4,6 @@ import {
4
4
  } from "@agent-native/core/client/agent-chat";
5
5
  import { appPath } from "@agent-native/core/client/api-path";
6
6
  import { DevDatabaseLink } from "@agent-native/core/client/db-admin";
7
- import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
8
7
  import { getBrowserTabId } from "@agent-native/core/client/hooks";
9
8
  import { useT } from "@agent-native/core/client/i18n";
10
9
  import {
@@ -241,9 +240,17 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
241
240
  icon: IconTrash,
242
241
  match: (p) => p.startsWith("/trash"),
243
242
  },
243
+ ];
244
+
245
+ const bottomNavItems: {
246
+ to: string;
247
+ label: string;
248
+ icon: React.ComponentType<{ className?: string }>;
249
+ match: (path: string) => boolean;
250
+ }[] = [
244
251
  {
245
252
  to: "/agent",
246
- label: t("navigation.agent"),
253
+ label: t("settings.agentTitle"),
247
254
  icon: IconHierarchy2,
248
255
  match: (p) => p.startsWith("/agent"),
249
256
  },
@@ -519,6 +526,49 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
519
526
  )}
520
527
  </div>
521
528
 
529
+ <div className="shrink-0">
530
+ {showCollapsedSidebar ? (
531
+ <nav className="flex flex-col items-center gap-1 px-2 py-1">
532
+ {bottomNavItems.map(({ to, label, icon: Icon, match }) => (
533
+ <Tooltip key={to}>
534
+ <TooltipTrigger asChild>
535
+ <NavLink
536
+ to={to}
537
+ aria-label={label}
538
+ className={cn(
539
+ "flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/60 hover:text-foreground",
540
+ match(location.pathname) &&
541
+ "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary",
542
+ )}
543
+ >
544
+ <Icon className="h-4 w-4" />
545
+ </NavLink>
546
+ </TooltipTrigger>
547
+ <TooltipContent side="right">{label}</TooltipContent>
548
+ </Tooltip>
549
+ ))}
550
+ </nav>
551
+ ) : (
552
+ <nav className="space-y-0.5 border-t border-border px-2 py-1">
553
+ {bottomNavItems.map(({ to, label, icon: Icon, match }) => (
554
+ <NavLink
555
+ key={to}
556
+ to={to}
557
+ className={cn(
558
+ "flex items-center gap-2 rounded px-2 py-1.5 text-xs",
559
+ match(location.pathname)
560
+ ? "bg-primary/10 font-medium text-primary"
561
+ : "text-foreground hover:bg-accent/60",
562
+ )}
563
+ >
564
+ <Icon className="h-4 w-4" />
565
+ <span className="flex-1 truncate">{label}</span>
566
+ </NavLink>
567
+ ))}
568
+ </nav>
569
+ )}
570
+ </div>
571
+
522
572
  {!showCollapsedSidebar && (
523
573
  <>
524
574
  <div className="shrink-0 space-y-1.5 px-2 py-1.5">
@@ -539,10 +589,6 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
539
589
  {(isMobile || !pageHasHeaderSearch) && <SearchBar />}
540
590
  </div>
541
591
 
542
- <div className="shrink-0 px-1 py-1">
543
- <ExtensionsSidebarSection />
544
- </div>
545
-
546
592
  <div className="shrink-0 space-y-2 px-3 py-2">
547
593
  <OrgSwitcher settingsPath="/settings/organization" />
548
594
  <DevDatabaseLink />
@@ -1,19 +1,5 @@
1
- import { ExtensionsListPage } from "@agent-native/core/client/extensions";
2
- import { useT } from "@agent-native/core/client/i18n";
3
-
4
- import { PageHeader } from "@/components/library/page-header";
1
+ import { Navigate } from "react-router";
5
2
 
6
3
  export default function ExtensionsRoute() {
7
- const t = useT();
8
-
9
- return (
10
- <>
11
- <PageHeader>
12
- <h1 className="text-base font-semibold tracking-tight truncate">
13
- {t("navigation.extensions")}
14
- </h1>
15
- </PageHeader>
16
- <ExtensionsListPage />
17
- </>
18
- );
4
+ return <Navigate to="/settings#extensions" replace />;
19
5
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Agent settings are clearly labeled Manage agent in the sidebar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Meeting recording meters now use thin, responsive audio bars, while dismissed reminders stay dismissed and frontmost Zoom detection stays quiet unless Auto mode is enabled.
@@ -88,7 +88,7 @@ export function sampleLevels(
88
88
  const levels: number[] = [];
89
89
  for (let i = 0; i < barCount; i++) {
90
90
  const idx = Math.min(usable - 1, Math.floor((i / barCount) * usable));
91
- levels.push(data[idx] / 255);
91
+ levels.push(Math.min(1, (data[idx] / 255) * 1.65));
92
92
  }
93
93
  return levels;
94
94
  }
@@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core";
3
3
  export interface DismissibleMeetingNotification {
4
4
  type: "calendar" | "adhoc";
5
5
  meetingId: string;
6
+ joinUrl?: string | null;
6
7
  platform?: string | null;
7
8
  scheduledStart?: string | null;
8
9
  scheduledEnd?: string | null;
@@ -21,6 +22,7 @@ export async function dismissMeetingNotification(
21
22
  await nativeInvoke("dismiss_meeting_notification", {
22
23
  meetingId: notification.meetingId,
23
24
  notificationType: notification.type,
25
+ joinUrl: notification.joinUrl ?? null,
24
26
  platform: notification.platform ?? null,
25
27
  scheduledStart: notification.scheduledStart ?? null,
26
28
  scheduledEnd: notification.scheduledEnd ?? null,
@@ -1139,7 +1139,7 @@ export function installDesktopVoiceDictation(
1139
1139
  * `stop()` works the same way as for the browser path.
1140
1140
  *
1141
1141
  * No `getUserMedia()` here — the audio engine handles the mic on the Rust
1142
- * side. The synthetic meter still drives the flow-bar's waveform.
1142
+ * side and emits the real level events consumed by the flow bar.
1143
1143
  */
1144
1144
  const startNative = async (
1145
1145
  cleanupProvider?: ServerVoiceProvider,
@@ -1191,7 +1191,6 @@ export function installDesktopVoiceDictation(
1191
1191
  };
1192
1192
  session = next;
1193
1193
  startInFlight = false;
1194
- startSyntheticMeter(next);
1195
1194
  try {
1196
1195
  // Bias the recognizer toward the user's learned vocabulary. Stage
1197
1196
  // the list via a separate command so meeting capture can pass mic
@@ -1299,7 +1298,6 @@ export function installDesktopVoiceDictation(
1299
1298
  };
1300
1299
  session = next;
1301
1300
  startInFlight = false;
1302
- startSyntheticMeter(next);
1303
1301
  if (stopRequestedBeforeReady) {
1304
1302
  stop();
1305
1303
  }
@@ -0,0 +1,86 @@
1
+ import { listen } from "@tauri-apps/api/event";
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ const BAR_SHAPES = [0.52, 1, 0.68];
5
+ const LEVEL_DECAY = 0.78;
6
+ const EVENT_ATTACK_DECAY = 0.52;
7
+
8
+ interface LiveAudioBarsProps {
9
+ className?: string;
10
+ compact?: boolean;
11
+ }
12
+
13
+ /** Small Granola-style meter shared by the compact and expanded overlays. */
14
+ export function LiveAudioBars({
15
+ className,
16
+ compact = false,
17
+ }: LiveAudioBarsProps) {
18
+ const [level, setLevel] = useState(0);
19
+ const levelRef = useRef(0);
20
+
21
+ useEffect(() => {
22
+ let stopped = false;
23
+ let unlisten: (() => void) | null = null;
24
+
25
+ const decayTimer = window.setInterval(() => {
26
+ const next = levelRef.current * LEVEL_DECAY;
27
+ levelRef.current = next < 0.01 ? 0 : next;
28
+ setLevel(levelRef.current);
29
+ }, 60);
30
+
31
+ listen<{ level?: number }>("voice:audio-level", (event) => {
32
+ const incoming = Number(event.payload?.level);
33
+ if (!Number.isFinite(incoming)) return;
34
+ const next = Math.max(
35
+ levelRef.current * EVENT_ATTACK_DECAY,
36
+ Math.max(0, Math.min(1, incoming)),
37
+ );
38
+ levelRef.current = next;
39
+ setLevel(next);
40
+ })
41
+ .then((cleanup) => {
42
+ if (stopped) {
43
+ cleanup();
44
+ } else {
45
+ unlisten = cleanup;
46
+ }
47
+ })
48
+ .catch(() => {});
49
+
50
+ return () => {
51
+ stopped = true;
52
+ window.clearInterval(decayTimer);
53
+ unlisten?.();
54
+ };
55
+ }, []);
56
+
57
+ // Peak levels from speech taps are often quiet even when speech is clear.
58
+ // A gentle curve keeps the meter responsive without turning background noise
59
+ // into a full-height signal.
60
+ const visualLevel = level > 0 ? Math.min(1, Math.pow(level, 0.52) * 1.08) : 0;
61
+ const rootClassName = [
62
+ "live-audio-bars",
63
+ compact ? "live-audio-bars-compact" : null,
64
+ className,
65
+ ]
66
+ .filter(Boolean)
67
+ .join(" ");
68
+
69
+ return (
70
+ <span className={rootClassName} aria-hidden="true">
71
+ {BAR_SHAPES.map((shape, index) => {
72
+ const idleHeight = index === 1 ? 0.2 : 0.14;
73
+ const height = Math.round(
74
+ (idleHeight + visualLevel * shape * 0.8) * 100,
75
+ );
76
+ return (
77
+ <span
78
+ className="live-audio-bar"
79
+ key={index}
80
+ style={{ height: `${Math.max(12, height)}%` }}
81
+ />
82
+ );
83
+ })}
84
+ </span>
85
+ );
86
+ }
@@ -39,6 +39,7 @@ interface TranscriptionStatusPayload {
39
39
 
40
40
  const SNOOZE_MS = 5 * 60_000;
41
41
  const FALLBACK_AUTO_HIDE_MS = 6 * 60_000;
42
+ const DISMISSAL_TOMBSTONE_MS = 30 * 60_000;
42
43
  // Card is up to 440px wide; the extra width leaves room for the drop shadow
43
44
  // (~32px each side) so it isn't clipped by the transparent window edges.
44
45
  const NOTIFICATION_WINDOW_WIDTH = 504;
@@ -128,6 +129,7 @@ export function MeetingNotification() {
128
129
  const [pending, setPending] = useState(false);
129
130
  const autoHideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
130
131
  const dataRef = useRef<NotificationData | null>(null);
132
+ const dismissedKeysRef = useRef(new Map<string, number>());
131
133
  // Real DOM hover only fires while this overlay window is key, which macOS
132
134
  // won't grant it without a click (`show_without_activation` never
133
135
  // activates). `polledHovered` mirrors the Rust-side global cursor poll
@@ -139,6 +141,21 @@ export function MeetingNotification() {
139
141
  const hovered = domHovered || polledHovered;
140
142
  const prevHoveredRef = useRef(false);
141
143
 
144
+ function notificationKey(payload: NotificationData): string {
145
+ return [payload.type, payload.meetingId, payload.scheduledStart ?? ""].join(
146
+ "|",
147
+ );
148
+ }
149
+
150
+ function isDismissed(payload: NotificationData): boolean {
151
+ const now = Date.now();
152
+ const dismissed = dismissedKeysRef.current;
153
+ for (const [key, expiresAt] of dismissed) {
154
+ if (expiresAt <= now) dismissed.delete(key);
155
+ }
156
+ return dismissed.has(notificationKey(payload));
157
+ }
158
+
142
159
  useEffect(() => {
143
160
  dataRef.current = data;
144
161
  }, [data]);
@@ -188,6 +205,7 @@ export function MeetingNotification() {
188
205
  payload: NotificationData,
189
206
  options?: { hydrated?: boolean },
190
207
  ) {
208
+ if (isDismissed(payload)) return;
191
209
  setData(payload);
192
210
  setError(null);
193
211
  setMenuOpen(false);
@@ -311,6 +329,12 @@ export function MeetingNotification() {
311
329
 
312
330
  function dismissNotification() {
313
331
  const current = dataRef.current;
332
+ if (current) {
333
+ dismissedKeysRef.current.set(
334
+ notificationKey(current),
335
+ Date.now() + DISMISSAL_TOMBSTONE_MS,
336
+ );
337
+ }
314
338
  hideNotification();
315
339
  if (current) {
316
340
  void dismissMeetingNotification(current);
@@ -17,6 +17,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
17
17
 
18
18
  import { isDirectPillClick, type ScreenPoint } from "../lib/pill-interaction";
19
19
  import { speakerFor } from "../lib/transcription-engine";
20
+ import { LiveAudioBars } from "./live-audio-bars";
20
21
  import { LiveTranscript, type FinalLine } from "./live-transcript";
21
22
  import { PillLogo } from "./pill-logo";
22
23
 
@@ -65,9 +66,6 @@ export function RecordingPill() {
65
66
  const tickRef = useRef<ReturnType<typeof setInterval> | null>(null);
66
67
  // Mic and system audio share one calm activity meter, matching Granola's
67
68
  // single indicator for the combined meeting capture.
68
- const levelRef = useRef(0);
69
- const canvasRef = useRef<HTMLCanvasElement | null>(null);
70
- const rafRef = useRef<number | null>(null);
71
69
  const stopFallbackRef = useRef<ReturnType<typeof setTimeout> | null>(null);
72
70
  const dragStartScreenPointRef = useRef<ScreenPoint | null>(null);
73
71
 
@@ -165,15 +163,6 @@ export function RecordingPill() {
165
163
  if (ev.payload?.detached) setExpanded(false);
166
164
  }),
167
165
  );
168
- trackListen(
169
- listen<{ level: number; source?: "mic" | "system" }>(
170
- "voice:audio-level",
171
- (ev) => {
172
- const lvl = Math.max(0, Math.min(1, ev.payload.level));
173
- levelRef.current = lvl;
174
- },
175
- ),
176
- );
177
166
  // Signal that all listeners are registered. app.tsx listens for this and
178
167
  // re-emits the pill context and transcript preload for a fresh window.
179
168
  emit("clips:pill-ready", {}).catch(() => {});
@@ -207,62 +196,6 @@ export function RecordingPill() {
207
196
  };
208
197
  }, [ctx.mode, paused]);
209
198
 
210
- // One combined "dancing bars" meter — a few discrete vertical bars instead
211
- // of separate mic and system waveforms.
212
- useEffect(() => {
213
- const canvas = canvasRef.current;
214
- if (!canvas) return;
215
- const N_BARS = 3;
216
- const dpr = window.devicePixelRatio || 1;
217
- const W = canvas.clientWidth;
218
- const H = canvas.clientHeight;
219
- canvas.width = W * dpr;
220
- canvas.height = H * dpr;
221
- const ctx2d = canvas.getContext("2d");
222
- if (!ctx2d) return;
223
- ctx2d.scale(dpr, dpr);
224
- const slot = W / N_BARS;
225
- const gap = Math.max(2, slot * 0.3);
226
- const barWidth = Math.max(3, slot - gap);
227
- const centerY = H / 2;
228
- const startMs = Date.now();
229
- let lastDrawMs = 0;
230
- const FRAME_INTERVAL_MS = 1000 / 20;
231
- const tick = () => {
232
- rafRef.current = requestAnimationFrame(tick);
233
- const nowMs = Date.now();
234
- if (nowMs - lastDrawMs < FRAME_INTERVAL_MS) return;
235
- lastDrawMs = nowMs;
236
- const t = (nowMs - startMs) % 1_000_000;
237
- const target = Math.min(1, levelRef.current);
238
- ctx2d.clearRect(0, 0, W, H);
239
- ctx2d.fillStyle = "rgba(132, 204, 22, 0.98)";
240
- ctx2d.shadowColor = "rgba(132, 204, 22, 0.48)";
241
- ctx2d.shadowBlur = 4;
242
- for (let i = 0; i < N_BARS; i += 1) {
243
- const phase = t * 0.005 + i * (Math.PI * 0.65);
244
- const barTarget = 0.2 + Math.sin(phase) * 0.42 * target + target * 0.38;
245
- const h = Math.max(4, Math.min(1, barTarget) * H * 0.92);
246
- const x = i * (barWidth + gap) + gap / 2;
247
- const y = centerY - h / 2;
248
- const radius = Math.min(barWidth / 2, 3);
249
- ctx2d.beginPath();
250
- if (typeof ctx2d.roundRect === "function") {
251
- ctx2d.roundRect(x, y, barWidth, h, radius);
252
- } else {
253
- ctx2d.rect(x, y, barWidth, h);
254
- }
255
- ctx2d.fill();
256
- }
257
- ctx2d.shadowBlur = 0;
258
- };
259
- tick();
260
- return () => {
261
- if (rafRef.current) cancelAnimationFrame(rafRef.current);
262
- rafRef.current = null;
263
- };
264
- }, [expanded]);
265
-
266
199
  // Let the compact chip land first, then reveal the live transcript once per
267
200
  // meeting. The delay keeps the indicator from feeling like a sudden panel.
268
201
  useEffect(() => {
@@ -380,7 +313,10 @@ export function RecordingPill() {
380
313
  onClick={!expanded && !detached ? handlePillMediaClick : undefined}
381
314
  >
382
315
  <PillLogo className="pill-logo" />
383
- <canvas ref={canvasRef} className="pill-wave-canvas" aria-hidden />
316
+ <LiveAudioBars
317
+ compact={!expanded && !detached}
318
+ className="pill-wave-meter"
319
+ />
384
320
  </div>
385
321
  <div className="pill-controls">
386
322
  <span className="pill-timer">
@@ -805,13 +805,45 @@ body[data-clips-route="recording-pill"] #root {
805
805
  fill: none;
806
806
  /* Matches the recording pill's mic waveform accent. */
807
807
  stroke: rgba(74, 222, 128, 0.95);
808
- stroke-width: 1.75;
808
+ stroke-width: 1.25;
809
809
  stroke-linecap: round;
810
810
  stroke-linejoin: round;
811
811
  /* Keep the line crisp regardless of the non-uniform x-stretch. */
812
812
  vector-effect: non-scaling-stroke;
813
813
  }
814
814
 
815
+ .live-audio-bars {
816
+ display: inline-flex;
817
+ width: 48px;
818
+ height: 26px;
819
+ align-items: center;
820
+ justify-content: center;
821
+ gap: 4px;
822
+ flex-shrink: 0;
823
+ }
824
+
825
+ .live-audio-bars-compact {
826
+ width: 22px;
827
+ height: 22px;
828
+ gap: 3px;
829
+ }
830
+
831
+ .live-audio-bar {
832
+ display: block;
833
+ width: 2px;
834
+ min-height: 3px;
835
+ border-radius: 999px;
836
+ background: rgba(132, 204, 22, 0.98);
837
+ box-shadow: 0 0 5px rgba(132, 204, 22, 0.35);
838
+ transition: height 70ms ease-out;
839
+ }
840
+
841
+ @media (prefers-reduced-motion: reduce) {
842
+ .live-audio-bar {
843
+ transition: none;
844
+ }
845
+ }
846
+
815
847
  /* ------------------------------------------------------------------------- */
816
848
  /* Primary CTA */
817
849
  /* ------------------------------------------------------------------------- */
@@ -5534,7 +5566,7 @@ body[data-clips-route="recording-pill"] #root {
5534
5566
  display: none;
5535
5567
  }
5536
5568
 
5537
- .pill-vertical .pill-wave-canvas {
5569
+ .pill-vertical .pill-wave-meter {
5538
5570
  height: 22px;
5539
5571
  width: 22px;
5540
5572
  }
@@ -5569,7 +5601,7 @@ body[data-clips-route="recording-pill"] #root {
5569
5601
  font-variant-numeric: tabular-nums;
5570
5602
  }
5571
5603
 
5572
- .pill-wave-canvas {
5604
+ .pill-wave-meter {
5573
5605
  height: 26px;
5574
5606
  width: 48px;
5575
5607
  flex-shrink: 0;
@@ -2,8 +2,9 @@
2
2
  //!
3
3
  //! Polls the frontmost macOS app every few seconds. When Zoom or Teams stays
4
4
  //! frontmost for a short dwell window, creates a meeting row via
5
- //! `create-meeting` and shows the same meeting-notification overlay used for
6
- //! calendar reminders with `type: "adhoc"`.
5
+ //! `create-meeting`, and silently starts transcription when the user has
6
+ //! explicitly selected Auto mode. Ask/manual mode stays quiet because a
7
+ //! frontmost Zoom window is not reliable evidence of an active call.
7
8
  //!
8
9
  //! Reuses `MeetingsWatcherState` session (server URL + cookie + auth token)
9
10
  //! so the popover only needs to push credentials once.
@@ -157,7 +158,7 @@ async fn tick_macos(
157
158
  let front = crate::util::frontmost_bundle_id();
158
159
  let matched = front.as_deref().and_then(match_vc_bundle);
159
160
 
160
- let Some((platform, title)) = matched else {
161
+ let Some((platform, _title)) = matched else {
161
162
  // VC app left front — clear session dedupe so a later re-focus can
162
163
  // fire again (cooldown still applies).
163
164
  clear_session_if_left(app, None);
@@ -173,9 +174,7 @@ async fn tick_macos(
173
174
  return Ok(());
174
175
  }
175
176
 
176
- if config.meeting_transcription_mode == MeetingTranscriptionMode::Manual
177
- && !config.show_meeting_widget_enabled
178
- {
177
+ if config.meeting_transcription_mode != MeetingTranscriptionMode::Auto {
179
178
  reset_dwell(app);
180
179
  return Ok(());
181
180
  }
@@ -250,44 +249,14 @@ async fn tick_macos(
250
249
  }
251
250
  };
252
251
 
253
- let auto_start = config.meeting_transcription_mode == MeetingTranscriptionMode::Auto;
254
- let show_widget = config.show_meeting_widget_enabled
255
- || config.meeting_transcription_mode == MeetingTranscriptionMode::Ask
256
- || auto_start;
257
-
258
- if show_widget {
259
- let app_clone = app.clone();
260
- let id_clone = meeting_id.clone();
261
- let title_clone = title.to_string();
262
- let platform_clone = platform.to_string();
263
- let scheduled_start = chrono::Utc::now().to_rfc3339();
264
- tauri::async_runtime::spawn(async move {
265
- let _ = crate::notifications::notify_meeting_starting(
266
- app_clone,
267
- id_clone,
268
- title_clone,
269
- 0,
270
- None,
271
- Some(scheduled_start),
272
- None,
273
- Some(platform_clone),
274
- Some(auto_start),
275
- Some("adhoc".to_string()),
276
- )
277
- .await;
278
- });
279
- }
280
-
281
- if auto_start {
282
- let _ = app.emit(
283
- "meetings:start-transcription",
284
- serde_json::json!({
285
- "meetingId": meeting_id,
286
- "joinUrl": null,
287
- "reason": "adhoc-auto",
288
- }),
289
- );
290
- }
252
+ let _ = app.emit(
253
+ "meetings:start-transcription",
254
+ serde_json::json!({
255
+ "meetingId": meeting_id,
256
+ "joinUrl": null,
257
+ "reason": "adhoc-auto",
258
+ }),
259
+ );
291
260
 
292
261
  Ok(())
293
262
  }