@agent-native/core 0.175.4 → 0.175.5-nightly-20260827014436

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 (202) hide show
  1. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +1 -0
  2. package/corpus/templates/analytics/app/components/auth/AuthProvider.tsx +8 -6
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +32 -0
  4. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
  5. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +66 -7
  6. package/corpus/templates/analytics/app/lib/prefetch-keys.ts +10 -0
  7. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +9 -7
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +10 -11
  9. package/corpus/templates/analytics/changelog/2026-08-24-feature-flag-changes-recover-from-temporary-workspace-direct.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-26-analytics-distinguishes-daily-from-weekly-activity-shows-sta.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-08-26-dashboard-metadata-now-shows-the-original-creator-separately.md +6 -0
  12. package/corpus/templates/analytics/changelog/2026-08-26-dashboard-names-stay-editable-when-the-analytics-sidebar-ref.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-08-26-fixed-the-agent-re-asking-dashboard-scope-questions-you-had-.md +6 -0
  14. package/corpus/templates/analytics/server/db/schema.ts +2 -0
  15. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +8 -2
  16. package/corpus/templates/analytics/server/lib/dashboards-store.ts +5 -0
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +6 -4
  18. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +19 -1
  19. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +40 -9
  20. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  21. package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +1 -0
  22. package/corpus/templates/analytics/shared/feature-flags.ts +11 -1
  23. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -9
  24. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +8 -7
  25. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +9 -8
  26. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -1
  27. package/corpus/templates/clips/actions/lib/loom-import-job.ts +52 -35
  28. package/corpus/templates/clips/actions/request-transcript.ts +6 -183
  29. package/corpus/templates/clips/app/components/clips-avatar.tsx +46 -0
  30. package/corpus/templates/clips/app/components/library/recording-card.tsx +8 -7
  31. package/corpus/templates/clips/app/components/library/space-card.tsx +8 -5
  32. package/corpus/templates/clips/app/components/meetings/attendee-stack.tsx +7 -8
  33. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +19 -12
  34. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +128 -94
  35. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +25 -104
  36. package/corpus/templates/clips/app/components/workspace/members-list.tsx +8 -6
  37. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +8 -6
  38. package/corpus/templates/clips/app/components/workspace/top-creators-table.tsx +8 -6
  39. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +8 -7
  40. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +0 -26
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -4
  42. package/corpus/templates/clips/changelog/2026-08-26-detected-calls-no-longer-duplicate-meetings-or-strand-the-prompt.md +6 -0
  43. package/corpus/templates/clips/desktop/src/app.tsx +8 -10
  44. package/corpus/templates/clips/desktop/src/components/live-waveform.tsx +99 -0
  45. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +248 -55
  46. package/corpus/templates/clips/desktop/src/lib/agent-steps.ts +477 -0
  47. package/corpus/templates/clips/desktop/src/lib/ask-sheet-layout.ts +71 -0
  48. package/corpus/templates/clips/desktop/src/lib/meeting-ask.ts +297 -0
  49. package/corpus/templates/clips/desktop/src/lib/url.ts +24 -0
  50. package/corpus/templates/clips/desktop/src/overlays/ask-steps.tsx +155 -0
  51. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +7 -81
  52. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +29 -2
  53. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +80 -22
  54. package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +13 -53
  55. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1017 -82
  56. package/corpus/templates/clips/desktop/src/styles.css +796 -57
  57. package/corpus/templates/clips/desktop/src/tailwind.css +23 -2
  58. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +1765 -219
  59. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -0
  60. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +45 -30
  61. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +5 -3
  62. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +91 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +123 -27
  64. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +16 -0
  65. package/corpus/templates/clips/shared/audio-meter.ts +145 -0
  66. package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
  67. package/corpus/templates/clips/shared/live-waveform.tsx +119 -0
  68. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +44 -22
  69. package/corpus/templates/content/app/components/ui/avatar.tsx +1 -0
  70. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  71. package/corpus/templates/design/app/hooks/use-question-flow.ts +9 -2
  72. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +111 -37
  73. package/corpus/templates/design/changelog/2026-08-26-design-no-longer-asks-users-to-confirm-guided-questions-they-have-already-answered.md +6 -0
  74. package/corpus/templates/dispatch/changelog/2026-08-26-signing-out-goes-straight-to-the-sign-in-page.md +5 -0
  75. package/corpus/templates/dispatch/server/lib/org-apps-directory.ts +96 -47
  76. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +73 -29
  77. package/corpus/templates/factory/.env.example +6 -2
  78. package/corpus/templates/factory/AGENTS.md +3 -3
  79. package/corpus/templates/factory/README.md +4 -2
  80. package/corpus/templates/factory/server/connectors/credentials.ts +70 -4
  81. package/corpus/templates/mail/actions/get-email.ts +68 -21
  82. package/corpus/templates/mail/actions/get-thread.ts +92 -51
  83. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -9
  84. package/corpus/templates/mail/app/hooks/use-emails.ts +7 -4
  85. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +7 -4
  86. package/corpus/templates/plan/app/pages/PlansPage.tsx +23 -15
  87. package/corpus/templates/slides/actions/add-deck.ts +20 -0
  88. package/corpus/templates/slides/actions/create-deck.ts +17 -7
  89. package/corpus/templates/slides/actions/delete-deck.ts +45 -5
  90. package/corpus/templates/slides/actions/get-deck.ts +77 -11
  91. package/corpus/templates/slides/actions/patch-deck.ts +14 -2
  92. package/corpus/templates/slides/actions/save-deck.ts +20 -0
  93. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +18 -7
  94. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +38 -14
  95. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +2 -2
  96. package/corpus/templates/slides/app/components/ui/avatar.tsx +1 -0
  97. package/corpus/templates/slides/server/handlers/decks.ts +10 -0
  98. package/corpus/templates/slides/shared/slide-ids.ts +133 -0
  99. package/corpus/templates/tasks/DEVELOPING.md +8 -0
  100. package/corpus/templates/tasks/drizzle.config.ts +3 -1
  101. package/corpus/templates/tasks/drizzle.local.config.ts +6 -0
  102. package/corpus/templates/tasks/package.json +2 -1
  103. package/corpus/templates/tasks/server/db/migrations/meta/0000_snapshot.json +234 -0
  104. package/corpus/templates/tasks/server/db/migrations/meta/_journal.json +13 -0
  105. package/corpus/templates/tasks/server/db/migrations.ts +110 -0
  106. package/corpus/templates/tasks/server/plugins/db.ts +3 -87
  107. package/dist/client/AssistantChat.js +3 -10
  108. package/dist/client/guided-questions.d.ts +11 -1
  109. package/dist/client/guided-questions.js +24 -7
  110. package/dist/client/hooks/index.d.ts +2 -1
  111. package/dist/client/hooks/index.js +2 -1
  112. package/dist/client/org/OrgSwitcher.js +3 -17
  113. package/dist/client/review/ReviewThreadPanel.js +4 -2
  114. package/dist/client/sign-out.d.ts +15 -0
  115. package/dist/client/sign-out.js +82 -0
  116. package/dist/client/use-action.d.ts +2 -0
  117. package/dist/client/use-action.js +4 -2
  118. package/dist/client/use-session.d.ts +23 -1
  119. package/dist/client/use-session.js +46 -0
  120. package/dist/collab/client.js +20 -5
  121. package/dist/collab/presence.d.ts +3 -1
  122. package/dist/collab/presence.js +48 -45
  123. package/dist/collab/routes.d.ts +1 -1
  124. package/dist/db/drizzle-config.d.ts +7 -0
  125. package/dist/db/drizzle-config.js +35 -9
  126. package/dist/db/drizzle-migrations.d.ts +28 -0
  127. package/dist/db/drizzle-migrations.js +81 -0
  128. package/dist/db/index.d.ts +1 -1
  129. package/dist/db/migrations.d.ts +13 -1
  130. package/dist/db/migrations.js +60 -31
  131. package/dist/db/schema.js +4 -1
  132. package/dist/deploy/build.d.ts +6 -0
  133. package/dist/deploy/build.js +41 -0
  134. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  135. package/dist/mcp/index.d.ts +1 -1
  136. package/dist/mcp/index.js +1 -1
  137. package/dist/mcp/org-directory.d.ts +19 -7
  138. package/dist/mcp/org-directory.js +92 -68
  139. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  140. package/dist/notifications/routes.d.ts +3 -3
  141. package/dist/org/context.d.ts +6 -0
  142. package/dist/org/context.js +22 -0
  143. package/dist/org/index.d.ts +1 -1
  144. package/dist/org/index.js +1 -1
  145. package/dist/resources/store.d.ts +12 -0
  146. package/dist/resources/store.js +55 -0
  147. package/dist/server/agent-discovery.d.ts +18 -2
  148. package/dist/server/agent-discovery.js +191 -22
  149. package/dist/server/auth.js +3 -2
  150. package/dist/server/builder-browser.d.ts +0 -4
  151. package/dist/server/builder-browser.js +23 -8
  152. package/dist/server/google-oauth.d.ts +18 -0
  153. package/dist/server/google-oauth.js +3 -3
  154. package/dist/server/onboarding-html.js +3 -3
  155. package/dist/server/poll.d.ts +4 -2
  156. package/dist/server/poll.js +8 -3
  157. package/dist/server/realtime-token.d.ts +1 -1
  158. package/dist/server/short-lived-token.d.ts +1 -1
  159. package/dist/server/short-lived-token.js +1 -1
  160. package/dist/shared/environment-lanes.d.ts +1 -0
  161. package/dist/shared/environment-lanes.js +1 -0
  162. package/dist/shared/google-profile-image.d.ts +2 -0
  163. package/dist/shared/google-profile-image.js +24 -0
  164. package/dist/shared/index.d.ts +1 -0
  165. package/dist/shared/index.js +1 -0
  166. package/dist/styles/agent-conversation.css +13 -0
  167. package/docs/content/creating-templates.mdx +10 -1
  168. package/docs/content/server-database.mdx +49 -3
  169. package/docs/content/server-plugins.mdx +39 -3
  170. package/docs/content/webmcp.mdx +2 -0
  171. package/package.json +2 -1
  172. package/corpus/templates/analytics/data/explorer-configs/_autosave.json +0 -18
  173. package/corpus/templates/analytics/data/explorer-configs/steve-chart.json +0 -18
  174. package/corpus/templates/analytics/data/explorer-dashboards/dashboard-1773260837897.json +0 -10
  175. package/corpus/templates/analytics/data/sync-config.json +0 -8
  176. package/corpus/templates/brain/data/sync-config.json +0 -1
  177. package/corpus/templates/calendar/data/availability.json +0 -32
  178. package/corpus/templates/calendar/data/settings.json +0 -6
  179. package/corpus/templates/calendar/data/sync-config.json +0 -10
  180. package/corpus/templates/chat/data/sync-config.json +0 -1
  181. package/corpus/templates/clips/data/sync-config.json +0 -1
  182. package/corpus/templates/clips/desktop/src/lib/audio-meter.ts +0 -80
  183. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +0 -113
  184. package/corpus/templates/design/data/sync-config.json +0 -1
  185. package/corpus/templates/factory/data/sync-config.json +0 -1
  186. package/corpus/templates/forms/data/settings.json +0 -8
  187. package/corpus/templates/forms/data/sync-config.json +0 -3
  188. package/corpus/templates/macros/data/sync-config.json +0 -4
  189. package/corpus/templates/mail/data/emails.defaults.json +0 -1
  190. package/corpus/templates/mail/data/labels.defaults.json +0 -59
  191. package/corpus/templates/mail/data/settings.defaults.json +0 -9
  192. package/corpus/templates/mail/data/sync-config.json +0 -8
  193. package/corpus/templates/slides/data/sync-config.json +0 -8
  194. package/corpus/templates/tasks/data/sync-config.json +0 -1
  195. package/dist/templates/chat/data/.gitkeep +0 -0
  196. package/dist/templates/chat/data/sync-config.json +0 -1
  197. package/dist/templates/default/data/.gitkeep +0 -0
  198. package/dist/templates/default/data/sync-config.json +0 -1
  199. package/src/templates/chat/data/.gitkeep +0 -0
  200. package/src/templates/chat/data/sync-config.json +0 -1
  201. package/src/templates/default/data/.gitkeep +0 -0
  202. package/src/templates/default/data/sync-config.json +0 -1
@@ -17,9 +17,9 @@ import {
17
17
  import { useCallback, useMemo, useRef, useState } from "react";
18
18
  import { Link } from "react-router";
19
19
 
20
+ import { ClipsAvatar } from "@/components/clips-avatar";
20
21
  import { AgentViewCount } from "@/components/player/recording-views-badge";
21
22
  import { ViewedByPopover } from "@/components/sharing/viewed-by-popover";
22
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
23
23
  import { Checkbox } from "@/components/ui/checkbox";
24
24
  import {
25
25
  DropdownMenu,
@@ -340,12 +340,13 @@ export function RecordingCard({
340
340
  </div>
341
341
  )}
342
342
  <div className="mt-1 flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
343
- <Avatar className="h-4 w-4 shrink-0">
344
- <AvatarImage src="" alt={recording.ownerEmail} />
345
- <AvatarFallback className="bg-primary/15 text-[8px] text-primary">
346
- {ownerInitials}
347
- </AvatarFallback>
348
- </Avatar>
343
+ <ClipsAvatar
344
+ email={recording.ownerEmail}
345
+ alt={recording.ownerEmail}
346
+ fallback={ownerInitials}
347
+ className="h-4 w-4 shrink-0"
348
+ fallbackClassName="bg-primary/15 text-[8px] text-primary"
349
+ />
349
350
  <span className="min-w-0 truncate">{recording.ownerEmail}</span>
350
351
  <span aria-hidden>•</span>
351
352
  <span className="shrink-0">{relative}</span>
@@ -9,6 +9,7 @@ import {
9
9
  import { useState } from "react";
10
10
  import { Link } from "react-router";
11
11
 
12
+ import { ClipsAvatar } from "@/components/clips-avatar";
12
13
  import {
13
14
  ContextMenu,
14
15
  ContextMenuContent,
@@ -104,13 +105,15 @@ export function SpaceCard({
104
105
  .slice(0, 2)
105
106
  .toUpperCase();
106
107
  return (
107
- <div
108
+ <ClipsAvatar
108
109
  key={email}
110
+ email={email}
111
+ alt={email}
112
+ fallback={initials}
113
+ className="h-5 w-5 border border-background"
114
+ fallbackClassName="bg-primary/15 text-[9px] font-medium text-primary"
109
115
  title={email}
110
- className="flex h-5 w-5 items-center justify-center rounded-full border border-background bg-primary/15 text-[9px] font-medium text-primary"
111
- >
112
- {initials}
113
- </div>
116
+ />
114
117
  );
115
118
  })}
116
119
  {members.length > 5 && (
@@ -5,7 +5,7 @@
5
5
  * gets a shadcn Tooltip showing name/email. Used on both list cards and
6
6
  * detail headers.
7
7
  */
8
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
8
+ import { ClipsAvatar } from "@/components/clips-avatar";
9
9
  import {
10
10
  Tooltip,
11
11
  TooltipContent,
@@ -58,14 +58,13 @@ export function AttendeeStack({
58
58
  {visible.map((p, i) => (
59
59
  <Tooltip key={`${p.email}-${i}`}>
60
60
  <TooltipTrigger asChild>
61
- <Avatar
61
+ <ClipsAvatar
62
+ email={p.email}
63
+ alt={p.name || p.email}
64
+ fallback={attendeeInitials(p)}
65
+ fallbackClassName="font-medium"
62
66
  className={`${sizeClass} ring-2 ring-background cursor-default`}
63
- >
64
- <AvatarImage alt={p.name || p.email} />
65
- <AvatarFallback className="font-medium">
66
- {attendeeInitials(p)}
67
- </AvatarFallback>
68
- </Avatar>
67
+ />
69
68
  </TooltipTrigger>
70
69
  <TooltipContent side="top" className="text-xs">
71
70
  <div className="font-medium">{p.name || p.email}</div>
@@ -9,7 +9,7 @@ import {
9
9
  import type { KeyboardEvent, ReactNode } from "react";
10
10
  import { useEffect, useMemo, useRef, useState } from "react";
11
11
 
12
- import { Avatar, AvatarFallback } from "@/components/ui/avatar";
12
+ import { ClipsAvatar } from "@/components/clips-avatar";
13
13
  import { Button } from "@/components/ui/button";
14
14
  import { Input } from "@/components/ui/input";
15
15
  import {
@@ -59,6 +59,7 @@ export interface SpeakerIdentity {
59
59
  key: string;
60
60
  label: string | null;
61
61
  initialsSource: AttendeeStackParticipant | string;
62
+ email?: string | null;
62
63
  isOwner: boolean;
63
64
  accentClass: string;
64
65
  /** The capture could not tell speakers apart, so this transcript names
@@ -291,6 +292,7 @@ export function resolveSpeaker(
291
292
  key,
292
293
  label,
293
294
  initialsSource: participant ?? label ?? (source === "mic" ? "Me" : "Them"),
295
+ email: participant?.email ?? (source === "mic" ? ownerEmail : null),
294
296
  isOwner: source === "mic",
295
297
  accentClass: accentForSpeaker(key, source === "mic"),
296
298
  };
@@ -641,21 +643,26 @@ export function TranscriptBubbles({
641
643
  >
642
644
  {!group.speaker.unattributed && (
643
645
  <div className="flex h-6 items-center gap-2">
644
- <Avatar
646
+ <ClipsAvatar
647
+ email={group.speaker.email}
648
+ alt={
649
+ group.speaker.label ||
650
+ (group.speaker.isOwner
651
+ ? t("transcriptBubbles.me")
652
+ : t("transcriptBubbles.them"))
653
+ }
654
+ fallback={attendeeInitials(
655
+ group.speaker.initialsSource,
656
+ )}
645
657
  className={cn(
646
658
  "size-6 shrink-0",
647
659
  group.speaker.accentClass,
648
660
  )}
649
- >
650
- <AvatarFallback
651
- className={cn(
652
- "text-[9px] font-semibold",
653
- group.speaker.accentClass,
654
- )}
655
- >
656
- {attendeeInitials(group.speaker.initialsSource)}
657
- </AvatarFallback>
658
- </Avatar>
661
+ fallbackClassName={cn(
662
+ "text-[9px] font-semibold",
663
+ group.speaker.accentClass,
664
+ )}
665
+ />
659
666
  <div className="flex min-h-6 items-center">
660
667
  <span
661
668
  className={cn(
@@ -32,6 +32,8 @@ export interface TranscriptSegment {
32
32
  startMs: number;
33
33
  endMs: number;
34
34
  text: string;
35
+ source?: "mic" | "system";
36
+ speaker?: string | null;
35
37
  }
36
38
 
37
39
  export interface TranscriptPanelProps {
@@ -42,12 +44,6 @@ export interface TranscriptPanelProps {
42
44
  onSeek: (ms: number) => void;
43
45
  status?: "pending" | "ready" | "failed";
44
46
  failureReason?: string | null;
45
- cleanup?: {
46
- status?: string | null;
47
- provider?: string | null;
48
- failureReason?: string | null;
49
- updatedAt?: string | null;
50
- } | null;
51
47
  recordingTitle?: string;
52
48
  /** Called when the user asks us to retry transcription after fixing an error. */
53
49
  onRetry?: () => void;
@@ -56,6 +52,87 @@ export interface TranscriptPanelProps {
56
52
  isRegenerating?: boolean;
57
53
  }
58
54
 
55
+ const DISPLAY_MAX_WORDS = 36;
56
+ const DISPLAY_MIN_WORDS_BEFORE_SENTENCE_BREAK = 18;
57
+ const DISPLAY_MAX_GAP_MS = 3_500;
58
+
59
+ export function mergeTranscriptSegmentsForDisplay(
60
+ segments: TranscriptSegment[],
61
+ ): TranscriptSegment[] {
62
+ const merged: TranscriptSegment[] = [];
63
+ let current: TranscriptSegment | null = null;
64
+
65
+ for (const segment of segments) {
66
+ if (!current) {
67
+ current = { ...segment };
68
+ continue;
69
+ }
70
+
71
+ const currentWords = countWords(current.text);
72
+ const segmentWords = countWords(segment.text);
73
+ const sameSpeaker =
74
+ current.source === segment.source && current.speaker === segment.speaker;
75
+ const shouldBreak =
76
+ !sameSpeaker ||
77
+ segment.startMs - current.endMs > DISPLAY_MAX_GAP_MS ||
78
+ (currentWords >= DISPLAY_MIN_WORDS_BEFORE_SENTENCE_BREAK &&
79
+ endsSentence(current.text)) ||
80
+ currentWords + segmentWords > DISPLAY_MAX_WORDS;
81
+
82
+ if (shouldBreak) {
83
+ merged.push(current);
84
+ current = { ...segment };
85
+ continue;
86
+ }
87
+
88
+ current = {
89
+ ...current,
90
+ endMs: segment.endMs,
91
+ text: `${current.text} ${segment.text}`,
92
+ };
93
+ }
94
+
95
+ if (current) merged.push(current);
96
+ return merged;
97
+ }
98
+
99
+ function countWords(text: string): number {
100
+ return text.split(/\s+/).filter(Boolean).length;
101
+ }
102
+
103
+ function endsSentence(text: string): boolean {
104
+ return /[.!?]["')\]}]*$/.test(text.trim());
105
+ }
106
+
107
+ export function getTranscriptSeekMs(
108
+ displaySegment: TranscriptSegment,
109
+ query: string,
110
+ segments: TranscriptSegment[],
111
+ ): number {
112
+ const normalizedQuery = query.trim().toLowerCase();
113
+ if (!normalizedQuery || segments.length === 0) return displaySegment.startMs;
114
+
115
+ const displaySegments = segments.filter(
116
+ (segment) =>
117
+ segment.startMs >= displaySegment.startMs &&
118
+ segment.endMs <= displaySegment.endMs,
119
+ );
120
+ const searchableText = displaySegments
121
+ .map((segment) => segment.text)
122
+ .join(" ")
123
+ .toLowerCase();
124
+ const matchIndex = searchableText.indexOf(normalizedQuery);
125
+ if (matchIndex < 0) return displaySegment.startMs;
126
+
127
+ let offset = 0;
128
+ for (const segment of displaySegments) {
129
+ if (matchIndex < offset + segment.text.length) return segment.startMs;
130
+ offset += segment.text.length + 1;
131
+ }
132
+
133
+ return displaySegment.startMs;
134
+ }
135
+
59
136
  export function TranscriptPanel(props: TranscriptPanelProps) {
60
137
  const t = useT();
61
138
  const {
@@ -66,7 +143,6 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
66
143
  onSeek,
67
144
  status,
68
145
  failureReason,
69
- cleanup,
70
146
  recordingTitle,
71
147
  onRetry,
72
148
  onRegenerate,
@@ -76,7 +152,7 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
76
152
  const [copied, setCopied] = useState(false);
77
153
 
78
154
  const displaySegments = useMemo<TranscriptSegment[]>(() => {
79
- if (segments.length > 0) return segments;
155
+ if (segments.length > 0) return mergeTranscriptSegmentsForDisplay(segments);
80
156
  const text = fullText?.trim();
81
157
  if (!text) return [];
82
158
  return [
@@ -110,7 +186,7 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
110
186
  }
111
187
 
112
188
  function downloadSrt() {
113
- const srt = toSrt(displaySegments);
189
+ const srt = toSrt(segments.length > 0 ? segments : displaySegments);
114
190
  const blob = new Blob([srt], { type: "text/srt;charset=utf-8" });
115
191
  const url = URL.createObjectURL(blob);
116
192
  const a = document.createElement("a");
@@ -133,7 +209,7 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
133
209
  if (status === "failed" && builderCreditsPaused) {
134
210
  return (
135
211
  <div className="p-4">
136
- <BuilderCreditsPausedNotice mode="transcription" onRetry={onRetry} />
212
+ <BuilderCreditsPausedNotice onRetry={onRetry} />
137
213
  </div>
138
214
  );
139
215
  }
@@ -219,7 +295,7 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
219
295
 
220
296
  return (
221
297
  <div className="flex flex-col h-full">
222
- <div className="flex items-center gap-2 p-3 border-b border-border">
298
+ <div className="flex items-center gap-2 border-b border-border p-3">
223
299
  <div className="relative flex-1">
224
300
  <IconSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
225
301
  <Input
@@ -229,62 +305,49 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
229
305
  className="pl-8 h-8 text-xs"
230
306
  />
231
307
  </div>
232
- <Tooltip>
233
- <TooltipTrigger asChild>
234
- <Button variant="ghost" size="icon" onClick={copyAll}>
235
- {copied ? (
236
- <IconCheck className="h-4 w-4 text-green-600" />
237
- ) : (
238
- <IconCopy className="h-4 w-4" />
239
- )}
240
- </Button>
241
- </TooltipTrigger>
242
- <TooltipContent>{t("transcriptPanel.copyTranscript")}</TooltipContent>
243
- </Tooltip>
244
- <Tooltip>
245
- <TooltipTrigger asChild>
246
- <Button variant="ghost" size="icon" onClick={downloadSrt}>
247
- <IconDownload className="h-4 w-4" />
248
- </Button>
249
- </TooltipTrigger>
250
- <TooltipContent>{t("transcriptPanel.downloadSrt")}</TooltipContent>
251
- </Tooltip>
252
- {onRegenerate ? (
308
+ <div className="flex items-center gap-0.5">
253
309
  <Tooltip>
254
310
  <TooltipTrigger asChild>
255
- <Button
256
- variant="ghost"
257
- size="icon"
258
- onClick={onRegenerate}
259
- disabled={isRegenerating}
260
- aria-label={t("transcriptPanel.regenerate")}
261
- >
262
- <IconRefresh
263
- className={cn("h-4 w-4", isRegenerating && "animate-spin")}
264
- />
311
+ <Button variant="ghost" size="icon" onClick={copyAll}>
312
+ {copied ? (
313
+ <IconCheck className="h-4 w-4 text-muted-foreground" />
314
+ ) : (
315
+ <IconCopy className="h-4 w-4" />
316
+ )}
265
317
  </Button>
266
318
  </TooltipTrigger>
267
- <TooltipContent>{t("transcriptPanel.regenerate")}</TooltipContent>
319
+ <TooltipContent>
320
+ {t("transcriptPanel.copyTranscript")}
321
+ </TooltipContent>
268
322
  </Tooltip>
269
- ) : null}
270
- </div>
271
-
272
- {cleanup?.status === "running" ? (
273
- <div className="mx-3 mt-3 rounded-md border border-border bg-accent/30 px-3 py-2 text-xs text-muted-foreground flex items-center gap-2">
274
- <IconLoader2 className="h-3.5 w-3.5 animate-spin shrink-0" />
275
- <span>{t("transcriptPanel.cleanupRunning")}</span>
276
- </div>
277
- ) : null}
278
-
279
- {cleanup?.status === "failed" &&
280
- isBuilderCreditsExhaustedMessage(cleanup.failureReason) ? (
281
- <BuilderCreditsPausedNotice mode="cleanup" className="mx-3 mt-3" />
282
- ) : cleanup?.status === "failed" ? (
283
- <div className="mx-3 mt-3 rounded-md border border-border bg-accent/30 px-3 py-2 text-xs text-muted-foreground flex items-start gap-2">
284
- <IconBolt className="h-3.5 w-3.5 mt-0.5 shrink-0" />
285
- <span>{friendlyCleanupFailure(cleanup.failureReason, t)}</span>
323
+ <Tooltip>
324
+ <TooltipTrigger asChild>
325
+ <Button variant="ghost" size="icon" onClick={downloadSrt}>
326
+ <IconDownload className="h-4 w-4" />
327
+ </Button>
328
+ </TooltipTrigger>
329
+ <TooltipContent>{t("transcriptPanel.downloadSrt")}</TooltipContent>
330
+ </Tooltip>
331
+ {onRegenerate ? (
332
+ <Tooltip>
333
+ <TooltipTrigger asChild>
334
+ <Button
335
+ variant="ghost"
336
+ size="icon"
337
+ onClick={onRegenerate}
338
+ disabled={isRegenerating}
339
+ aria-label={t("transcriptPanel.regenerate")}
340
+ >
341
+ <IconRefresh
342
+ className={cn("h-4 w-4", isRegenerating && "animate-spin")}
343
+ />
344
+ </Button>
345
+ </TooltipTrigger>
346
+ <TooltipContent>{t("transcriptPanel.regenerate")}</TooltipContent>
347
+ </Tooltip>
348
+ ) : null}
286
349
  </div>
287
- ) : null}
350
+ </div>
288
351
 
289
352
  <div className="flex-1 overflow-y-auto">
290
353
  {filtered.length === 0 ? (
@@ -297,6 +360,7 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
297
360
  <ul className="py-1">
298
361
  {filtered.map((seg) => {
299
362
  const isActive = displaySegments[activeIndex] === seg;
363
+ const seekMs = getTranscriptSeekMs(seg, query, segments);
300
364
  return (
301
365
  <li key={seg.startMs}>
302
366
  <TranscriptSegmentRow
@@ -305,12 +369,12 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
305
369
  gutter="panel"
306
370
  onClick={(event) => {
307
371
  if (hasSelectionWithin(event.currentTarget)) return;
308
- onSeek(seg.startMs);
372
+ onSeek(seekMs);
309
373
  }}
310
374
  onKeyDown={(event) => {
311
375
  if (event.key !== "Enter" && event.key !== " ") return;
312
376
  event.preventDefault();
313
- onSeek(seg.startMs);
377
+ onSeek(seekMs);
314
378
  }}
315
379
  >
316
380
  <span
@@ -382,17 +446,14 @@ function sanitizeFilename(s: string): string {
382
446
 
383
447
  const BUILDER_CREDITS_FEATURE_LABELS = [
384
448
  "builderCredits.featureBackupTranscription",
385
- "builderCredits.featureCleanup",
386
449
  "builderCredits.featureSummaries",
387
450
  "builderCredits.featureTitles",
388
451
  ] as const;
389
452
 
390
453
  function BuilderCreditsPausedNotice({
391
- mode,
392
454
  onRetry,
393
455
  className,
394
456
  }: {
395
- mode: "transcription" | "cleanup";
396
457
  onRetry?: () => void;
397
458
  className?: string;
398
459
  }) {
@@ -414,9 +475,7 @@ function BuilderCreditsPausedNotice({
414
475
  {t("builderCredits.pausedTitle")}
415
476
  </p>
416
477
  <p className="mt-1 text-xs leading-relaxed text-amber-900/80 dark:text-amber-100/80">
417
- {mode === "cleanup"
418
- ? t("builderCredits.cleanupDescription")
419
- : t("builderCredits.transcriptionDescription")}
478
+ {t("builderCredits.transcriptionDescription")}
420
479
  </p>
421
480
  </div>
422
481
  <div className="flex flex-wrap gap-1.5">
@@ -543,31 +602,6 @@ function friendlyTranscriptFailure(
543
602
  return reason;
544
603
  }
545
604
 
546
- function friendlyCleanupFailure(
547
- reason: string | null | undefined,
548
- t: ReturnType<typeof useT>,
549
- ): string {
550
- if (!reason) return t("transcriptPanel.cleanupKept");
551
- const normalized = reason.toLowerCase();
552
- if (
553
- normalized.includes("is connected, but") ||
554
- normalized.includes("returned no text") ||
555
- normalized.includes("service failed")
556
- ) {
557
- return t("transcriptPanel.cleanupBuilderFailed");
558
- }
559
- if (
560
- normalized.includes("incomplete") ||
561
- isBuilderCreditsExhaustedMessage(reason) ||
562
- normalized.includes("connect builder") ||
563
- normalized.includes("not configured") ||
564
- normalized.includes("api key")
565
- ) {
566
- return t("transcriptPanel.cleanupPaused");
567
- }
568
- return t("transcriptPanel.cleanupKept");
569
- }
570
-
571
605
  /**
572
606
  * Inline card shown when transcription needs a provider set up.
573
607
  *