@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,114 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { recordChange } from "@agent-native/core/server";
4
+ import { and, desc, eq } from "drizzle-orm";
5
+
6
+ import { getDb, schema } from "../db/index.js";
7
+ import {
8
+ FIRST_PARTY_DASHBOARD_ID,
9
+ repairFirstPartyRecurringUserPanels,
10
+ } from "./first-party-metric-catalog";
11
+
12
+ export async function repairPersistedFirstPartyDashboardQueries(): Promise<boolean> {
13
+ // guard:allow-unscoped — startup repair targets one fixed canonical dashboard
14
+ // and only replaces the exact shipped legacy SQL under an optimistic fence.
15
+ const db = getDb() as any;
16
+ const [row] = await db
17
+ .select({
18
+ id: schema.dashboards.id,
19
+ config: schema.dashboards.config,
20
+ kind: schema.dashboards.kind,
21
+ title: schema.dashboards.title,
22
+ updatedAt: schema.dashboards.updatedAt,
23
+ ownerEmail: schema.dashboards.ownerEmail,
24
+ orgId: schema.dashboards.orgId,
25
+ visibility: schema.dashboards.visibility,
26
+ })
27
+ .from(schema.dashboards)
28
+ .where(eq(schema.dashboards.id, FIRST_PARTY_DASHBOARD_ID));
29
+ if (!row || row.kind !== "sql" || typeof row.config !== "string") {
30
+ return false;
31
+ }
32
+
33
+ let config: Record<string, unknown>;
34
+ try {
35
+ config = JSON.parse(row.config) as Record<string, unknown>;
36
+ } catch {
37
+ return false;
38
+ }
39
+ const repaired = repairFirstPartyRecurringUserPanels(config);
40
+ if (!repaired.changed) return false;
41
+
42
+ const repairedAt = new Date().toISOString();
43
+ const revisionId = `dashrev-${Date.now()}-${randomUUID()}`;
44
+ const changed = await db.transaction(async (tx: any) => {
45
+ const updated = await tx
46
+ .update(schema.dashboards)
47
+ .set({
48
+ config: JSON.stringify(repaired.config),
49
+ updatedAt: repairedAt,
50
+ updatedBy: null,
51
+ })
52
+ .where(
53
+ and(
54
+ eq(schema.dashboards.id, FIRST_PARTY_DASHBOARD_ID),
55
+ eq(schema.dashboards.config, row.config),
56
+ eq(schema.dashboards.updatedAt, row.updatedAt),
57
+ ),
58
+ )
59
+ .returning({ id: schema.dashboards.id });
60
+ if (updated.length !== 1) return false;
61
+
62
+ await tx.insert(schema.dashboardRevisions).values({
63
+ id: revisionId,
64
+ dashboardId: row.id,
65
+ kind: row.kind,
66
+ title: row.title,
67
+ config: row.config,
68
+ createdAt: repairedAt,
69
+ createdBy: null,
70
+ ownerEmail: row.ownerEmail,
71
+ orgId: row.orgId,
72
+ });
73
+ const revisions = await tx
74
+ .select({ id: schema.dashboardRevisions.id })
75
+ .from(schema.dashboardRevisions)
76
+ .where(eq(schema.dashboardRevisions.dashboardId, row.id))
77
+ .orderBy(
78
+ desc(schema.dashboardRevisions.createdAt),
79
+ desc(schema.dashboardRevisions.id),
80
+ );
81
+ const retainedRevisionIds = new Set([revisionId]);
82
+ for (const revision of revisions) {
83
+ if (retainedRevisionIds.size >= 50) break;
84
+ retainedRevisionIds.add(revision.id);
85
+ }
86
+ for (const revision of revisions) {
87
+ if (retainedRevisionIds.has(revision.id)) continue;
88
+ await tx
89
+ .delete(schema.dashboardRevisions)
90
+ .where(eq(schema.dashboardRevisions.id, revision.id));
91
+ }
92
+ return true;
93
+ });
94
+ if (!changed) return false;
95
+
96
+ try {
97
+ recordChange({
98
+ source: "dashboards",
99
+ type: "change",
100
+ key: row.id,
101
+ ...(row.visibility === "public"
102
+ ? {}
103
+ : row.visibility === "org" && row.orgId
104
+ ? { orgId: row.orgId }
105
+ : { owner: row.ownerEmail }),
106
+ });
107
+ } catch (err) {
108
+ console.warn(
109
+ "[db] Canonical dashboard repair committed without a live change event:",
110
+ err instanceof Error ? err.message : err,
111
+ );
112
+ }
113
+ return true;
114
+ }
@@ -26,6 +26,8 @@
26
26
 
27
27
  export type MetricWindow = "30d" | "90d" | "all";
28
28
 
29
+ export const FIRST_PARTY_DASHBOARD_ID = "agent-native-templates-first-party";
30
+
29
31
  export interface FirstPartyMetric {
30
32
  /** Stable catalog key, also used as the default panel id. */
31
33
  key: string;
@@ -122,6 +124,7 @@ const USER_KEY_SQL = "NULLIF(user_key, '')";
122
124
  const RETENTION_ROLLING_DAYS = 7;
123
125
  const RETENTION_MIN_COHORT_SIZE = 5;
124
126
  const PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE = 20;
127
+ const RECURRING_ACTIVITY_LOOKBACK_DAYS = 365;
125
128
 
126
129
  function daysAgoSql(days: number): string {
127
130
  const unit = days === 1 ? "day" : "days";
@@ -223,8 +226,85 @@ const RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_S
223
226
  const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
224
227
  const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
225
228
  const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
226
- const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
227
- const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
229
+ export const LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
230
+ export const LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
231
+ const RECURRING_ACTIVITY_LOOKBACK_FILTER = `${EVENT_DATE_SQL} >= ${daysAgoSql(RECURRING_ACTIVITY_LOOKBACK_DAYS)}`;
232
+ const RECURRING_USERS_BY_TEMPLATE_SQL =
233
+ LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL.replace(
234
+ `${DASHBOARD_EMAIL_FILTER}), first_seen`,
235
+ `${DASHBOARD_EMAIL_FILTER} AND ${RECURRING_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
236
+ );
237
+ const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
238
+ LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL.replace(
239
+ `${DASHBOARD_EMAIL_FILTER}), first_seen`,
240
+ `${DASHBOARD_EMAIL_FILTER} AND ${RECURRING_ACTIVITY_LOOKBACK_FILTER}), first_seen`,
241
+ );
242
+ const LEGACY_RECURRING_USERS_DESCRIPTION =
243
+ "Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
244
+ const LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION =
245
+ "Weekly distinct signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.";
246
+ const RECURRING_USERS_DESCRIPTION =
247
+ "Daily signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
248
+ const RECURRING_USERS_WEEKLY_DESCRIPTION =
249
+ "Weekly distinct signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.";
250
+
251
+ export function repairFirstPartyRecurringUserPanels(
252
+ config: Record<string, unknown>,
253
+ ): { config: Record<string, unknown>; changed: boolean } {
254
+ if (!Array.isArray(config.panels)) return { config, changed: false };
255
+
256
+ const replacements = new Map([
257
+ [
258
+ "recurring-users-by-template",
259
+ {
260
+ legacySql: LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL,
261
+ sql: RECURRING_USERS_BY_TEMPLATE_SQL,
262
+ legacyDescription: LEGACY_RECURRING_USERS_DESCRIPTION,
263
+ description: RECURRING_USERS_DESCRIPTION,
264
+ },
265
+ ],
266
+ [
267
+ "recurring-users-by-template-bar",
268
+ {
269
+ legacySql: LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
270
+ sql: RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL,
271
+ legacyDescription: LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION,
272
+ description: RECURRING_USERS_WEEKLY_DESCRIPTION,
273
+ },
274
+ ],
275
+ ]);
276
+ let changed = false;
277
+ const panels = config.panels.map((rawPanel) => {
278
+ if (!rawPanel || typeof rawPanel !== "object") return rawPanel;
279
+ const panel = rawPanel as Record<string, unknown>;
280
+ const replacement =
281
+ typeof panel.id === "string" ? replacements.get(panel.id) : undefined;
282
+ if (!replacement || panel.sql !== replacement.legacySql) return rawPanel;
283
+
284
+ changed = true;
285
+ const panelConfig =
286
+ panel.config && typeof panel.config === "object"
287
+ ? (panel.config as Record<string, unknown>)
288
+ : null;
289
+ return {
290
+ ...panel,
291
+ sql: replacement.sql,
292
+ ...(panelConfig
293
+ ? {
294
+ config: {
295
+ ...panelConfig,
296
+ ...(panelConfig.description === replacement.legacyDescription
297
+ ? { description: replacement.description }
298
+ : {}),
299
+ },
300
+ }
301
+ : {}),
302
+ };
303
+ });
304
+ return changed
305
+ ? { config: { ...config, panels }, changed }
306
+ : { config, changed };
307
+ }
228
308
  const DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
229
309
  const WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
230
310
 
@@ -684,8 +764,7 @@ const ENTRIES: FirstPartyMetric[] = [
684
764
  valueKey: "users",
685
765
  },
686
766
  stacked: true,
687
- description:
688
- "Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.",
767
+ description: RECURRING_USERS_DESCRIPTION,
689
768
  },
690
769
  },
691
770
  {
@@ -706,8 +785,7 @@ const ENTRIES: FirstPartyMetric[] = [
706
785
  valueKey: "users",
707
786
  },
708
787
  stacked: true,
709
- description:
710
- "Weekly distinct signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded.",
788
+ description: RECURRING_USERS_WEEKLY_DESCRIPTION,
711
789
  },
712
790
  },
713
791
  {
@@ -8,6 +8,7 @@ import {
8
8
  // startup so the dashboard / analysis share actions know where to dispatch.
9
9
  import "../db/index.js";
10
10
  import * as schema from "../db/schema.js";
11
+ import { repairPersistedFirstPartyDashboardQueries } from "../lib/first-party-dashboard-repair.js";
11
12
 
12
13
  /**
13
14
  * Every Drizzle table exported from schema.ts. Filters out type-only and
@@ -1280,6 +1281,18 @@ const runAnalyticsMigrations = runMigrations(
1280
1281
  */
1281
1282
  export default async (nitroApp: any): Promise<void> => {
1282
1283
  await runAnalyticsMigrations(nitroApp);
1284
+ try {
1285
+ if (await repairPersistedFirstPartyDashboardQueries()) {
1286
+ console.info(
1287
+ "[db] Repaired bounded recurring-user queries on the canonical first-party dashboard.",
1288
+ );
1289
+ }
1290
+ } catch (err) {
1291
+ console.warn(
1292
+ "[db] Failed to repair canonical first-party dashboard queries (non-fatal):",
1293
+ err instanceof Error ? err.message : err,
1294
+ );
1295
+ }
1283
1296
  try {
1284
1297
  const summary = await ensureAdditiveColumns({
1285
1298
  db: getDbExec(),
@@ -646,10 +646,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
646
646
  const togglePlayback = useCallback(() => {
647
647
  const v = videoRef.current;
648
648
  if (!v) return;
649
- if (!v.paused || isPlaying) {
650
- pauseVideo();
651
- return;
652
- }
649
+ // A finished clip must always replay from the start, even when the browser
650
+ // left `paused` false at end of stream (MSE end-of-stream / DB-duration
651
+ // mismatch) or `isPlaying` is stale — otherwise the toggle below would
652
+ // pause an already-ended element and the play button appears to do nothing.
653
653
  if (v.ended) {
654
654
  try {
655
655
  v.currentTime = 0;
@@ -657,6 +657,12 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
657
657
  } catch {
658
658
  // Let the normal play attempt report a media error if the seek fails.
659
659
  }
660
+ requestPlay();
661
+ return;
662
+ }
663
+ if (!v.paused || isPlaying) {
664
+ pauseVideo();
665
+ return;
660
666
  }
661
667
  requestPlay();
662
668
  }, [isPlaying, pauseVideo, requestPlay]);
@@ -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
+ Live dictation text now appears above the desktop recording pill.
@@ -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,
@@ -4940,17 +4941,20 @@ function Setup({
4940
4941
  featureConfig?.meetingTranscriptionMode ?? "ask";
4941
4942
  const showMeetingWidgetEnabled =
4942
4943
  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,
4944
+ const whisper = useWhisperSettings(
4945
+ featureConfig,
4946
+ voiceProvider,
4947
+ onVoiceProviderChange,
4948
+ nativeVoiceProvider,
4953
4949
  );
4950
+ const {
4951
+ catalog: whisperModels,
4952
+ status: whisperStatus,
4953
+ enabled: whisperModelEnabled,
4954
+ modelId: whisperModelId,
4955
+ selectedModel: selectedWhisperModel,
4956
+ deletableModels,
4957
+ } = whisper;
4954
4958
  const [screenMemoryStatus, setScreenMemoryStatus] =
4955
4959
  useState<ScreenMemoryStatus | null>(null);
4956
4960
  const screenMemoryStatusRefreshVersionRef = useRef(0);
@@ -5059,24 +5063,6 @@ function Setup({
5059
5063
  );
5060
5064
  }
5061
5065
 
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
5066
  function setLaunchAtLoginEnabled(enabled: boolean) {
5081
5067
  if (!featureConfig) return;
5082
5068
  invoke("set_feature_config", {
@@ -5480,47 +5466,6 @@ function Setup({
5480
5466
  };
5481
5467
  }, [refreshScreenMemoryStatus]);
5482
5468
 
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
5469
  useEffect(() => {
5525
5470
  const base = (serverUrl ?? initial ?? DEFAULT_URL).replace(/\/+$/, "");
5526
5471
  let cancelled = false;
@@ -5606,7 +5551,7 @@ function Setup({
5606
5551
  onVoiceProviderChange(nativeVoiceProvider());
5607
5552
  } else if (mode === "whisper") {
5608
5553
  onVoiceProviderChange("whisper");
5609
- if (!whisperModelEnabled) setWhisperModelEnabled(true);
5554
+ if (!whisperModelEnabled) whisper.setEnabled(true);
5610
5555
  } else if (mode === "builder") {
5611
5556
  onVoiceProviderChange("builder-gemini");
5612
5557
  } else {
@@ -7192,15 +7137,58 @@ function Setup({
7192
7137
  />
7193
7138
  <Switch
7194
7139
  on={whisperModelEnabled}
7195
- onChange={setWhisperModelEnabled}
7140
+ onChange={whisper.setEnabled}
7196
7141
  label="Enable Whisper model"
7197
7142
  />
7198
7143
  </div>
7144
+ <SettingLabel
7145
+ label="Model"
7146
+ hint="Larger models can improve transcription accuracy but use more storage and may run more slowly."
7147
+ htmlFor="whisper-model"
7148
+ />
7149
+ <select
7150
+ id="whisper-model"
7151
+ className="setup-select"
7152
+ value={whisperModelId}
7153
+ onChange={(event) => whisper.setModelId(event.target.value)}
7154
+ disabled={
7155
+ whisperModels.length === 0 || whisperStatus?.state === "downloading"
7156
+ }
7157
+ >
7158
+ {whisperModels.map((model) => (
7159
+ <option key={model.id} value={model.id}>
7160
+ {model.title} · {model.sizeMb} MB — {model.description}
7161
+ </option>
7162
+ ))}
7163
+ </select>
7164
+ {selectedWhisperModel ? (
7165
+ <p className="setup-hint">{selectedWhisperModel.description}</p>
7166
+ ) : null}
7199
7167
  <WhisperModelStatusRow
7200
7168
  status={whisperStatus}
7201
7169
  enabled={whisperModelEnabled}
7202
- onDownload={triggerWhisperDownload}
7170
+ onDownload={whisper.triggerDownload}
7203
7171
  />
7172
+ {deletableModels.length > 0 ? (
7173
+ <div className="whisper-other-models">
7174
+ <p className="setup-hint">Other downloaded models</p>
7175
+ {deletableModels.map((model) => (
7176
+ <div key={model.id} className="whisper-other-model-row">
7177
+ <span className="whisper-other-model-name">
7178
+ {model.title} &middot; {model.sizeMb} MB
7179
+ </span>
7180
+ <button
7181
+ type="button"
7182
+ className="whisper-delete-btn"
7183
+ onClick={() => whisper.deleteModel(model.id)}
7184
+ >
7185
+ <IconTrash size={13} />
7186
+ Delete
7187
+ </button>
7188
+ </div>
7189
+ ))}
7190
+ </div>
7191
+ ) : null}
7204
7192
  </div>
7205
7193
 
7206
7194
  <div className="setup-section-heading">Dictation</div>
@@ -7528,9 +7516,12 @@ function WhisperModelStatusRow({
7528
7516
  <span className="whisper-progress-label">
7529
7517
  Downloading… {status.downloadedMb} / {status.totalMb} MB ({pct}%)
7530
7518
  </span>
7531
- <div className="whisper-progress-bar">
7532
- <div className="whisper-progress-fill" style={{ width: `${pct}%` }} />
7533
- </div>
7519
+ <progress
7520
+ className="whisper-progress-bar"
7521
+ value={pct}
7522
+ max={100}
7523
+ aria-label={`Whisper model download ${pct}% complete`}
7524
+ />
7534
7525
  </div>
7535
7526
  );
7536
7527
  }
@@ -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,