@agent-native/core 0.114.12 → 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 (241) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -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/application-state/index.ts +3 -0
  11. package/corpus/core/src/application-state/script-helpers.ts +12 -1
  12. package/corpus/core/src/application-state/store.ts +216 -15
  13. package/corpus/core/src/client/AgentPanel.tsx +10 -4
  14. package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
  15. package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
  16. package/corpus/core/src/client/error-format.ts +2 -1
  17. package/corpus/core/src/client/index.ts +1 -0
  18. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
  19. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
  20. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
  21. package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
  22. package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
  23. package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
  24. package/corpus/core/src/db/client.ts +44 -1
  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/player/video-player.tsx +10 -4
  46. package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
  50. package/corpus/templates/clips/desktop/src/app.tsx +66 -75
  51. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
  53. package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
  54. package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
  55. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
  56. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
  57. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  58. package/corpus/templates/clips/desktop/src/styles.css +100 -7
  59. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
  60. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
  61. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  62. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
  63. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
  64. package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
  65. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
  66. package/corpus/templates/clips/learnings.defaults.md +1 -1
  67. package/corpus/templates/clips/server/plugins/db.ts +11 -0
  68. package/corpus/templates/clips/shared/builder-credits.ts +1 -1
  69. package/corpus/templates/design/AGENTS.md +7 -3
  70. package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
  71. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
  72. package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
  73. package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
  74. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
  75. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
  76. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
  77. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
  78. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
  79. package/corpus/templates/design/shared/node-rewrite.ts +32 -0
  80. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
  81. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
  82. package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
  83. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
  84. package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
  85. package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
  86. package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
  87. package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
  88. package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
  89. package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
  90. package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
  91. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
  92. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
  93. package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
  94. package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
  95. package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
  96. package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
  97. package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
  98. package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
  99. package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
  100. package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
  101. package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
  102. package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
  103. package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
  104. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
  105. package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
  106. package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
  107. package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
  108. package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
  109. package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
  110. package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
  111. package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
  112. package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
  113. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
  114. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
  115. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
  116. package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
  117. package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
  118. package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
  119. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
  120. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
  121. package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
  122. package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
  123. package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
  124. package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
  125. package/corpus/templates/tasks/app/i18n/index.ts +34 -0
  126. package/corpus/templates/tasks/app/root.tsx +34 -2
  127. package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
  128. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
  129. package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
  130. package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
  131. package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
  132. package/corpus/templates/tasks/app/routes/team.tsx +6 -3
  133. package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
  134. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  135. package/dist/agent/engine/builder-engine.js +4 -0
  136. package/dist/agent/engine/builder-engine.js.map +1 -1
  137. package/dist/application-state/index.d.ts +2 -2
  138. package/dist/application-state/index.d.ts.map +1 -1
  139. package/dist/application-state/index.js +2 -2
  140. package/dist/application-state/index.js.map +1 -1
  141. package/dist/application-state/script-helpers.d.ts +3 -1
  142. package/dist/application-state/script-helpers.d.ts.map +1 -1
  143. package/dist/application-state/script-helpers.js +7 -1
  144. package/dist/application-state/script-helpers.js.map +1 -1
  145. package/dist/application-state/store.d.ts +7 -1
  146. package/dist/application-state/store.d.ts.map +1 -1
  147. package/dist/application-state/store.js +143 -17
  148. package/dist/application-state/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +10 -3
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  153. package/dist/client/ConnectBuilderCard.js +5 -1
  154. package/dist/client/ConnectBuilderCard.js.map +1 -1
  155. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  156. package/dist/client/chat/markdown-renderer.js +3 -2
  157. package/dist/client/chat/markdown-renderer.js.map +1 -1
  158. package/dist/client/error-format.d.ts +1 -1
  159. package/dist/client/error-format.d.ts.map +1 -1
  160. package/dist/client/error-format.js +1 -1
  161. package/dist/client/error-format.js.map +1 -1
  162. package/dist/client/index.d.ts +1 -0
  163. package/dist/client/index.d.ts.map +1 -1
  164. package/dist/client/index.js +1 -0
  165. package/dist/client/index.js.map +1 -1
  166. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  167. package/dist/client/integrations/IntegrationsPanel.js +3 -4
  168. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  169. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  170. package/dist/client/resources/McpIntegrationDialog.js +21 -7
  171. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  172. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  173. package/dist/client/resources/mcp-integration-catalog.js +2 -2
  174. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  175. package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
  176. package/dist/client/settings/BackgroundAgentSection.js +5 -1
  177. package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
  178. package/dist/client/settings/SettingsPanel.js +1 -1
  179. package/dist/client/settings/SettingsPanel.js.map +1 -1
  180. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  181. package/dist/client/settings/useBuilderStatus.js +2 -0
  182. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  183. package/dist/collab/routes.d.ts +1 -1
  184. package/dist/db/client.d.ts +7 -0
  185. package/dist/db/client.d.ts.map +1 -1
  186. package/dist/db/client.js +39 -1
  187. package/dist/db/client.js.map +1 -1
  188. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  189. package/dist/localization/default-messages.d.ts +0 -6
  190. package/dist/localization/default-messages.d.ts.map +1 -1
  191. package/dist/localization/default-messages.js +1 -7
  192. package/dist/localization/default-messages.js.map +1 -1
  193. package/dist/notifications/routes.d.ts +2 -2
  194. package/dist/observability/routes.d.ts +3 -3
  195. package/dist/resources/handlers.d.ts +1 -1
  196. package/dist/server/builder-browser.d.ts.map +1 -1
  197. package/dist/server/builder-browser.js +4 -0
  198. package/dist/server/builder-browser.js.map +1 -1
  199. package/dist/server/builder-design-systems.d.ts.map +1 -1
  200. package/dist/server/builder-design-systems.js +6 -1
  201. package/dist/server/builder-design-systems.js.map +1 -1
  202. package/dist/server/fusion-app.d.ts.map +1 -1
  203. package/dist/server/fusion-app.js +2 -1
  204. package/dist/server/fusion-app.js.map +1 -1
  205. package/dist/server/transcribe-voice.d.ts +1 -1
  206. package/dist/shared/builder-link-tracking.d.ts +14 -0
  207. package/dist/shared/builder-link-tracking.d.ts.map +1 -0
  208. package/dist/shared/builder-link-tracking.js +26 -0
  209. package/dist/shared/builder-link-tracking.js.map +1 -0
  210. package/dist/shared/index.d.ts +1 -0
  211. package/dist/shared/index.d.ts.map +1 -1
  212. package/dist/shared/index.js +1 -0
  213. package/dist/shared/index.js.map +1 -1
  214. package/docs/content/deployment.mdx +1 -1
  215. package/docs/content/frames.mdx +1 -1
  216. package/docs/content/integrations.mdx +27 -27
  217. package/docs/content/pr-visual-recap.mdx +17 -13
  218. package/docs/content/workspace-management.mdx +1 -1
  219. package/package.json +3 -2
  220. package/src/agent/engine/builder-engine.ts +4 -0
  221. package/src/application-state/index.ts +3 -0
  222. package/src/application-state/script-helpers.ts +12 -1
  223. package/src/application-state/store.ts +216 -15
  224. package/src/client/AgentPanel.tsx +10 -4
  225. package/src/client/ConnectBuilderCard.tsx +5 -1
  226. package/src/client/chat/markdown-renderer.tsx +3 -5
  227. package/src/client/error-format.ts +2 -1
  228. package/src/client/index.ts +1 -0
  229. package/src/client/integrations/IntegrationsPanel.tsx +2 -18
  230. package/src/client/resources/McpIntegrationDialog.tsx +43 -32
  231. package/src/client/resources/mcp-integration-catalog.ts +4 -2
  232. package/src/client/settings/BackgroundAgentSection.tsx +5 -1
  233. package/src/client/settings/SettingsPanel.tsx +4 -4
  234. package/src/client/settings/useBuilderStatus.ts +3 -0
  235. package/src/db/client.ts +44 -1
  236. package/src/localization/default-messages.ts +1 -7
  237. package/src/server/builder-browser.ts +4 -0
  238. package/src/server/builder-design-systems.ts +6 -1
  239. package/src/server/fusion-app.ts +5 -1
  240. package/src/shared/builder-link-tracking.ts +41 -0
  241. 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;
@@ -1445,6 +1445,9 @@ export function installDesktopVoiceDictation(
1445
1445
  // the tail because Web Speech only marks a segment as `isFinal`
1446
1446
  // after a confidence-threshold pass.
1447
1447
  next.browserTranscript = (finalSoFar + interim).trim();
1448
+ emit("voice:dictation-preview", {
1449
+ text: next.browserTranscript,
1450
+ }).catch(() => {});
1448
1451
  };
1449
1452
  recognition.onerror = (ev) => {
1450
1453
  if (ev.error !== "no-speech" && ev.error !== "aborted") {
@@ -2038,15 +2041,16 @@ export function installDesktopVoiceDictation(
2038
2041
 
2039
2042
  // Native (SFSpeechRecognizer) event subscriptions. These are always
2040
2043
  // installed — the events only fire when the Rust side has an active
2041
- // session, so subscribing on non-native sessions is harmless. The
2042
- // flow-bar listens to `voice:partial-transcript` independently so we
2043
- // don't re-emit it here.
2044
+ // session, so subscribing on non-native sessions is harmless.
2044
2045
  onPartialTranscript(({ text }) => {
2045
2046
  const current = session;
2046
2047
  if (!current || (current.kind !== "native" && current.kind !== "whisper"))
2047
2048
  return;
2048
2049
  if (current.cancelled || current.stopping) return;
2049
2050
  setInterimTranscript(current, text);
2051
+ emit("voice:dictation-preview", {
2052
+ text: current.browserTranscript,
2053
+ }).catch(() => {});
2050
2054
  })
2051
2055
  .then((u) => unlistens.push(u))
2052
2056
  .catch(() => {});
@@ -2065,6 +2069,13 @@ export function installDesktopVoiceDictation(
2065
2069
  if (!current) return;
2066
2070
  if (current.cancelled) return;
2067
2071
  appendFinalTranscript(current, text);
2072
+ const supersededLingeringSession =
2073
+ current === lingeringSession && session !== null && session !== current;
2074
+ if (!supersededLingeringSession) {
2075
+ emit("voice:dictation-preview", {
2076
+ text: current.browserTranscript,
2077
+ }).catch(() => {});
2078
+ }
2068
2079
  if (current === lingeringSession) {
2069
2080
  current.onNativeFinalize?.();
2070
2081
  }
@@ -16,6 +16,7 @@ type FlowState = "idle" | "recording" | "processing" | "complete" | "error";
16
16
  * Events:
17
17
  * - `voice:state-change` { state: "idle"|"recording"|"processing"|"complete"|"error" }
18
18
  * - `voice:audio-level` { level: number } (0-1) for waveform visualization
19
+ * - `voice:dictation-preview` { text: string }
19
20
  */
20
21
  export function FlowBar() {
21
22
  // Default to "recording" not "idle" — there's a race between the Rust
@@ -23,6 +24,8 @@ export function FlowBar() {
23
24
  // "idle" caused the bar to flash an "EN" language pill that never went
24
25
  // away if the start event was missed.
25
26
  const [state, setState] = useState<FlowState>("recording");
27
+ const [transcript, setTranscript] = useState("");
28
+ const transcriptRef = useRef<HTMLDivElement | null>(null);
26
29
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
27
30
  const levelRef = useRef(0);
28
31
  const rafRef = useRef<number | null>(null);
@@ -48,6 +51,7 @@ export function FlowBar() {
48
51
  trackListen(
49
52
  listen<{ state: FlowState }>("voice:state-change", (ev) => {
50
53
  setState(ev.payload.state);
54
+ if (ev.payload.state === "recording") setTranscript("");
51
55
  }),
52
56
  );
53
57
 
@@ -57,6 +61,12 @@ export function FlowBar() {
57
61
  }),
58
62
  );
59
63
 
64
+ trackListen(
65
+ listen<{ text: string }>("voice:dictation-preview", (ev) => {
66
+ setTranscript(ev.payload.text.trim());
67
+ }),
68
+ );
69
+
60
70
  return () => {
61
71
  stopped = true;
62
72
  unlistens.forEach((u) => {
@@ -70,6 +80,11 @@ export function FlowBar() {
70
80
  };
71
81
  }, []);
72
82
 
83
+ useEffect(() => {
84
+ const preview = transcriptRef.current;
85
+ if (preview) preview.scrollTop = preview.scrollHeight;
86
+ }, [transcript]);
87
+
73
88
  // Waveform canvas rendering loop — only runs during the "recording" state.
74
89
  useEffect(() => {
75
90
  if (state !== "recording") {
@@ -151,6 +166,16 @@ export function FlowBar() {
151
166
 
152
167
  return (
153
168
  <div className="flow-bar-root">
169
+ {transcript ? (
170
+ <div
171
+ ref={transcriptRef}
172
+ className="flow-bar-transcript-preview"
173
+ aria-live="polite"
174
+ >
175
+ {transcript}
176
+ </div>
177
+ ) : null}
178
+
154
179
  {/* Pill is ALWAYS mounted — when state goes idle we fade the
155
180
  opacity to 0 (see CSS) instead of removing it from the DOM.
156
181
  Inner content keeps its last frame rendered during the fade
@@ -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() {
@@ -4905,13 +4905,33 @@ body[data-clips-route="recording-pill"] #root {
4905
4905
  position: fixed;
4906
4906
  inset: var(--overlay-shadow-gutter);
4907
4907
  display: flex;
4908
+ flex-direction: column;
4908
4909
  align-items: center;
4909
4910
  justify-content: flex-end;
4911
+ gap: 6px;
4910
4912
  padding-bottom: 8px;
4911
4913
  background: transparent;
4912
4914
  pointer-events: none;
4913
4915
  }
4914
4916
 
4917
+ .flow-bar-transcript-preview {
4918
+ max-width: 560px;
4919
+ max-height: 90px;
4920
+ padding: 5px 10px;
4921
+ overflow: hidden;
4922
+ border: 1px solid rgba(255, 255, 255, 0.06);
4923
+ border-radius: 10px;
4924
+ background: rgba(18, 18, 20, 0.92);
4925
+ color: rgba(255, 255, 255, 0.9);
4926
+ font-size: 12px;
4927
+ font-weight: 500;
4928
+ line-height: 18px;
4929
+ text-align: center;
4930
+ word-break: break-word;
4931
+ backdrop-filter: blur(20px);
4932
+ -webkit-backdrop-filter: blur(20px);
4933
+ }
4934
+
4915
4935
  .flow-bar {
4916
4936
  display: flex;
4917
4937
  align-items: center;
@@ -5580,23 +5600,39 @@ body[data-clips-route="recording-pill"] #root {
5580
5600
  }
5581
5601
 
5582
5602
  .whisper-progress-bar {
5583
- height: 4px;
5584
5603
  width: 100%;
5604
+ height: 4px;
5605
+ margin-top: 4px;
5606
+ border: none;
5585
5607
  border-radius: 999px;
5586
- background: var(--surface-strong);
5608
+ appearance: none;
5587
5609
  overflow: hidden;
5588
- margin-top: 4px;
5610
+ background: var(--surface-strong);
5611
+ color: #16a34a;
5589
5612
  }
5590
5613
 
5591
- .whisper-progress-fill {
5592
- 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 {
5593
5625
  border-radius: inherit;
5594
5626
  background: #16a34a;
5595
- transition: width 300ms ease;
5596
5627
  }
5597
5628
 
5598
5629
  @media (prefers-color-scheme: dark) {
5599
- .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 {
5600
5636
  background: #4ade80;
5601
5637
  }
5602
5638
  }
@@ -5624,6 +5660,63 @@ body[data-clips-route="recording-pill"] #root {
5624
5660
  background: var(--surface-hover);
5625
5661
  }
5626
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
+
5627
5720
  /* ---- */
5628
5721
 
5629
5722
  .pill-error {
@@ -1506,10 +1506,9 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
1506
1506
 
1507
1507
  let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
1508
1508
  let scale = overlay_scale_factor(&app);
1509
- // Compact Wispr-style pill window: just enough transparent canvas for
1510
- // the bottom-centered waveform bar and its shadow gutter.
1511
- let content_w: u32 = (160.0 * scale).round() as u32;
1512
- let content_h: u32 = (56.0 * scale).round() as u32;
1509
+ // Wide + tall enough for a 5-line transcript preview above the pill.
1510
+ let content_w: u32 = (640.0 * scale).round() as u32;
1511
+ let content_h: u32 = (160.0 * scale).round() as u32;
1513
1512
  let bottom_margin: i32 = (14.0 * scale).round() as i32;
1514
1513
  let gutter = overlay_shadow_gutter_physical(&app);
1515
1514
  let w: u32 = content_w + gutter * 2;
@@ -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)
@@ -2465,8 +2465,15 @@ impl LiveAudioMixer {
2465
2465
  }
2466
2466
 
2467
2467
  /// Emit mixed sample buffers covering `out_pos..safe_end` in
2468
- /// `MIX_CHUNK_FRAMES` chunks: sum system + mic per frame, clamp, wrap as
2469
- /// LPCM `CMSampleBuffer`s with contiguous PTS.
2468
+ /// `MIX_CHUNK_FRAMES` chunks: average system + mic per frame, clamp, wrap
2469
+ /// as LPCM `CMSampleBuffer`s with contiguous PTS.
2470
+ ///
2471
+ /// Each source is weighted at 0.5 before summing so that two full-scale
2472
+ /// signals (which occurs when a USB audio interface with software monitoring
2473
+ /// routes the mic back through system audio) can never exceed ±1.0 and
2474
+ /// hard-clip. The standard SCK pipeline applies the same 0.5×L + 0.5×R
2475
+ /// pan-downmix for the same reason; loudnorm restores the target loudness
2476
+ /// in post-processing.
2470
2477
  fn drain_ready(
2471
2478
  &mut self,
2472
2479
  flush: bool,
@@ -2488,8 +2495,8 @@ impl LiveAudioMixer {
2488
2495
  let frame = a + f as i64;
2489
2496
  let (sl, sr) = self.system.sample_at(frame);
2490
2497
  let (ml, mr) = self.mic.sample_at(frame);
2491
- interleaved[f * 2] = (sl + ml).clamp(-1.0, 1.0);
2492
- interleaved[f * 2 + 1] = (sr + mr).clamp(-1.0, 1.0);
2498
+ interleaved[f * 2] = (sl * 0.5 + ml * 0.5).clamp(-1.0, 1.0);
2499
+ interleaved[f * 2 + 1] = (sr * 0.5 + mr * 0.5).clamp(-1.0, 1.0);
2493
2500
  }
2494
2501
  emitted.push(self.build_sample_buffer(&interleaved, a)?);
2495
2502
  a = b;