@agent-native/core 0.114.13 → 0.114.16

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 (163) hide show
  1. package/bin/agent-native.js +35 -14
  2. package/bin/launcher.js +34 -0
  3. package/corpus/README.md +2 -2
  4. package/corpus/core/CHANGELOG.md +21 -0
  5. package/corpus/core/bin/agent-native.js +35 -14
  6. package/corpus/core/bin/launcher.js +34 -0
  7. package/corpus/core/docs/content/deployment.mdx +1 -1
  8. package/corpus/core/docs/content/frames.mdx +1 -1
  9. package/corpus/core/docs/content/integrations.mdx +27 -27
  10. package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
  11. package/corpus/core/docs/content/workspace-management.mdx +1 -1
  12. package/corpus/core/package.json +2 -1
  13. package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
  14. package/corpus/core/src/client/AgentPanel.tsx +10 -4
  15. package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
  16. package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
  17. package/corpus/core/src/client/error-format.ts +2 -1
  18. package/corpus/core/src/client/index.ts +1 -0
  19. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
  20. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
  21. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
  22. package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
  23. package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
  24. package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
  25. package/corpus/core/src/localization/default-messages.ts +1 -7
  26. package/corpus/core/src/server/builder-browser.ts +4 -0
  27. package/corpus/core/src/server/builder-design-systems.ts +6 -1
  28. package/corpus/core/src/server/fusion-app.ts +5 -1
  29. package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
  30. package/corpus/core/src/shared/index.ts +1 -0
  31. package/corpus/templates/analytics/AGENTS.md +10 -1
  32. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
  33. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
  34. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
  35. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
  36. package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
  37. package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
  38. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
  39. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
  40. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
  41. package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
  42. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
  43. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
  44. package/corpus/templates/analytics/server/plugins/db.ts +13 -0
  45. package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +25 -8
  46. package/corpus/templates/clips/app/components/library/library-grid.tsx +118 -9
  47. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  48. package/corpus/templates/clips/app/components/library/recording-card.tsx +10 -8
  49. package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
  50. package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
  51. package/corpus/templates/clips/app/routes/_app.trash.tsx +16 -1
  52. package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
  55. package/corpus/templates/clips/desktop/src/app.tsx +90 -78
  56. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
  57. package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
  58. package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
  59. package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
  60. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  61. package/corpus/templates/clips/desktop/src/styles.css +80 -7
  62. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  64. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
  65. package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
  66. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
  67. package/corpus/templates/clips/learnings.defaults.md +1 -1
  68. package/corpus/templates/clips/server/plugins/db.ts +11 -0
  69. package/corpus/templates/clips/shared/builder-credits.ts +1 -1
  70. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +20 -38
  71. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +40 -1
  72. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
  73. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
  74. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
  75. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
  76. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  77. package/dist/agent/engine/builder-engine.js +4 -0
  78. package/dist/agent/engine/builder-engine.js.map +1 -1
  79. package/dist/client/AgentPanel.d.ts.map +1 -1
  80. package/dist/client/AgentPanel.js +10 -3
  81. package/dist/client/AgentPanel.js.map +1 -1
  82. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  83. package/dist/client/ConnectBuilderCard.js +5 -1
  84. package/dist/client/ConnectBuilderCard.js.map +1 -1
  85. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  86. package/dist/client/chat/markdown-renderer.js +3 -2
  87. package/dist/client/chat/markdown-renderer.js.map +1 -1
  88. package/dist/client/error-format.d.ts +1 -1
  89. package/dist/client/error-format.d.ts.map +1 -1
  90. package/dist/client/error-format.js +1 -1
  91. package/dist/client/error-format.js.map +1 -1
  92. package/dist/client/index.d.ts +1 -0
  93. package/dist/client/index.d.ts.map +1 -1
  94. package/dist/client/index.js +1 -0
  95. package/dist/client/index.js.map +1 -1
  96. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  97. package/dist/client/integrations/IntegrationsPanel.js +3 -4
  98. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  99. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  100. package/dist/client/resources/McpIntegrationDialog.js +21 -7
  101. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  102. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  103. package/dist/client/resources/mcp-integration-catalog.js +2 -2
  104. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  105. package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
  106. package/dist/client/settings/BackgroundAgentSection.js +5 -1
  107. package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
  108. package/dist/client/settings/SettingsPanel.js +1 -1
  109. package/dist/client/settings/SettingsPanel.js.map +1 -1
  110. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  111. package/dist/client/settings/useBuilderStatus.js +2 -0
  112. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  113. package/dist/collab/struct-routes.d.ts +1 -1
  114. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  115. package/dist/localization/default-messages.d.ts +0 -6
  116. package/dist/localization/default-messages.d.ts.map +1 -1
  117. package/dist/localization/default-messages.js +1 -7
  118. package/dist/localization/default-messages.js.map +1 -1
  119. package/dist/observability/routes.d.ts +5 -5
  120. package/dist/resources/handlers.d.ts +1 -1
  121. package/dist/secrets/routes.d.ts +9 -9
  122. package/dist/server/builder-browser.d.ts.map +1 -1
  123. package/dist/server/builder-browser.js +4 -0
  124. package/dist/server/builder-browser.js.map +1 -1
  125. package/dist/server/builder-design-systems.d.ts.map +1 -1
  126. package/dist/server/builder-design-systems.js +6 -1
  127. package/dist/server/builder-design-systems.js.map +1 -1
  128. package/dist/server/fusion-app.d.ts.map +1 -1
  129. package/dist/server/fusion-app.js +2 -1
  130. package/dist/server/fusion-app.js.map +1 -1
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/shared/builder-link-tracking.d.ts +14 -0
  133. package/dist/shared/builder-link-tracking.d.ts.map +1 -0
  134. package/dist/shared/builder-link-tracking.js +26 -0
  135. package/dist/shared/builder-link-tracking.js.map +1 -0
  136. package/dist/shared/index.d.ts +1 -0
  137. package/dist/shared/index.d.ts.map +1 -1
  138. package/dist/shared/index.js +1 -0
  139. package/dist/shared/index.js.map +1 -1
  140. package/docs/content/deployment.mdx +1 -1
  141. package/docs/content/frames.mdx +1 -1
  142. package/docs/content/integrations.mdx +27 -27
  143. package/docs/content/pr-visual-recap.mdx +17 -13
  144. package/docs/content/workspace-management.mdx +1 -1
  145. package/package.json +3 -2
  146. package/src/agent/engine/builder-engine.ts +4 -0
  147. package/src/client/AgentPanel.tsx +10 -4
  148. package/src/client/ConnectBuilderCard.tsx +5 -1
  149. package/src/client/chat/markdown-renderer.tsx +3 -5
  150. package/src/client/error-format.ts +2 -1
  151. package/src/client/index.ts +1 -0
  152. package/src/client/integrations/IntegrationsPanel.tsx +2 -18
  153. package/src/client/resources/McpIntegrationDialog.tsx +43 -32
  154. package/src/client/resources/mcp-integration-catalog.ts +4 -2
  155. package/src/client/settings/BackgroundAgentSection.tsx +5 -1
  156. package/src/client/settings/SettingsPanel.tsx +4 -4
  157. package/src/client/settings/useBuilderStatus.ts +3 -0
  158. package/src/localization/default-messages.ts +1 -7
  159. package/src/server/builder-browser.ts +4 -0
  160. package/src/server/builder-design-systems.ts +6 -1
  161. package/src/server/fusion-app.ts +5 -1
  162. package/src/shared/builder-link-tracking.ts +41 -0
  163. package/src/shared/index.ts +1 -0
@@ -46,6 +46,7 @@ export default function TrashRoute() {
46
46
  const t = useT();
47
47
  const [sort, setSort] = useState<SortKey>("recent");
48
48
  const [selected, setSelected] = useState<Set<string>>(new Set());
49
+ const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
49
50
  const [confirmPurge, setConfirmPurge] = useState(false);
50
51
  const [singlePurgeId, setSinglePurgeId] = useState<string | null>(null);
51
52
  const [isBulkPending, setIsBulkPending] = useState(false);
@@ -60,13 +61,26 @@ export default function TrashRoute() {
60
61
  "delete-recording-permanent",
61
62
  );
62
63
 
63
- const toggleSelect = (id: string) => {
64
+ const toggleSelect = (id: string, shiftKey = false) => {
64
65
  setSelected((prev) => {
66
+ if (shiftKey && lastSelectedId && lastSelectedId !== id) {
67
+ const ids = recordings.map((r) => r.id);
68
+ const fromIndex = ids.indexOf(lastSelectedId);
69
+ const toIndex = ids.indexOf(id);
70
+ if (fromIndex !== -1 && toIndex !== -1) {
71
+ const [start, end] =
72
+ fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
73
+ const next = new Set(prev);
74
+ for (let i = start; i <= end; i++) next.add(ids[i]);
75
+ return next;
76
+ }
77
+ }
65
78
  const next = new Set(prev);
66
79
  if (next.has(id)) next.delete(id);
67
80
  else next.add(id);
68
81
  return next;
69
82
  });
83
+ setLastSelectedId(id);
70
84
  };
71
85
 
72
86
  const restoreAll = async (ids: string[]) => {
@@ -170,6 +184,7 @@ export default function TrashRoute() {
170
184
  ? new Set()
171
185
  : new Set(recordings.map((r) => r.id)),
172
186
  );
187
+ setLastSelectedId(null);
173
188
  };
174
189
 
175
190
  return (
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ You can now choose which Whisper model to use for offline transcription in Settings — from Tiny (fast, small) to Large v3 Turbo (most accurate) — and delete previously downloaded models you no longer need.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ New recordings default to public link visibility across existing organizations, while admins can still choose a different default.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ Meeting notes now stop shortly after Zoom, Teams, or Meet calls end.
@@ -51,6 +51,7 @@ import { UpdateBanner } from "./components/UpdateBanner";
51
51
  import { useMediaDevices } from "./hooks/useMediaDevices";
52
52
  import { useMeetingTranscription } from "./hooks/useMeetingTranscription";
53
53
  import { stopAllMicMeters } from "./hooks/useMicMeter";
54
+ import { useWhisperSettings } from "./hooks/useWhisperSettings";
54
55
  import { startBubbleFramePump } from "./lib/bubble-pump";
55
56
  import {
56
57
  startBubbleWebrtc,
@@ -571,6 +572,21 @@ function openPrivacySettings(pane: MacosPrivacyPane): void {
571
572
  }
572
573
  }
573
574
 
575
+ // Same explicit-drag pattern the toolbar/bubble overlays use —
576
+ // `data-tauri-drag-region` has been unreliable, so we call `startDragging()`
577
+ // directly on mousedown. Clicks on buttons/inputs still reach their handlers
578
+ // since we only start a drag when the mousedown target isn't inside one.
579
+ function handlePopoverHeaderMouseDown(event: React.MouseEvent) {
580
+ if (event.button !== 0) return;
581
+ const target = event.target as HTMLElement;
582
+ if (target.closest("button, a, input, select, textarea")) return;
583
+ getCurrentWindow()
584
+ .startDragging()
585
+ .catch((err) => {
586
+ console.warn("[clips-popover] startDragging failed:", err);
587
+ });
588
+ }
589
+
574
590
  function nativeVoiceProvider(): VoiceProvider {
575
591
  return isMacPlatform() ? "macos-native" : "browser";
576
592
  }
@@ -4247,7 +4263,10 @@ function Header({
4247
4263
  // close button lives top-right as an absolute-positioned sibling, so the
4248
4264
  // tabs aren't offset by the close button's width.
4249
4265
  return (
4250
- <div className="header header-centered">
4266
+ <div
4267
+ className="header header-centered"
4268
+ onMouseDown={handlePopoverHeaderMouseDown}
4269
+ >
4251
4270
  <FeedbackButton submitterEmail={submitterEmail} />
4252
4271
  <div
4253
4272
  className="mode-toggle"
@@ -4443,7 +4462,10 @@ function PopoverSubViewHeader({
4443
4462
  action?: ReactNode;
4444
4463
  }) {
4445
4464
  return (
4446
- <div className="setup-header popover-view-header">
4465
+ <div
4466
+ className="setup-header popover-view-header"
4467
+ onMouseDown={handlePopoverHeaderMouseDown}
4468
+ >
4447
4469
  <button
4448
4470
  type="button"
4449
4471
  className="setup-back"
@@ -4940,17 +4962,20 @@ function Setup({
4940
4962
  featureConfig?.meetingTranscriptionMode ?? "ask";
4941
4963
  const showMeetingWidgetEnabled =
4942
4964
  featureConfig?.showMeetingWidgetEnabled !== false;
4943
- const whisperModelEnabled = featureConfig?.whisperModelEnabled !== false;
4944
- type WhisperModelState = "disabled" | "missing" | "downloading" | "ready";
4945
- interface WhisperModelStatus {
4946
- state: WhisperModelState;
4947
- path: string;
4948
- downloadedMb: number;
4949
- totalMb: number;
4950
- }
4951
- const [whisperStatus, setWhisperStatus] = useState<WhisperModelStatus | null>(
4952
- null,
4965
+ const whisper = useWhisperSettings(
4966
+ featureConfig,
4967
+ voiceProvider,
4968
+ onVoiceProviderChange,
4969
+ nativeVoiceProvider,
4953
4970
  );
4971
+ const {
4972
+ catalog: whisperModels,
4973
+ status: whisperStatus,
4974
+ enabled: whisperModelEnabled,
4975
+ modelId: whisperModelId,
4976
+ selectedModel: selectedWhisperModel,
4977
+ deletableModels,
4978
+ } = whisper;
4954
4979
  const [screenMemoryStatus, setScreenMemoryStatus] =
4955
4980
  useState<ScreenMemoryStatus | null>(null);
4956
4981
  const screenMemoryStatusRefreshVersionRef = useRef(0);
@@ -5059,24 +5084,6 @@ function Setup({
5059
5084
  );
5060
5085
  }
5061
5086
 
5062
- function triggerWhisperDownload() {
5063
- invoke("whisper_model_download").catch(() => {});
5064
- }
5065
-
5066
- function setWhisperModelEnabled(enabled: boolean) {
5067
- if (!featureConfig) return;
5068
- invoke("set_feature_config", {
5069
- config: { ...featureConfig, whisperModelEnabled: enabled },
5070
- }).catch((err) =>
5071
- console.error("[settings] set_feature_config failed", err),
5072
- );
5073
- if (enabled) {
5074
- triggerWhisperDownload();
5075
- } else if (voiceProvider === "whisper") {
5076
- onVoiceProviderChange(nativeVoiceProvider());
5077
- }
5078
- }
5079
-
5080
5087
  function setLaunchAtLoginEnabled(enabled: boolean) {
5081
5088
  if (!featureConfig) return;
5082
5089
  invoke("set_feature_config", {
@@ -5480,47 +5487,6 @@ function Setup({
5480
5487
  };
5481
5488
  }, [refreshScreenMemoryStatus]);
5482
5489
 
5483
- // Load model status on mount and keep it current via events.
5484
- useEffect(() => {
5485
- let cancelled = false;
5486
- const refresh = () => {
5487
- invoke<WhisperModelStatus>("whisper_model_status")
5488
- .then((s) => {
5489
- if (!cancelled) setWhisperStatus(s);
5490
- })
5491
- .catch(() => {});
5492
- };
5493
- refresh();
5494
- const unlistens: Array<() => void> = [];
5495
- const track = (p: Promise<() => void>) => {
5496
- p.then((u) => {
5497
- if (cancelled) {
5498
- try {
5499
- u();
5500
- } catch {
5501
- /* ignore */
5502
- }
5503
- return;
5504
- }
5505
- unlistens.push(u);
5506
- }).catch(() => {});
5507
- };
5508
- track(listen("whisper:model-progress", () => refresh()));
5509
- track(listen("whisper:model-ready", () => refresh()));
5510
- track(listen("whisper:model-error", () => refresh()));
5511
- track(listen("whisper:model-enabled-changed", () => refresh()));
5512
- return () => {
5513
- cancelled = true;
5514
- unlistens.forEach((u) => {
5515
- try {
5516
- u();
5517
- } catch {
5518
- /* ignore */
5519
- }
5520
- });
5521
- };
5522
- }, []);
5523
-
5524
5490
  useEffect(() => {
5525
5491
  const base = (serverUrl ?? initial ?? DEFAULT_URL).replace(/\/+$/, "");
5526
5492
  let cancelled = false;
@@ -5606,7 +5572,7 @@ function Setup({
5606
5572
  onVoiceProviderChange(nativeVoiceProvider());
5607
5573
  } else if (mode === "whisper") {
5608
5574
  onVoiceProviderChange("whisper");
5609
- if (!whisperModelEnabled) setWhisperModelEnabled(true);
5575
+ if (!whisperModelEnabled) whisper.setEnabled(true);
5610
5576
  } else if (mode === "builder") {
5611
5577
  onVoiceProviderChange("builder-gemini");
5612
5578
  } else {
@@ -6431,7 +6397,7 @@ function Setup({
6431
6397
 
6432
6398
  return (
6433
6399
  <div className="setup">
6434
- <div className="setup-header">
6400
+ <div className="setup-header" onMouseDown={handlePopoverHeaderMouseDown}>
6435
6401
  {onCancel ? (
6436
6402
  <button
6437
6403
  type="button"
@@ -7192,15 +7158,58 @@ function Setup({
7192
7158
  />
7193
7159
  <Switch
7194
7160
  on={whisperModelEnabled}
7195
- onChange={setWhisperModelEnabled}
7161
+ onChange={whisper.setEnabled}
7196
7162
  label="Enable Whisper model"
7197
7163
  />
7198
7164
  </div>
7165
+ <SettingLabel
7166
+ label="Model"
7167
+ hint="Larger models can improve transcription accuracy but use more storage and may run more slowly."
7168
+ htmlFor="whisper-model"
7169
+ />
7170
+ <select
7171
+ id="whisper-model"
7172
+ className="setup-select"
7173
+ value={whisperModelId}
7174
+ onChange={(event) => whisper.setModelId(event.target.value)}
7175
+ disabled={
7176
+ whisperModels.length === 0 || whisperStatus?.state === "downloading"
7177
+ }
7178
+ >
7179
+ {whisperModels.map((model) => (
7180
+ <option key={model.id} value={model.id}>
7181
+ {model.title} · {model.sizeMb} MB — {model.description}
7182
+ </option>
7183
+ ))}
7184
+ </select>
7185
+ {selectedWhisperModel ? (
7186
+ <p className="setup-hint">{selectedWhisperModel.description}</p>
7187
+ ) : null}
7199
7188
  <WhisperModelStatusRow
7200
7189
  status={whisperStatus}
7201
7190
  enabled={whisperModelEnabled}
7202
- onDownload={triggerWhisperDownload}
7191
+ onDownload={whisper.triggerDownload}
7203
7192
  />
7193
+ {deletableModels.length > 0 ? (
7194
+ <div className="whisper-other-models">
7195
+ <p className="setup-hint">Other downloaded models</p>
7196
+ {deletableModels.map((model) => (
7197
+ <div key={model.id} className="whisper-other-model-row">
7198
+ <span className="whisper-other-model-name">
7199
+ {model.title} &middot; {model.sizeMb} MB
7200
+ </span>
7201
+ <button
7202
+ type="button"
7203
+ className="whisper-delete-btn"
7204
+ onClick={() => whisper.deleteModel(model.id)}
7205
+ >
7206
+ <IconTrash size={13} />
7207
+ Delete
7208
+ </button>
7209
+ </div>
7210
+ ))}
7211
+ </div>
7212
+ ) : null}
7204
7213
  </div>
7205
7214
 
7206
7215
  <div className="setup-section-heading">Dictation</div>
@@ -7528,9 +7537,12 @@ function WhisperModelStatusRow({
7528
7537
  <span className="whisper-progress-label">
7529
7538
  Downloading… {status.downloadedMb} / {status.totalMb} MB ({pct}%)
7530
7539
  </span>
7531
- <div className="whisper-progress-bar">
7532
- <div className="whisper-progress-fill" style={{ width: `${pct}%` }} />
7533
- </div>
7540
+ <progress
7541
+ className="whisper-progress-bar"
7542
+ value={pct}
7543
+ max={100}
7544
+ aria-label={`Whisper model download ${pct}% complete`}
7545
+ />
7534
7546
  </div>
7535
7547
  );
7536
7548
  }
@@ -3,6 +3,7 @@ import { emit, listen } from "@tauri-apps/api/event";
3
3
  import { open as openExternal } from "@tauri-apps/plugin-shell";
4
4
  import { useCallback, useEffect, useMemo, useRef } from "react";
5
5
 
6
+ import { callAppBundleIdsForJoinUrl } from "../lib/meeting-call-app";
6
7
  import {
7
8
  appendFinalTranscript,
8
9
  onFinalTranscript,
@@ -414,6 +415,7 @@ export function useMeetingTranscription({
414
415
  silenceThreshold: 0.05,
415
416
  silenceMs: 15 * 60 * 1000,
416
417
  callEndedMs: 2 * 60 * 1000,
418
+ callAppBundleIds: callAppBundleIdsForJoinUrl(payload.joinUrl),
417
419
  scheduledEndMs,
418
420
  watchSleep: true,
419
421
  watchCallEnded: true,
@@ -0,0 +1,172 @@
1
+ import { invoke } from "@tauri-apps/api/core";
2
+ import { listen } from "@tauri-apps/api/event";
3
+ import { useCallback, useEffect, useState } from "react";
4
+
5
+ import type { VoiceProvider } from "../lib/voice-dictation";
6
+ import type { FeatureConfig } from "../shared/config";
7
+
8
+ export interface WhisperModelOption {
9
+ id: string;
10
+ title: string;
11
+ description: string;
12
+ url: string;
13
+ filename: string;
14
+ sha256: string;
15
+ sizeBytes: number;
16
+ sizeMb: number;
17
+ }
18
+
19
+ export type WhisperModelState =
20
+ | "disabled"
21
+ | "missing"
22
+ | "downloading"
23
+ | "ready";
24
+
25
+ export interface WhisperModelStatus {
26
+ state: WhisperModelState;
27
+ path: string;
28
+ downloadedMb: number;
29
+ totalMb: number;
30
+ }
31
+
32
+ const WHISPER_STATUS_EVENTS = [
33
+ "whisper:model-progress",
34
+ "whisper:model-error",
35
+ "whisper:model-enabled-changed",
36
+ "whisper:model-selection-changed",
37
+ ] as const;
38
+
39
+ export function useWhisperSettings(
40
+ featureConfig: FeatureConfig | null,
41
+ voiceProvider: VoiceProvider,
42
+ onVoiceProviderChange: (provider: VoiceProvider) => void,
43
+ nativeVoiceProvider: () => VoiceProvider,
44
+ ) {
45
+ const [catalog, setCatalog] = useState<WhisperModelOption[]>([]);
46
+ const [downloadedModelIds, setDownloadedModelIds] = useState<string[]>([]);
47
+ const [status, setStatus] = useState<WhisperModelStatus | null>(null);
48
+
49
+ const enabled = featureConfig?.whisperModelEnabled !== false;
50
+ const modelId = featureConfig?.whisperModelId ?? "base";
51
+ const selectedModel = catalog.find((m) => m.id === modelId) ?? null;
52
+ const deletableModels = catalog.filter(
53
+ (m) => m.id !== modelId && downloadedModelIds.includes(m.id),
54
+ );
55
+
56
+ useEffect(() => {
57
+ invoke<WhisperModelOption[]>("whisper_models")
58
+ .then(setCatalog)
59
+ .catch(() => {});
60
+ }, []);
61
+
62
+ const refreshDownloaded = useCallback(() => {
63
+ invoke<string[]>("whisper_downloaded_models")
64
+ .then(setDownloadedModelIds)
65
+ .catch(() => {});
66
+ }, []);
67
+
68
+ useEffect(() => {
69
+ let cancelled = false;
70
+ const refreshStatus = () => {
71
+ invoke<WhisperModelStatus>("whisper_model_status")
72
+ .then((s) => {
73
+ if (!cancelled) setStatus(s);
74
+ })
75
+ .catch(() => {});
76
+ };
77
+
78
+ refreshStatus();
79
+ refreshDownloaded();
80
+
81
+ const unlistens: Array<() => void> = [];
82
+ const track = (p: Promise<() => void>) => {
83
+ p.then((u) => {
84
+ if (cancelled) {
85
+ try {
86
+ u();
87
+ } catch {
88
+ /* ignore */
89
+ }
90
+ return;
91
+ }
92
+ unlistens.push(u);
93
+ }).catch(() => {});
94
+ };
95
+
96
+ for (const event of WHISPER_STATUS_EVENTS) {
97
+ track(listen(event, () => refreshStatus()));
98
+ }
99
+ track(
100
+ listen("whisper:model-ready", () => {
101
+ refreshStatus();
102
+ refreshDownloaded();
103
+ }),
104
+ );
105
+ track(listen("whisper:model-deleted", () => refreshDownloaded()));
106
+
107
+ return () => {
108
+ cancelled = true;
109
+ unlistens.forEach((u) => {
110
+ try {
111
+ u();
112
+ } catch {
113
+ /* ignore */
114
+ }
115
+ });
116
+ };
117
+ }, [refreshDownloaded]);
118
+
119
+ function triggerDownload() {
120
+ invoke("whisper_model_download").catch(() => {});
121
+ }
122
+
123
+ function setEnabled(next: boolean) {
124
+ if (!featureConfig) return;
125
+ invoke("set_feature_config", {
126
+ config: { ...featureConfig, whisperModelEnabled: next },
127
+ })
128
+ .then(() => {
129
+ if (next) {
130
+ triggerDownload();
131
+ } else if (voiceProvider === "whisper") {
132
+ onVoiceProviderChange(nativeVoiceProvider());
133
+ }
134
+ })
135
+ .catch((err) =>
136
+ console.error("[whisper] set_feature_config failed", err),
137
+ );
138
+ }
139
+
140
+ function setModelId(next: string) {
141
+ if (!featureConfig) return;
142
+ invoke("set_feature_config", {
143
+ config: { ...featureConfig, whisperModelId: next },
144
+ })
145
+ .then(() => {
146
+ if (enabled) triggerDownload();
147
+ })
148
+ .catch((err) =>
149
+ console.error("[whisper] set_feature_config failed", err),
150
+ );
151
+ }
152
+
153
+ function deleteModel(modelIdToDelete: string) {
154
+ invoke("whisper_model_delete", { modelId: modelIdToDelete })
155
+ .then(() => refreshDownloaded())
156
+ .catch(() => {});
157
+ }
158
+
159
+ return {
160
+ catalog,
161
+ downloadedModelIds,
162
+ status,
163
+ enabled,
164
+ modelId,
165
+ selectedModel,
166
+ deletableModels,
167
+ triggerDownload,
168
+ setEnabled,
169
+ setModelId,
170
+ deleteModel,
171
+ };
172
+ }
@@ -0,0 +1,41 @@
1
+ const NATIVE_CALL_APP_BUNDLE_IDS = [
2
+ "us.zoom.xos",
3
+ "us.zoom.ZoomClips",
4
+ "com.microsoft.teams2",
5
+ "com.microsoft.teams",
6
+ ] as const;
7
+
8
+ const BROWSER_CALL_APP_BUNDLE_IDS = [
9
+ "com.google.Chrome",
10
+ "company.thebrowser.Browser",
11
+ "com.apple.Safari",
12
+ "org.mozilla.firefox",
13
+ ] as const;
14
+
15
+ function isHost(hostname: string, host: string): boolean {
16
+ return hostname === host || hostname.endsWith(`.${host}`);
17
+ }
18
+
19
+ /**
20
+ * Limit microphone-use monitoring to the app that can own this meeting.
21
+ * Browser bundles are included only when the calendar join URL identifies a
22
+ * browser-hosted provider, so another tab using the microphone cannot end a
23
+ * native Zoom or Teams recording.
24
+ */
25
+ export function callAppBundleIdsForJoinUrl(joinUrl?: string | null): string[] {
26
+ if (!joinUrl) return [...NATIVE_CALL_APP_BUNDLE_IDS];
27
+
28
+ try {
29
+ const hostname = new URL(joinUrl).hostname.toLowerCase();
30
+ if (isHost(hostname, "meet.google.com")) {
31
+ return [...BROWSER_CALL_APP_BUNDLE_IDS];
32
+ }
33
+ if (isHost(hostname, "teams.microsoft.com")) {
34
+ return [...NATIVE_CALL_APP_BUNDLE_IDS, ...BROWSER_CALL_APP_BUNDLE_IDS];
35
+ }
36
+ } catch {
37
+ // Native Zoom and Teams remain the safe fallback for malformed URLs.
38
+ }
39
+
40
+ return [...NATIVE_CALL_APP_BUNDLE_IDS];
41
+ }
@@ -23,6 +23,7 @@ export interface SilenceConfig {
23
23
  silenceThreshold?: number;
24
24
  silenceMs?: number;
25
25
  callEndedMs?: number;
26
+ callAppBundleIds?: string[];
26
27
  scheduledEndMs?: number | null;
27
28
  watchSleep?: boolean;
28
29
  watchCallEnded?: boolean;
@@ -130,6 +130,7 @@ export interface FeatureConfig {
130
130
  screenMemory: ScreenMemoryConfig;
131
131
  onboardingComplete: boolean;
132
132
  whisperModelEnabled: boolean;
133
+ whisperModelId: string;
133
134
  }
134
135
 
135
136
  export function useFeatureConfig() {
@@ -5600,23 +5600,39 @@ body[data-clips-route="recording-pill"] #root {
5600
5600
  }
5601
5601
 
5602
5602
  .whisper-progress-bar {
5603
- height: 4px;
5604
5603
  width: 100%;
5604
+ height: 4px;
5605
+ margin-top: 4px;
5606
+ border: none;
5605
5607
  border-radius: 999px;
5606
- background: var(--surface-strong);
5608
+ appearance: none;
5607
5609
  overflow: hidden;
5608
- margin-top: 4px;
5610
+ background: var(--surface-strong);
5611
+ color: #16a34a;
5609
5612
  }
5610
5613
 
5611
- .whisper-progress-fill {
5612
- height: 100%;
5614
+ .whisper-progress-bar::-webkit-progress-bar {
5615
+ border-radius: inherit;
5616
+ background: var(--surface-strong);
5617
+ }
5618
+
5619
+ .whisper-progress-bar::-webkit-progress-value {
5620
+ border-radius: inherit;
5621
+ background: #16a34a;
5622
+ }
5623
+
5624
+ .whisper-progress-bar::-moz-progress-bar {
5613
5625
  border-radius: inherit;
5614
5626
  background: #16a34a;
5615
- transition: width 300ms ease;
5616
5627
  }
5617
5628
 
5618
5629
  @media (prefers-color-scheme: dark) {
5619
- .whisper-progress-fill {
5630
+ .whisper-progress-bar {
5631
+ color: #4ade80;
5632
+ }
5633
+
5634
+ .whisper-progress-bar::-webkit-progress-value,
5635
+ .whisper-progress-bar::-moz-progress-bar {
5620
5636
  background: #4ade80;
5621
5637
  }
5622
5638
  }
@@ -5644,6 +5660,63 @@ body[data-clips-route="recording-pill"] #root {
5644
5660
  background: var(--surface-hover);
5645
5661
  }
5646
5662
 
5663
+ .whisper-other-models {
5664
+ display: flex;
5665
+ flex-direction: column;
5666
+ gap: 4px;
5667
+ margin-top: 6px;
5668
+ }
5669
+
5670
+ .whisper-other-model-row {
5671
+ display: flex;
5672
+ align-items: center;
5673
+ justify-content: space-between;
5674
+ gap: 8px;
5675
+ padding: 5px 8px;
5676
+ border: 1px solid var(--border);
5677
+ border-radius: var(--radius-sm);
5678
+ background: var(--surface);
5679
+ }
5680
+
5681
+ .whisper-other-model-name {
5682
+ font-size: 11px;
5683
+ color: var(--fg-muted);
5684
+ }
5685
+
5686
+ .whisper-delete-btn {
5687
+ display: inline-flex;
5688
+ align-items: center;
5689
+ gap: 4px;
5690
+ flex-shrink: 0;
5691
+ height: 22px;
5692
+ padding: 0 8px;
5693
+ border: 1px solid var(--border-strong);
5694
+ border-radius: var(--radius-sm);
5695
+ background: var(--surface);
5696
+ color: var(--fg-muted);
5697
+ cursor: pointer;
5698
+ font-size: 11px;
5699
+ font-weight: 600;
5700
+ transition:
5701
+ background 120ms,
5702
+ color 120ms,
5703
+ border-color 120ms;
5704
+ }
5705
+
5706
+ .whisper-delete-btn:hover {
5707
+ background: rgba(239, 68, 68, 0.1);
5708
+ border-color: rgba(239, 68, 68, 0.4);
5709
+ color: #ef4444;
5710
+ }
5711
+
5712
+ @media (prefers-color-scheme: dark) {
5713
+ .whisper-delete-btn:hover {
5714
+ background: rgba(239, 68, 68, 0.15);
5715
+ border-color: rgba(239, 68, 68, 0.5);
5716
+ color: #f87171;
5717
+ }
5718
+ }
5719
+
5647
5720
  /* ---- */
5648
5721
 
5649
5722
  .pill-error {