@agent-native/core 0.114.13 → 0.114.15

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 (151) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/deployment.mdx +1 -1
  4. package/corpus/core/docs/content/frames.mdx +1 -1
  5. package/corpus/core/docs/content/integrations.mdx +27 -27
  6. package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
  7. package/corpus/core/docs/content/workspace-management.mdx +1 -1
  8. package/corpus/core/package.json +2 -1
  9. package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
  10. package/corpus/core/src/client/AgentPanel.tsx +10 -4
  11. package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
  12. package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
  13. package/corpus/core/src/client/error-format.ts +2 -1
  14. package/corpus/core/src/client/index.ts +1 -0
  15. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
  16. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
  17. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
  18. package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
  20. package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
  21. package/corpus/core/src/localization/default-messages.ts +1 -7
  22. package/corpus/core/src/server/builder-browser.ts +4 -0
  23. package/corpus/core/src/server/builder-design-systems.ts +6 -1
  24. package/corpus/core/src/server/fusion-app.ts +5 -1
  25. package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
  26. package/corpus/core/src/shared/index.ts +1 -0
  27. package/corpus/templates/analytics/AGENTS.md +10 -1
  28. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
  29. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
  30. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
  31. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
  32. package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
  34. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
  35. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
  36. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
  38. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
  39. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
  40. package/corpus/templates/analytics/server/plugins/db.ts +13 -0
  41. package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
  42. package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
  45. package/corpus/templates/clips/desktop/src/app.tsx +66 -75
  46. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
  47. package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
  48. package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
  49. package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
  50. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  51. package/corpus/templates/clips/desktop/src/styles.css +80 -7
  52. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
  53. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  54. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
  55. package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
  56. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
  57. package/corpus/templates/clips/learnings.defaults.md +1 -1
  58. package/corpus/templates/clips/server/plugins/db.ts +11 -0
  59. package/corpus/templates/clips/shared/builder-credits.ts +1 -1
  60. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
  61. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
  62. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
  63. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
  64. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  65. package/dist/agent/engine/builder-engine.js +4 -0
  66. package/dist/agent/engine/builder-engine.js.map +1 -1
  67. package/dist/client/AgentPanel.d.ts.map +1 -1
  68. package/dist/client/AgentPanel.js +10 -3
  69. package/dist/client/AgentPanel.js.map +1 -1
  70. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  71. package/dist/client/ConnectBuilderCard.js +5 -1
  72. package/dist/client/ConnectBuilderCard.js.map +1 -1
  73. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  74. package/dist/client/chat/markdown-renderer.js +3 -2
  75. package/dist/client/chat/markdown-renderer.js.map +1 -1
  76. package/dist/client/error-format.d.ts +1 -1
  77. package/dist/client/error-format.d.ts.map +1 -1
  78. package/dist/client/error-format.js +1 -1
  79. package/dist/client/error-format.js.map +1 -1
  80. package/dist/client/index.d.ts +1 -0
  81. package/dist/client/index.d.ts.map +1 -1
  82. package/dist/client/index.js +1 -0
  83. package/dist/client/index.js.map +1 -1
  84. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  85. package/dist/client/integrations/IntegrationsPanel.js +3 -4
  86. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  87. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  88. package/dist/client/resources/McpIntegrationDialog.js +21 -7
  89. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  90. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  91. package/dist/client/resources/mcp-integration-catalog.js +2 -2
  92. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  93. package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
  94. package/dist/client/settings/BackgroundAgentSection.js +5 -1
  95. package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
  96. package/dist/client/settings/SettingsPanel.js +1 -1
  97. package/dist/client/settings/SettingsPanel.js.map +1 -1
  98. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  99. package/dist/client/settings/useBuilderStatus.js +2 -0
  100. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  101. package/dist/collab/struct-routes.d.ts +1 -1
  102. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  103. package/dist/localization/default-messages.d.ts +0 -6
  104. package/dist/localization/default-messages.d.ts.map +1 -1
  105. package/dist/localization/default-messages.js +1 -7
  106. package/dist/localization/default-messages.js.map +1 -1
  107. package/dist/notifications/routes.d.ts +3 -3
  108. package/dist/observability/routes.d.ts +1 -1
  109. package/dist/resources/handlers.d.ts +1 -1
  110. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  111. package/dist/server/builder-browser.d.ts.map +1 -1
  112. package/dist/server/builder-browser.js +4 -0
  113. package/dist/server/builder-browser.js.map +1 -1
  114. package/dist/server/builder-design-systems.d.ts.map +1 -1
  115. package/dist/server/builder-design-systems.js +6 -1
  116. package/dist/server/builder-design-systems.js.map +1 -1
  117. package/dist/server/fusion-app.d.ts.map +1 -1
  118. package/dist/server/fusion-app.js +2 -1
  119. package/dist/server/fusion-app.js.map +1 -1
  120. package/dist/shared/builder-link-tracking.d.ts +14 -0
  121. package/dist/shared/builder-link-tracking.d.ts.map +1 -0
  122. package/dist/shared/builder-link-tracking.js +26 -0
  123. package/dist/shared/builder-link-tracking.js.map +1 -0
  124. package/dist/shared/index.d.ts +1 -0
  125. package/dist/shared/index.d.ts.map +1 -1
  126. package/dist/shared/index.js +1 -0
  127. package/dist/shared/index.js.map +1 -1
  128. package/docs/content/deployment.mdx +1 -1
  129. package/docs/content/frames.mdx +1 -1
  130. package/docs/content/integrations.mdx +27 -27
  131. package/docs/content/pr-visual-recap.mdx +17 -13
  132. package/docs/content/workspace-management.mdx +1 -1
  133. package/package.json +3 -2
  134. package/src/agent/engine/builder-engine.ts +4 -0
  135. package/src/client/AgentPanel.tsx +10 -4
  136. package/src/client/ConnectBuilderCard.tsx +5 -1
  137. package/src/client/chat/markdown-renderer.tsx +3 -5
  138. package/src/client/error-format.ts +2 -1
  139. package/src/client/index.ts +1 -0
  140. package/src/client/integrations/IntegrationsPanel.tsx +2 -18
  141. package/src/client/resources/McpIntegrationDialog.tsx +43 -32
  142. package/src/client/resources/mcp-integration-catalog.ts +4 -2
  143. package/src/client/settings/BackgroundAgentSection.tsx +5 -1
  144. package/src/client/settings/SettingsPanel.tsx +4 -4
  145. package/src/client/settings/useBuilderStatus.ts +3 -0
  146. package/src/localization/default-messages.ts +1 -7
  147. package/src/server/builder-browser.ts +4 -0
  148. package/src/server/builder-design-systems.ts +6 -1
  149. package/src/server/fusion-app.ts +5 -1
  150. package/src/shared/builder-link-tracking.ts +41 -0
  151. package/src/shared/index.ts +1 -0
@@ -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 {
@@ -135,6 +135,8 @@ pub struct FeatureConfig {
135
135
  pub onboarding_complete: bool,
136
136
  #[serde(default = "default_whisper_model_enabled")]
137
137
  pub whisper_model_enabled: bool,
138
+ #[serde(default = "default_whisper_model_id")]
139
+ pub whisper_model_id: String,
138
140
  }
139
141
 
140
142
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -170,6 +172,10 @@ fn default_whisper_model_enabled() -> bool {
170
172
  true
171
173
  }
172
174
 
175
+ fn default_whisper_model_id() -> String {
176
+ "base".to_string()
177
+ }
178
+
173
179
  fn default_screen_memory_retention_hours() -> u32 {
174
180
  8
175
181
  }
@@ -227,6 +233,7 @@ impl Default for FeatureConfig {
227
233
  screen_memory: ScreenMemoryConfig::default(),
228
234
  onboarding_complete: false,
229
235
  whisper_model_enabled: default_whisper_model_enabled(),
236
+ whisper_model_id: default_whisper_model_id(),
230
237
  }
231
238
  }
232
239
  }
@@ -333,6 +340,12 @@ pub async fn get_feature_config(app: AppHandle) -> Result<FeatureConfig, String>
333
340
  /// Save feature config to disk and emit a change event.
334
341
  #[tauri::command]
335
342
  pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result<(), String> {
343
+ if !crate::whisper_model::is_supported_model_id(&config.whisper_model_id) {
344
+ return Err(format!(
345
+ "unsupported Whisper model: {}",
346
+ config.whisper_model_id
347
+ ));
348
+ }
336
349
  let previous = load_config(&app);
337
350
  if (crate::rewind_clip::is_active(&app) || crate::util::is_recording_active(&app))
338
351
  && rewind_capture_contract_changed(&previous.screen_memory, &config.screen_memory)
@@ -366,6 +379,12 @@ pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result
366
379
  serde_json::json!({ "enabled": config.whisper_model_enabled }),
367
380
  );
368
381
  }
382
+ if previous.whisper_model_id != config.whisper_model_id {
383
+ let _ = app.emit(
384
+ "whisper:model-selection-changed",
385
+ serde_json::json!({ "modelId": config.whisper_model_id }),
386
+ );
387
+ }
369
388
  crate::screen_memory::sync_from_config(&app, &config);
370
389
  let _ = app.emit("app:feature-config-changed", config);
371
390
  Ok(())
@@ -406,6 +425,19 @@ mod tests {
406
425
  .contains(&"com.1password.1password".to_string()));
407
426
  }
408
427
 
428
+ #[test]
429
+ fn feature_config_defaults_to_base_whisper_model_for_legacy_json() {
430
+ let config: FeatureConfig = serde_json::from_value(serde_json::json!({
431
+ "clipsEnabled": true,
432
+ "meetingsEnabled": true,
433
+ "voiceEnabled": true,
434
+ "onboardingComplete": true
435
+ }))
436
+ .unwrap();
437
+
438
+ assert_eq!(config.whisper_model_id, "base");
439
+ }
440
+
409
441
  #[test]
410
442
  fn rewind_enums_use_stable_kebab_case_values() {
411
443
  assert_eq!(
@@ -231,8 +231,11 @@ pub fn run() {
231
231
  shortcuts::set_fn_shortcut_enabled,
232
232
  shortcuts::set_dictation_escape_active,
233
233
  // whisper model management
234
+ whisper_model::whisper_models,
234
235
  whisper_model::whisper_model_status,
235
236
  whisper_model::whisper_model_download,
237
+ whisper_model::whisper_downloaded_models,
238
+ whisper_model::whisper_model_delete,
236
239
  // permission status (silent checks for all TCC permissions)
237
240
  permission_status::check_permission_statuses,
238
241
  // persistent log file (production debugging)