@agent-native/core 0.114.8 → 0.114.10

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 (128) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +31 -3
  5. package/corpus/core/src/agent/engine/anthropic-engine.ts +20 -3
  6. package/corpus/core/src/agent/engine/builder-engine.ts +56 -9
  7. package/corpus/core/src/agent/engine/first-event-timeout.ts +64 -0
  8. package/corpus/core/src/client/AssistantChat.tsx +7 -3
  9. package/corpus/core/src/client/resources/ResourcesPanel.tsx +1 -1
  10. package/corpus/core/src/integrations/computer-supervision-store.ts +4 -4
  11. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  12. package/corpus/core/src/integrations/remote-commands-store.ts +5 -5
  13. package/corpus/core/src/integrations/remote-devices-store.ts +3 -3
  14. package/corpus/core/src/integrations/remote-push-store.ts +3 -3
  15. package/corpus/core/src/observability/traces.ts +38 -1
  16. package/corpus/core/src/server/action-routes.ts +23 -1
  17. package/corpus/core/src/server/http-response-telemetry.ts +6 -1
  18. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  20. package/corpus/templates/analytics/actions/account-deep-dive.ts +41 -6
  21. package/corpus/templates/analytics/changelog/2026-07-20-account-deep-dives-finish-with-partial-data-when-hubspot-lo.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-email-reports-now-include-an-image-when-a-single-p.md +6 -0
  23. package/corpus/templates/analytics/server/lib/dashboard-report.ts +11 -3
  24. package/corpus/templates/calendar/actions/create-event.ts +5 -0
  25. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +4 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +0 -1
  27. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +309 -421
  28. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +349 -0
  29. package/corpus/templates/calendar/app/i18n-data.ts +110 -0
  30. package/corpus/templates/calendar/app/lib/event-form-utils.ts +6 -0
  31. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  32. package/corpus/templates/calendar/app/root.tsx +4 -2
  33. package/corpus/templates/calendar/changelog/2026-07-20-events-can-be-updated-inline-with-date-time-timezone-and-rep.md +6 -0
  34. package/corpus/templates/calendar/changelog/2026-07-20-public-booking-pages-no-longer-hydrate-with-a-theme-mismatch.md +6 -0
  35. package/corpus/templates/calendar/shared/api.ts +1 -0
  36. package/corpus/templates/clips/AGENTS.md +7 -0
  37. package/corpus/templates/clips/actions/create-recording.ts +51 -10
  38. package/corpus/templates/clips/actions/get-recording-player-data.ts +34 -5
  39. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  40. package/corpus/templates/clips/actions/list-recordings.ts +15 -4
  41. package/corpus/templates/clips/actions/regenerate-summary.ts +32 -0
  42. package/corpus/templates/clips/actions/search-recordings.ts +28 -6
  43. package/corpus/templates/clips/actions/view-screen.ts +54 -47
  44. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -5
  45. package/corpus/templates/clips/app/components/library/recording-card.tsx +29 -32
  46. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +13 -0
  47. package/corpus/templates/clips/changelog/2026-07-20-agent-summaries-use-bounded-transcript-context.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-20-clip-summaries-now-finish-without-sending-a-long-transcript-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-clips-agents-only-discover-recordings-you-own-or-have-alread.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-hosted-recordings-no-longer-start-with-an-impossible-buffered-upload.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-20-meeting-recordings-now-offer-a-cleaner-transcript-view-with-.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-can-be-shared-with-limited-access-and-cards-sho.md +6 -0
  53. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +12 -8
  54. package/corpus/templates/clips/desktop/src/overlays/pill-logo.tsx +2 -15
  55. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +107 -340
  56. package/corpus/templates/clips/desktop/src/styles.css +27 -145
  57. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +6 -7
  58. package/corpus/templates/clips/server/lib/agent-recording-access.ts +73 -0
  59. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -2
  60. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +18 -0
  61. package/corpus/templates/plan/app/pages/PlansPage.tsx +10 -11
  62. package/corpus/templates/plan/changelog/2026-07-20-bridged-local-plans-now-load-comments-from-the-local-workspace.md +6 -0
  63. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  64. package/dist/agent/engine/ai-sdk-engine.js +27 -4
  65. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  66. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  67. package/dist/agent/engine/anthropic-engine.js +19 -4
  68. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  69. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  70. package/dist/agent/engine/builder-engine.js +40 -7
  71. package/dist/agent/engine/builder-engine.js.map +1 -1
  72. package/dist/agent/engine/first-event-timeout.d.ts +25 -0
  73. package/dist/agent/engine/first-event-timeout.d.ts.map +1 -0
  74. package/dist/agent/engine/first-event-timeout.js +49 -0
  75. package/dist/agent/engine/first-event-timeout.js.map +1 -0
  76. package/dist/client/AssistantChat.d.ts.map +1 -1
  77. package/dist/client/AssistantChat.js +7 -3
  78. package/dist/client/AssistantChat.js.map +1 -1
  79. package/dist/client/resources/ResourcesPanel.js +1 -1
  80. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/collab/struct-routes.d.ts +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/integrations/computer-supervision-store.js +4 -4
  85. package/dist/integrations/computer-supervision-store.js.map +1 -1
  86. package/dist/integrations/pending-tasks-store.js +1 -1
  87. package/dist/integrations/pending-tasks-store.js.map +1 -1
  88. package/dist/integrations/remote-commands-store.js +5 -5
  89. package/dist/integrations/remote-commands-store.js.map +1 -1
  90. package/dist/integrations/remote-devices-store.js +3 -3
  91. package/dist/integrations/remote-devices-store.js.map +1 -1
  92. package/dist/integrations/remote-push-store.js +3 -3
  93. package/dist/integrations/remote-push-store.js.map +1 -1
  94. package/dist/notifications/routes.d.ts +1 -1
  95. package/dist/observability/routes.d.ts +3 -3
  96. package/dist/observability/traces.d.ts.map +1 -1
  97. package/dist/observability/traces.js +23 -1
  98. package/dist/observability/traces.js.map +1 -1
  99. package/dist/progress/routes.d.ts +1 -1
  100. package/dist/resources/handlers.d.ts +1 -1
  101. package/dist/server/action-routes.d.ts.map +1 -1
  102. package/dist/server/action-routes.js +23 -1
  103. package/dist/server/action-routes.js.map +1 -1
  104. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  105. package/dist/server/http-response-telemetry.d.ts +2 -0
  106. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  107. package/dist/server/http-response-telemetry.js +5 -1
  108. package/dist/server/http-response-telemetry.js.map +1 -1
  109. package/dist/server/transcribe-voice.d.ts +1 -1
  110. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  111. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  112. package/package.json +2 -2
  113. package/src/agent/engine/ai-sdk-engine.ts +31 -3
  114. package/src/agent/engine/anthropic-engine.ts +20 -3
  115. package/src/agent/engine/builder-engine.ts +56 -9
  116. package/src/agent/engine/first-event-timeout.ts +64 -0
  117. package/src/client/AssistantChat.tsx +7 -3
  118. package/src/client/resources/ResourcesPanel.tsx +1 -1
  119. package/src/integrations/computer-supervision-store.ts +4 -4
  120. package/src/integrations/pending-tasks-store.ts +1 -1
  121. package/src/integrations/remote-commands-store.ts +5 -5
  122. package/src/integrations/remote-devices-store.ts +3 -3
  123. package/src/integrations/remote-push-store.ts +3 -3
  124. package/src/observability/traces.ts +38 -1
  125. package/src/server/action-routes.ts +23 -1
  126. package/src/server/http-response-telemetry.ts +6 -1
  127. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  128. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -4,8 +4,8 @@ import {
4
4
  IconChevronUp,
5
5
  IconCopy,
6
6
  IconExternalLink,
7
- IconGripHorizontal,
8
7
  IconLoader2,
8
+ IconMessageCircle,
9
9
  IconPlayerPauseFilled,
10
10
  IconPlayerPlayFilled,
11
11
  IconPlayerStopFilled,
@@ -50,12 +50,6 @@ export function RecordingPill() {
50
50
  const [hasTranscriptLines, setHasTranscriptLines] = useState(false);
51
51
  const [transcriptCopied, setTranscriptCopied] = useState(false);
52
52
  const [preloadedLines, setPreloadedLines] = useState<FinalLine[]>([]);
53
- const [notes, setNotes] = useState("");
54
- const [saveError, setSaveError] = useState(false);
55
- const notesDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
56
- // Latest typed notes, mirrored into a ref so the unmount/blur flush can
57
- // read the current value without re-subscribing.
58
- const pendingNotesRef = useRef<string | null>(null);
59
53
  const activeMeetingIdRef = useRef<string | null>(null);
60
54
  // Detached / "floating" mode — Wispr-style pill that auto-moves to the
61
55
  // top-right when the main app loses focus, with a drag handle. Driven by
@@ -69,19 +63,10 @@ export function RecordingPill() {
69
63
  const [hovered, setHovered] = useState(false);
70
64
  const startedAtRef = useRef<number>(Date.now());
71
65
  const tickRef = useRef<ReturnType<typeof setInterval> | null>(null);
72
- // Per-source levels. The mic recognizer (native_speech.rs) emits with
73
- // `source: "mic"`; the parallel ScreenCaptureKit tap (system_audio.rs)
74
- // emits `source: "system"`. We render two stacked bar groups so the user
75
- // can see each side is being captured.
76
- const micLevelRef = useRef(0);
77
- const sysLevelRef = useRef(0);
78
- // Track whether we've ever seen a system-audio level event in this
79
- // session — when present, we render the dual-stream waveform; otherwise
80
- // we collapse back to a single bar group so dictation-only recordings
81
- // don't get a dead second row.
82
- const [hasSystemAudio, setHasSystemAudio] = useState(false);
83
- const micCanvasRef = useRef<HTMLCanvasElement | null>(null);
84
- const sysCanvasRef = useRef<HTMLCanvasElement | null>(null);
66
+ // Mic and system audio share one calm activity meter, matching Granola's
67
+ // single indicator for the combined meeting capture.
68
+ const levelRef = useRef(0);
69
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
85
70
  const rafRef = useRef<number | null>(null);
86
71
  const stopFallbackRef = useRef<ReturnType<typeof setTimeout> | null>(null);
87
72
  const dragStartScreenPointRef = useRef<ScreenPoint | null>(null);
@@ -118,7 +103,7 @@ export function RecordingPill() {
118
103
  // (e.g. toggling the popover) while a meeting is already in progress.
119
104
  // Only reset session state below when the meeting/mode actually
120
105
  // changed — otherwise an in-progress meeting's timer, transcript, and
121
- // notes would wipe out on every tray click.
106
+ // transcript would wipe out on every tray click.
122
107
  if (isSameSession) return;
123
108
  // Reset timer on new context.
124
109
  startedAtRef.current = Date.now();
@@ -130,21 +115,11 @@ export function RecordingPill() {
130
115
  // disabled and a stale fallback timer can fire mid-session.
131
116
  setStopping(false);
132
117
  setError(null);
133
- // Reset notes and transcript state for the new session.
134
- setNotes("");
135
- setSaveError(false);
118
+ setExpanded(false);
119
+ // Reset transcript state for the new session.
136
120
  setPreloadedLines([]);
137
- pendingNotesRef.current = null;
138
- // Only clear the meeting id when leaving meeting mode. In meeting mode
139
- // clips:meeting-notes-init is the authoritative setter — resetting here
140
- // would race with that event and could wipe a freshly-set id.
141
- if (ev.payload?.mode !== "meeting") {
142
- activeMeetingIdRef.current = null;
143
- }
144
- if (notesDebounceRef.current) {
145
- clearTimeout(notesDebounceRef.current);
146
- notesDebounceRef.current = null;
147
- }
121
+ activeMeetingIdRef.current =
122
+ ev.payload?.mode === "meeting" ? (next.meetingId ?? null) : null;
148
123
  if (stopFallbackRef.current) {
149
124
  clearTimeout(stopFallbackRef.current);
150
125
  stopFallbackRef.current = null;
@@ -166,46 +141,12 @@ export function RecordingPill() {
166
141
  },
167
142
  ),
168
143
  );
169
- trackListen(
170
- listen<{ meetingId: string; initialNotes: string }>(
171
- "clips:meeting-notes-init",
172
- (ev) => {
173
- if (ctxRef.current.meetingId !== ev.payload.meetingId) return;
174
- activeMeetingIdRef.current = ev.payload.meetingId;
175
- if (pendingNotesRef.current !== null) {
176
- // User typed before the async fetch resolved — keep their edits and
177
- // save them now that we have the meeting id. Don't overwrite with
178
- // server data.
179
- emit("clips:save-meeting-notes", {
180
- meetingId: ev.payload.meetingId,
181
- notes: pendingNotesRef.current,
182
- }).catch(() => {});
183
- } else {
184
- setNotes(ev.payload.initialNotes ?? "");
185
- }
186
- },
187
- ),
188
- );
189
144
  trackListen(
190
145
  listen<{ lines: FinalLine[] }>("clips:transcript-preload", (ev) => {
191
146
  const lines = ev.payload?.lines;
192
147
  if (lines?.length) setPreloadedLines(lines);
193
148
  }),
194
149
  );
195
- // Unified auto-save signal from the popover — fires after either the
196
- // transcript or the notes are persisted.
197
- trackListen(
198
- listen<{ meetingId: string; ts: number }>("clips:meeting-saved", (ev) => {
199
- if (ev.payload?.meetingId !== activeMeetingIdRef.current) return;
200
- setSaveError(false);
201
- pendingNotesRef.current = null;
202
- }),
203
- );
204
- trackListen(
205
- listen("clips:meeting-save-failed", () => {
206
- setSaveError(true);
207
- }),
208
- );
209
150
  trackListen(
210
151
  listen<{ error: string }>("pill:error", (ev) => {
211
152
  setError(ev.payload?.error ?? "An error occurred.");
@@ -229,25 +170,15 @@ export function RecordingPill() {
229
170
  "voice:audio-level",
230
171
  (ev) => {
231
172
  const lvl = Math.max(0, Math.min(1, ev.payload.level));
232
- const source = ev.payload.source ?? "mic";
233
- if (source === "system") {
234
- sysLevelRef.current = lvl;
235
- setHasSystemAudio(true);
236
- } else {
237
- micLevelRef.current = lvl;
238
- }
173
+ levelRef.current = lvl;
239
174
  },
240
175
  ),
241
176
  );
242
177
  // Signal that all listeners are registered. app.tsx listens for this and
243
- // re-emits clips:pill-context + clips:meeting-notes-init so events that
244
- // fired before React mounted (fresh Tauri window) are not missed.
178
+ // re-emits the pill context and transcript preload for a fresh window.
245
179
  emit("clips:pill-ready", {}).catch(() => {});
246
180
  return () => {
247
181
  stopped = true;
248
- // Flush any pending note edit before tearing down (e.g. the pill window
249
- // closing on stop) so the last keystrokes aren't lost.
250
- flushNotesNow();
251
182
  unlistens.forEach((u) => {
252
183
  try {
253
184
  u();
@@ -255,10 +186,6 @@ export function RecordingPill() {
255
186
  // ignore
256
187
  }
257
188
  });
258
- if (notesDebounceRef.current) {
259
- clearTimeout(notesDebounceRef.current);
260
- notesDebounceRef.current = null;
261
- }
262
189
  if (stopFallbackRef.current) {
263
190
  clearTimeout(stopFallbackRef.current);
264
191
  stopFallbackRef.current = null;
@@ -280,140 +207,72 @@ export function RecordingPill() {
280
207
  };
281
208
  }, [ctx.mode, paused]);
282
209
 
283
- // Dual-stream "dancing bars" meter — one discrete vertical-bar group per
284
- // source (Granola/Wispr-style VU meter, not a continuous waveform line).
285
- // When system-audio hasn't emitted any levels yet (e.g. dictation-only
286
- // flow), the system canvas is hidden by the JSX below, but the rAF loop
287
- // still runs over whichever canvas refs are mounted.
210
+ // One combined "dancing bars" meter — a few discrete vertical bars instead
211
+ // of separate mic and system waveforms.
288
212
  useEffect(() => {
289
- const N_BARS = 14;
290
- const setups: Array<{
291
- W: number;
292
- H: number;
293
- centerY: number;
294
- ctx2d: CanvasRenderingContext2D;
295
- rng: number[];
296
- grad: CanvasGradient;
297
- levelRef: React.MutableRefObject<number>;
298
- shadowColor: string;
299
- gain: number;
300
- barWidth: number;
301
- gap: number;
302
- }> = [];
303
-
304
- const mount = (
305
- canvas: HTMLCanvasElement | null,
306
- levelRef: React.MutableRefObject<number>,
307
- color: string,
308
- shadowColor: string,
309
- gain: number,
310
- ) => {
311
- if (!canvas) return;
312
- const dpr = window.devicePixelRatio || 1;
313
- const W = canvas.clientWidth;
314
- const H = canvas.clientHeight;
315
- canvas.width = W * dpr;
316
- canvas.height = H * dpr;
317
- const ctx2d = canvas.getContext("2d");
318
- if (!ctx2d) return;
319
- ctx2d.scale(dpr, dpr);
320
- // Derive zero-alpha edge color from the full color to avoid duplicate strings.
321
- const color0 = color.replace(/[\d.]+\)$/, "0)");
322
- const grad = ctx2d.createLinearGradient(0, 0, W, 0);
323
- grad.addColorStop(0, color0);
324
- grad.addColorStop(0.1, color);
325
- grad.addColorStop(0.9, color);
326
- grad.addColorStop(1, color0);
327
- const centerY = H / 2;
328
- // Gap takes ~35% of each bar's slot, matching a "dancing bars" density.
329
- const slot = W / N_BARS;
330
- const gap = Math.max(1, slot * 0.35);
331
- const barWidth = Math.max(1, slot - gap);
332
- setups.push({
333
- W,
334
- H,
335
- centerY,
336
- ctx2d,
337
- rng: Array(N_BARS).fill(0.5),
338
- grad,
339
- levelRef,
340
- shadowColor,
341
- gain,
342
- barWidth,
343
- gap,
344
- });
345
- };
346
-
347
- // Mic (top, green — matches the collapsed pill's accent). Sys (bottom, sky
348
- // blue) with 2× gain — system levels run lower.
349
- mount(
350
- micCanvasRef.current,
351
- micLevelRef,
352
- "rgba(74, 222, 128, 0.95)",
353
- "rgba(74, 222, 128, 0.55)",
354
- 1.0,
355
- );
356
- mount(
357
- sysCanvasRef.current,
358
- sysLevelRef,
359
- "rgba(125, 211, 252, 0.85)",
360
- "rgba(125, 211, 252, 0.5)",
361
- 2.0,
362
- );
363
-
213
+ const canvas = canvasRef.current;
214
+ if (!canvas) return;
215
+ const N_BARS = 3;
216
+ const dpr = window.devicePixelRatio || 1;
217
+ const W = canvas.clientWidth;
218
+ const H = canvas.clientHeight;
219
+ canvas.width = W * dpr;
220
+ canvas.height = H * dpr;
221
+ const ctx2d = canvas.getContext("2d");
222
+ if (!ctx2d) return;
223
+ ctx2d.scale(dpr, dpr);
224
+ const slot = W / N_BARS;
225
+ const gap = Math.max(2, slot * 0.3);
226
+ const barWidth = Math.max(3, slot - gap);
227
+ const centerY = H / 2;
364
228
  const startMs = Date.now();
365
229
  let lastDrawMs = 0;
366
- // A bar meter reads the same to the eye well below display refresh rate
367
- // (60-120Hz); cap the actual draw work to ~20fps while still scheduling
368
- // via rAF every frame so the loop still pauses when the pill is hidden.
369
230
  const FRAME_INTERVAL_MS = 1000 / 20;
370
231
  const tick = () => {
371
232
  rafRef.current = requestAnimationFrame(tick);
372
233
  const nowMs = Date.now();
373
234
  if (nowMs - lastDrawMs < FRAME_INTERVAL_MS) return;
374
235
  lastDrawMs = nowMs;
375
- // Modulo prevents float precision loss on long recordings.
376
236
  const t = (nowMs - startMs) % 1_000_000;
377
- for (const s of setups) {
378
- const target = Math.min(1, s.levelRef.current * s.gain);
379
-
380
- s.ctx2d.clearRect(0, 0, s.W, s.H);
381
- s.ctx2d.fillStyle = s.grad;
382
- s.ctx2d.shadowColor = s.shadowColor;
383
- s.ctx2d.shadowBlur = 4;
384
-
385
- for (let i = 0; i < N_BARS; i += 1) {
386
- const phase = t * 0.005 + i * (Math.PI * 0.65);
387
- // Low idle baseline (~12% height) when silent; rises toward the
388
- // full bar height as level approaches 1.
389
- const barTarget =
390
- 0.12 + Math.sin(phase) * 0.5 * target + target * 0.38;
391
- s.rng[i] = s.rng[i] * 0.75 + Math.max(0.06, barTarget) * 0.25;
392
-
393
- const h = Math.min(1, s.rng[i]) * s.H * 0.9;
394
- const x = i * (s.barWidth + s.gap) + s.gap / 2;
395
- const y = s.centerY - h / 2;
396
- const radius = Math.min(s.barWidth / 2, 2);
397
- s.ctx2d.beginPath();
398
- if (typeof s.ctx2d.roundRect === "function") {
399
- s.ctx2d.roundRect(x, y, s.barWidth, h, radius);
400
- } else {
401
- s.ctx2d.rect(x, y, s.barWidth, h);
402
- }
403
- s.ctx2d.fill();
237
+ const target = Math.min(1, levelRef.current);
238
+ ctx2d.clearRect(0, 0, W, H);
239
+ ctx2d.fillStyle = "rgba(132, 204, 22, 0.98)";
240
+ ctx2d.shadowColor = "rgba(132, 204, 22, 0.48)";
241
+ ctx2d.shadowBlur = 4;
242
+ for (let i = 0; i < N_BARS; i += 1) {
243
+ const phase = t * 0.005 + i * (Math.PI * 0.65);
244
+ const barTarget = 0.2 + Math.sin(phase) * 0.42 * target + target * 0.38;
245
+ const h = Math.max(4, Math.min(1, barTarget) * H * 0.92);
246
+ const x = i * (barWidth + gap) + gap / 2;
247
+ const y = centerY - h / 2;
248
+ const radius = Math.min(barWidth / 2, 3);
249
+ ctx2d.beginPath();
250
+ if (typeof ctx2d.roundRect === "function") {
251
+ ctx2d.roundRect(x, y, barWidth, h, radius);
252
+ } else {
253
+ ctx2d.rect(x, y, barWidth, h);
404
254
  }
405
- s.ctx2d.shadowBlur = 0;
255
+ ctx2d.fill();
406
256
  }
257
+ ctx2d.shadowBlur = 0;
407
258
  };
408
259
  tick();
409
260
  return () => {
410
261
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
411
262
  rafRef.current = null;
412
263
  };
413
- // `expanded` is a dep because the collapsed view renders a single mic
414
- // canvas while the expanded meeting view can swap to the dual-stream
415
- // layout the canvas elements remount and must be re-initialized.
416
- }, [hasSystemAudio, expanded]);
264
+ }, [expanded]);
265
+
266
+ // Let the compact chip land first, then reveal the live transcript once per
267
+ // meeting. The delay keeps the indicator from feeling like a sudden panel.
268
+ useEffect(() => {
269
+ if (ctx.mode !== "meeting" || detached) return;
270
+ const timer = setTimeout(() => {
271
+ setExpanded(true);
272
+ invoke("recording_pill_expand", { expanded: true }).catch(() => {});
273
+ }, 280);
274
+ return () => clearTimeout(timer);
275
+ }, [ctx.mode, detached]);
417
276
 
418
277
  async function toggleExpanded() {
419
278
  const next = !expanded;
@@ -436,9 +295,6 @@ export function RecordingPill() {
436
295
  async function onStopClick() {
437
296
  if (stopping) return;
438
297
  setStopping(true);
439
- // Persist any pending note edit before the stop sequence tears the pill
440
- // window down.
441
- flushNotesNow();
442
298
  emit("clips:pill-stop", { meetingId: ctx.meetingId ?? null }).catch(
443
299
  () => {},
444
300
  );
@@ -447,17 +303,6 @@ export function RecordingPill() {
447
303
  }, 3_000);
448
304
  }
449
305
 
450
- // Click on the drag handle (detached mode) un-detaches the pill and
451
- // re-anchors it bottom-center on the meeting / main app. Re-focuses the
452
- // main app so the pill mode flips back through the focus listener too.
453
- async function onHandleClick() {
454
- try {
455
- await invoke("recording_pill_set_detached", { detached: false });
456
- } catch {
457
- // ignore — best effort
458
- }
459
- }
460
-
461
306
  // Stable callback for LiveTranscript to push locked-in lines up. Stable
462
307
  // identity matters — it's a dep of an effect inside LiveTranscript.
463
308
  const handleTranscriptLines = useCallback((lines: FinalLine[]) => {
@@ -480,20 +325,6 @@ export function RecordingPill() {
480
325
  }
481
326
  };
482
327
 
483
- // Immediately persist any pending (debounced) note edit. Used on blur and on
484
- // unmount so notes typed in the last ~800ms before stopping aren't dropped.
485
- const flushNotesNow = () => {
486
- if (!notesDebounceRef.current) return;
487
- clearTimeout(notesDebounceRef.current);
488
- notesDebounceRef.current = null;
489
- const mid = activeMeetingIdRef.current;
490
- if (mid && pendingNotesRef.current !== null)
491
- emit("clips:save-meeting-notes", {
492
- meetingId: mid,
493
- notes: pendingNotesRef.current,
494
- }).catch(() => {});
495
- };
496
-
497
328
  const handlePillMouseDown = (e: React.MouseEvent) => {
498
329
  if (e.button !== 0) return;
499
330
  const target = e.target as HTMLElement;
@@ -540,30 +371,7 @@ export function RecordingPill() {
540
371
  onClick={!expanded && !detached ? handlePillMediaClick : undefined}
541
372
  >
542
373
  <PillLogo className="pill-logo" />
543
- {hasSystemAudio ? (
544
- <div
545
- className="pill-wave-dual"
546
- aria-hidden
547
- title="Top: you. Bottom: speaker."
548
- >
549
- <canvas
550
- ref={micCanvasRef}
551
- className="pill-wave-canvas-half"
552
- aria-label="Microphone level"
553
- />
554
- <canvas
555
- ref={sysCanvasRef}
556
- className="pill-wave-canvas-half"
557
- aria-label="System audio level"
558
- />
559
- </div>
560
- ) : (
561
- <canvas
562
- ref={micCanvasRef}
563
- className="pill-wave-canvas"
564
- aria-hidden
565
- />
566
- )}
374
+ <canvas ref={canvasRef} className="pill-wave-canvas" aria-hidden />
567
375
  </div>
568
376
  <div className="pill-controls">
569
377
  <span className="pill-timer">
@@ -614,23 +422,8 @@ export function RecordingPill() {
614
422
  )}
615
423
  </button>
616
424
  </div>
617
- {!expanded && !detached ? (
618
- <div className="pill-vgrip" aria-hidden>
619
- <IconGripHorizontal size={14} stroke={2} />
620
- </div>
621
- ) : null}
622
425
  </div>
623
426
 
624
- {detached ? (
625
- <button
626
- type="button"
627
- onClick={onHandleClick}
628
- data-no-drag
629
- aria-label="Re-attach pill to main window"
630
- className="pill-drag-handle"
631
- />
632
- ) : null}
633
-
634
427
  {error ? (
635
428
  <div className="pill-error" role="alert">
636
429
  {error}
@@ -650,72 +443,30 @@ export function RecordingPill() {
650
443
  }
651
444
  >
652
445
  <div className="pill-divider" />
653
- {ctx.mode === "meeting" ? (
654
- <div className="pill-split">
655
- <div className="pill-split-pane">
656
- <div className="pill-pane-label pill-pane-label-row">
657
- <span>Transcript</span>
658
- <button
659
- type="button"
660
- data-no-drag
661
- className="pill-copy-btn"
662
- onClick={handleCopyTranscript}
663
- disabled={!hasTranscriptLines}
664
- aria-label="Copy transcript"
665
- title="Copy transcript"
666
- >
667
- {transcriptCopied ? (
668
- <IconCheck size={12} />
669
- ) : (
670
- <IconCopy size={12} />
671
- )}
672
- </button>
673
- </div>
674
- <div className="pill-transcript-area">
675
- <LiveTranscript
676
- onLinesChange={handleTranscriptLines}
677
- initialLines={preloadedLines}
678
- />
679
- </div>
680
- </div>
681
- <div className="pill-split-divider" />
682
- <div className="pill-split-pane">
683
- <div className="pill-pane-label">Notes</div>
684
- <div className="pill-notes-area">
685
- <textarea
686
- className="pill-notes-textarea"
687
- placeholder="Jot down notes during the meeting…"
688
- data-no-drag
689
- value={notes}
690
- onChange={(e) => {
691
- const val = e.target.value;
692
- setNotes(val);
693
- pendingNotesRef.current = val;
694
- if (saveError) setSaveError(false);
695
- if (notesDebounceRef.current)
696
- clearTimeout(notesDebounceRef.current);
697
- notesDebounceRef.current = setTimeout(() => {
698
- const mid = activeMeetingIdRef.current;
699
- if (mid)
700
- emit("clips:save-meeting-notes", {
701
- meetingId: mid,
702
- notes: val,
703
- }).catch(() => {});
704
- }, 800);
705
- }}
706
- onBlur={flushNotesNow}
707
- />
708
- </div>
709
- </div>
710
- </div>
711
- ) : (
712
- <div className="pill-transcript-area">
713
- <LiveTranscript
714
- onLinesChange={handleTranscriptLines}
715
- initialLines={preloadedLines}
716
- />
446
+ <div className="pill-transcript-area">
447
+ <div className="pill-pane-label pill-pane-label-row">
448
+ <span>Transcript</span>
449
+ <button
450
+ type="button"
451
+ data-no-drag
452
+ className="pill-copy-btn"
453
+ onClick={handleCopyTranscript}
454
+ disabled={!hasTranscriptLines}
455
+ aria-label="Copy transcript"
456
+ title="Copy transcript"
457
+ >
458
+ {transcriptCopied ? (
459
+ <IconCheck size={12} />
460
+ ) : (
461
+ <IconCopy size={12} />
462
+ )}
463
+ </button>
717
464
  </div>
718
- )}
465
+ <LiveTranscript
466
+ onLinesChange={handleTranscriptLines}
467
+ initialLines={preloadedLines}
468
+ />
469
+ </div>
719
470
  {ctx.mode === "meeting" ? (
720
471
  <div className="pill-saved-bar">
721
472
  <button
@@ -725,17 +476,33 @@ export function RecordingPill() {
725
476
  onClick={() => {
726
477
  const mid = activeMeetingIdRef.current;
727
478
  if (mid)
728
- emit("clips:open-meeting", { meetingId: mid }).catch(
729
- () => {},
730
- );
479
+ emit("clips:open-meeting", {
480
+ meetingId: mid,
481
+ openChat: true,
482
+ }).catch(() => {});
731
483
  }}
732
- title="Open this meeting in the browser"
484
+ title="Chat with transcript"
733
485
  >
734
- <IconExternalLink size={12} />
735
- Open in browser
486
+ <IconMessageCircle size={12} />
487
+ Chat with transcript
736
488
  </button>
737
489
  <span className="pill-saved-status">
738
- {saveError ? "Save failed — retrying on next edit" : ""}
490
+ <button
491
+ type="button"
492
+ data-no-drag
493
+ className="pill-open-web-btn"
494
+ onClick={() => {
495
+ const mid = activeMeetingIdRef.current;
496
+ if (mid)
497
+ emit("clips:open-meeting", { meetingId: mid }).catch(
498
+ () => {},
499
+ );
500
+ }}
501
+ title="Open this meeting in the browser"
502
+ >
503
+ <IconExternalLink size={12} />
504
+ Open in browser
505
+ </button>
739
506
  </span>
740
507
  </div>
741
508
  ) : null}