@agent-native/core 0.94.1 → 0.94.3

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 (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/cloneable-saas.mdx +15 -15
  4. package/corpus/core/docs/content/creating-templates.mdx +1 -1
  5. package/corpus/core/docs/content/extensions.mdx +1 -1
  6. package/corpus/core/docs/content/local-file-mode.mdx +6 -6
  7. package/corpus/core/docs/content/locales/ar-SA/voice-input.mdx +2 -2
  8. package/corpus/core/docs/content/locales/de-DE/voice-input.mdx +2 -2
  9. package/corpus/core/docs/content/locales/es-ES/voice-input.mdx +2 -2
  10. package/corpus/core/docs/content/locales/fr-FR/voice-input.mdx +2 -2
  11. package/corpus/core/docs/content/locales/hi-IN/voice-input.mdx +2 -2
  12. package/corpus/core/docs/content/locales/ja-JP/voice-input.mdx +2 -2
  13. package/corpus/core/docs/content/locales/ko-KR/voice-input.mdx +2 -2
  14. package/corpus/core/docs/content/locales/pt-BR/voice-input.mdx +2 -2
  15. package/corpus/core/docs/content/locales/zh-CN/voice-input.mdx +2 -2
  16. package/corpus/core/docs/content/locales/zh-TW/voice-input.mdx +2 -2
  17. package/corpus/core/docs/content/sharing.mdx +4 -4
  18. package/corpus/core/docs/content/template-analytics.mdx +2 -2
  19. package/corpus/core/docs/content/template-calendar.mdx +4 -4
  20. package/corpus/core/docs/content/template-clips.mdx +2 -2
  21. package/corpus/core/docs/content/template-content.mdx +12 -11
  22. package/corpus/core/docs/content/template-mail.mdx +1 -1
  23. package/corpus/core/docs/content/template-slides.mdx +2 -2
  24. package/corpus/core/docs/content/voice-input.mdx +2 -2
  25. package/corpus/core/docs/content/workspace.mdx +1 -1
  26. package/corpus/core/package.json +1 -1
  27. package/corpus/core/src/a2a/artifact-response.ts +154 -1
  28. package/corpus/core/src/client/AssistantChat.tsx +30 -0
  29. package/corpus/core/src/client/MultiTabAssistantChat.tsx +19 -1
  30. package/corpus/core/src/client/agent-chat.ts +6 -0
  31. package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +72 -2
  32. package/corpus/core/src/client/composer/realtime-voice-audio-level.ts +64 -0
  33. package/corpus/core/src/client/composer/realtime-voice-transcript.ts +134 -0
  34. package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +220 -6
  35. package/corpus/core/src/client/use-chat-threads.ts +33 -9
  36. package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
  37. package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
  38. package/corpus/core/src/integrations/adapters/slack.ts +77 -7
  39. package/corpus/core/src/integrations/types.ts +30 -0
  40. package/corpus/core/src/integrations/webhook-handler.ts +54 -18
  41. package/corpus/core/src/scripts/call-agent.ts +1 -0
  42. package/corpus/core/src/server/request-context.ts +2 -0
  43. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
  44. package/corpus/templates/clips/app/components/player/comments-panel.tsx +98 -1
  45. package/corpus/templates/clips/changelog/2026-07-10-comment-urls-open-as-clickable-links.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-10-desktop-recording-uploads-now-show-a-brief-uploaded-state-wi.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-10-desktop-recordings-now-retry-saving-their-locally-captured-t.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-10-desktop-update-prompts-are-more-compact-and-easier-to-dismiss.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-10-quiet-recordings-now-finish-saving-instead-of-getting-stuck.md +6 -0
  50. package/corpus/templates/clips/desktop/src/components/UpdateBanner.tsx +7 -4
  51. package/corpus/templates/clips/desktop/src/lib/recorder.ts +284 -85
  52. package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +148 -40
  53. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +13 -13
  54. package/corpus/templates/clips/desktop/src/styles.css +83 -13
  55. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -28
  56. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +130 -65
  57. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +10 -4
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +82 -1
  59. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +82 -1
  60. package/dist/a2a/artifact-response.d.ts.map +1 -1
  61. package/dist/a2a/artifact-response.js +127 -1
  62. package/dist/a2a/artifact-response.js.map +1 -1
  63. package/dist/client/AssistantChat.d.ts.map +1 -1
  64. package/dist/client/AssistantChat.js +20 -0
  65. package/dist/client/AssistantChat.js.map +1 -1
  66. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  67. package/dist/client/MultiTabAssistantChat.js +9 -0
  68. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  69. package/dist/client/agent-chat.d.ts +6 -0
  70. package/dist/client/agent-chat.d.ts.map +1 -1
  71. package/dist/client/agent-chat.js.map +1 -1
  72. package/dist/client/composer/RealtimeVoiceMode.d.ts +3 -1
  73. package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
  74. package/dist/client/composer/RealtimeVoiceMode.js +32 -3
  75. package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
  76. package/dist/client/composer/realtime-voice-audio-level.d.ts +14 -0
  77. package/dist/client/composer/realtime-voice-audio-level.d.ts.map +1 -0
  78. package/dist/client/composer/realtime-voice-audio-level.js +47 -0
  79. package/dist/client/composer/realtime-voice-audio-level.js.map +1 -0
  80. package/dist/client/composer/realtime-voice-transcript.d.ts +26 -0
  81. package/dist/client/composer/realtime-voice-transcript.d.ts.map +1 -0
  82. package/dist/client/composer/realtime-voice-transcript.js +103 -0
  83. package/dist/client/composer/realtime-voice-transcript.js.map +1 -0
  84. package/dist/client/composer/useRealtimeVoiceMode.d.ts +14 -0
  85. package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  86. package/dist/client/composer/useRealtimeVoiceMode.js +177 -6
  87. package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
  88. package/dist/client/use-chat-threads.d.ts.map +1 -1
  89. package/dist/client/use-chat-threads.js +26 -6
  90. package/dist/client/use-chat-threads.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  93. package/dist/integrations/a2a-continuation-processor.js +76 -21
  94. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  95. package/dist/integrations/a2a-continuations-store.d.ts +4 -1
  96. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  97. package/dist/integrations/a2a-continuations-store.js +130 -6
  98. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  99. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  100. package/dist/integrations/adapters/slack.js +56 -7
  101. package/dist/integrations/adapters/slack.js.map +1 -1
  102. package/dist/integrations/types.d.ts +25 -0
  103. package/dist/integrations/types.d.ts.map +1 -1
  104. package/dist/integrations/types.js.map +1 -1
  105. package/dist/integrations/webhook-handler.js +54 -16
  106. package/dist/integrations/webhook-handler.js.map +1 -1
  107. package/dist/observability/routes.d.ts +1 -1
  108. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  109. package/dist/scripts/call-agent.d.ts.map +1 -1
  110. package/dist/scripts/call-agent.js +1 -0
  111. package/dist/scripts/call-agent.js.map +1 -1
  112. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  113. package/dist/server/request-context.d.ts +2 -0
  114. package/dist/server/request-context.d.ts.map +1 -1
  115. package/dist/server/request-context.js.map +1 -1
  116. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
  117. package/docs/content/cloneable-saas.mdx +15 -15
  118. package/docs/content/creating-templates.mdx +1 -1
  119. package/docs/content/extensions.mdx +1 -1
  120. package/docs/content/local-file-mode.mdx +6 -6
  121. package/docs/content/locales/ar-SA/voice-input.mdx +2 -2
  122. package/docs/content/locales/de-DE/voice-input.mdx +2 -2
  123. package/docs/content/locales/es-ES/voice-input.mdx +2 -2
  124. package/docs/content/locales/fr-FR/voice-input.mdx +2 -2
  125. package/docs/content/locales/hi-IN/voice-input.mdx +2 -2
  126. package/docs/content/locales/ja-JP/voice-input.mdx +2 -2
  127. package/docs/content/locales/ko-KR/voice-input.mdx +2 -2
  128. package/docs/content/locales/pt-BR/voice-input.mdx +2 -2
  129. package/docs/content/locales/zh-CN/voice-input.mdx +2 -2
  130. package/docs/content/locales/zh-TW/voice-input.mdx +2 -2
  131. package/docs/content/sharing.mdx +4 -4
  132. package/docs/content/template-analytics.mdx +2 -2
  133. package/docs/content/template-calendar.mdx +4 -4
  134. package/docs/content/template-clips.mdx +2 -2
  135. package/docs/content/template-content.mdx +12 -11
  136. package/docs/content/template-mail.mdx +1 -1
  137. package/docs/content/template-slides.mdx +2 -2
  138. package/docs/content/voice-input.mdx +2 -2
  139. package/docs/content/workspace.mdx +1 -1
  140. package/package.json +1 -1
  141. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
@@ -1,3 +1,9 @@
1
+ import {
2
+ IconAlertCircle,
3
+ IconCheck,
4
+ IconExternalLink,
5
+ IconX,
6
+ } from "@tabler/icons-react";
1
7
  import { invoke } from "@tauri-apps/api/core";
2
8
  import { listen } from "@tauri-apps/api/event";
3
9
  import { open as openExternal } from "@tauri-apps/plugin-shell";
@@ -13,6 +19,7 @@ type NativeUploadProgress = {
13
19
  type ProcessingProgress = {
14
20
  stage?: string;
15
21
  progress?: number | null;
22
+ viewUrl?: string;
16
23
  };
17
24
 
18
25
  type NativeUploadFinished = {
@@ -23,15 +30,40 @@ type NativeUploadFinished = {
23
30
  localFilePath?: string | null;
24
31
  };
25
32
 
33
+ const FINALIZING_RESULT_STORAGE_KEY = "clips-finalizing-result";
34
+
35
+ function takePersistedFinalizingResult(): NativeUploadFinished | null {
36
+ try {
37
+ const raw = window.localStorage.getItem(FINALIZING_RESULT_STORAGE_KEY);
38
+ if (!raw) return null;
39
+ window.localStorage.removeItem(FINALIZING_RESULT_STORAGE_KEY);
40
+ const parsed: unknown = JSON.parse(raw);
41
+ if (!parsed || typeof parsed !== "object") return null;
42
+ const payload = parsed as Record<string, unknown>;
43
+ if (
44
+ typeof payload.recordingId !== "string" ||
45
+ typeof payload.viewUrl !== "string" ||
46
+ typeof payload.ok !== "boolean"
47
+ ) {
48
+ return null;
49
+ }
50
+ return {
51
+ recordingId: payload.recordingId,
52
+ viewUrl: payload.viewUrl,
53
+ ok: payload.ok,
54
+ error: typeof payload.error === "string" ? payload.error : null,
55
+ localFilePath: null,
56
+ };
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
26
62
  /**
27
- * Full-screen transparent feedback overlay. Rendered the moment the user
28
- * clicks Stop on the recording toolbar and kept visible while the desktop
29
- * finishes its durable backup and first upload/finalize attempt. The browser
30
- * can open `/r/:id` earlier so the page shows live progress.
31
- *
32
- * The window ignores cursor events on the Rust side, so the compact
33
- * bottom-left card does not block the user's screen while compression or
34
- * upload runs.
63
+ * Compact bottom-left feedback window. Rendered the moment the user clicks
64
+ * Stop and kept visible while the desktop finishes its durable backup and
65
+ * first upload/finalize attempt. The browser can open `/r/:id` earlier so the
66
+ * page shows live progress.
35
67
  */
36
68
  export function Finalizing() {
37
69
  const [progress, setProgress] = useState<ProcessingProgress>({
@@ -45,6 +77,9 @@ export function Finalizing() {
45
77
  let unlistenFinished: (() => void) | null = null;
46
78
  let completionTimer: ReturnType<typeof window.setTimeout> | null = null;
47
79
  let openingWatchdog: ReturnType<typeof window.setTimeout> | null = null;
80
+ const hardWatchdog = window.setTimeout(() => {
81
+ void invoke("hide_finalizing").catch(() => {});
82
+ }, 120_000);
48
83
  let finishedHandled = false;
49
84
  const clearCompletionTimer = () => {
50
85
  if (completionTimer) {
@@ -59,6 +94,7 @@ export function Finalizing() {
59
94
  }
60
95
  };
61
96
  listen<NativeUploadProgress>("clips:native-upload-progress", (event) => {
97
+ if (finishedHandled) return;
62
98
  const payload = event.payload ?? {};
63
99
  if (payload.stage === "opening" && payload.progress === 1) {
64
100
  clearOpeningWatchdog();
@@ -99,21 +135,36 @@ export function Finalizing() {
99
135
  const handleFinished = (payload: NativeUploadFinished) => {
100
136
  if (disposed || finishedHandled) return;
101
137
  finishedHandled = true;
138
+ try {
139
+ window.localStorage.removeItem(FINALIZING_RESULT_STORAGE_KEY);
140
+ } catch {
141
+ // Storage is a best-effort event-race fallback only.
142
+ }
102
143
  clearCompletionTimer();
103
144
  clearOpeningWatchdog();
104
145
  if (payload.ok && payload.viewUrl) {
105
- setProgress({ stage: "opening", progress: 1 });
106
- completionTimer = window.setTimeout(() => {
107
- void claimNativeOpen(payload.recordingId).then((claimed) => {
108
- if (!claimed || disposed) return;
109
- void openExternal(payload.viewUrl as string).catch(() => {});
110
- void invoke("hide_finalizing").catch(() => {});
146
+ setProgress({
147
+ stage: "uploaded",
148
+ progress: 1,
149
+ viewUrl: payload.viewUrl,
150
+ });
151
+ void claimNativeOpen(payload.recordingId).then((claimed) => {
152
+ if (!claimed || disposed) return;
153
+ void openExternal(payload.viewUrl as string).catch((err) => {
154
+ console.error("[clips-finalizing] open clip failed:", err);
111
155
  });
112
- }, 1500);
156
+ });
157
+ completionTimer = window.setTimeout(() => {
158
+ void invoke("hide_finalizing").catch(() => {});
159
+ }, 2500);
113
160
  return;
114
161
  }
115
162
 
116
- setProgress({ stage: "failed", progress: 1 });
163
+ setProgress({
164
+ stage: "failed",
165
+ progress: 1,
166
+ viewUrl: payload.viewUrl,
167
+ });
117
168
  completionTimer = window.setTimeout(() => {
118
169
  void invoke("show_popover").catch(() => {});
119
170
  void invoke("hide_finalizing").catch(() => {});
@@ -129,6 +180,11 @@ export function Finalizing() {
129
180
  return;
130
181
  }
131
182
  unlistenFinished = u;
183
+ const persisted = takePersistedFinalizingResult();
184
+ if (persisted) {
185
+ handleFinished(persisted);
186
+ return;
187
+ }
132
188
  void invoke<NativeUploadFinished | null>(
133
189
  "native_fullscreen_take_upload_finished",
134
190
  )
@@ -142,6 +198,7 @@ export function Finalizing() {
142
198
  disposed = true;
143
199
  clearCompletionTimer();
144
200
  clearOpeningWatchdog();
201
+ window.clearTimeout(hardWatchdog);
145
202
  unlisten?.();
146
203
  unlistenFinished?.();
147
204
  };
@@ -152,38 +209,89 @@ export function Finalizing() {
152
209
  ? Math.round(progress.progress * 100)
153
210
  : null;
154
211
  const caption =
155
- progress.stage === "uploading" ||
156
- progress.stage === "processing" ||
157
- progress.stage === "opening"
158
- ? "Uploading clip..."
212
+ progress.stage === "uploaded"
213
+ ? "Uploaded"
159
214
  : progress.stage === "failed"
160
215
  ? "Upload paused"
161
- : "Optimizing clip...";
216
+ : progress.stage === "uploading" ||
217
+ progress.stage === "processing" ||
218
+ progress.stage === "opening"
219
+ ? "Uploading clip..."
220
+ : "Optimizing clip...";
221
+ const finished = progress.stage === "uploaded" || progress.stage === "failed";
222
+
223
+ const dismiss = () => {
224
+ void invoke("hide_finalizing").catch(() => {});
225
+ };
226
+ const openClip = () => {
227
+ if (!progress.viewUrl) return;
228
+ void openExternal(progress.viewUrl).catch((err) => {
229
+ console.error("[clips-finalizing] open clip failed:", err);
230
+ });
231
+ };
162
232
 
163
233
  return (
164
234
  <div className="finalizing-root">
165
235
  <div className="finalizing-card">
166
- <div className="finalizing-spinner" aria-hidden="true" />
167
- <div className="finalizing-caption">{caption}</div>
168
- <div
169
- className="finalizing-progress"
170
- aria-label={
171
- percent === null ? caption : `${caption} ${percent}% complete`
172
- }
173
- role="progressbar"
174
- aria-valuemin={0}
175
- aria-valuemax={100}
176
- aria-valuenow={percent ?? undefined}
177
- >
178
- <div
179
- className={
180
- percent === null
181
- ? "finalizing-progress-fill finalizing-progress-fill-indeterminate"
182
- : "finalizing-progress-fill"
183
- }
184
- style={percent === null ? undefined : { width: `${percent}%` }}
236
+ {progress.stage === "uploaded" ? (
237
+ <IconCheck
238
+ className="finalizing-status-icon finalizing-status-icon-success"
239
+ aria-hidden="true"
240
+ />
241
+ ) : progress.stage === "failed" ? (
242
+ <IconAlertCircle
243
+ className="finalizing-status-icon finalizing-status-icon-failed"
244
+ aria-hidden="true"
185
245
  />
246
+ ) : (
247
+ <div className="finalizing-spinner" aria-hidden="true" />
248
+ )}
249
+ <div className="finalizing-caption" aria-live="polite">
250
+ {caption}
251
+ </div>
252
+ <div className="finalizing-actions">
253
+ {progress.viewUrl ? (
254
+ <button
255
+ type="button"
256
+ className="finalizing-action"
257
+ onClick={openClip}
258
+ aria-label="Open clip in browser"
259
+ title="Open clip in browser"
260
+ >
261
+ <IconExternalLink aria-hidden="true" />
262
+ </button>
263
+ ) : null}
264
+ <button
265
+ type="button"
266
+ className="finalizing-action"
267
+ onClick={dismiss}
268
+ aria-label="Dismiss upload status"
269
+ title="Dismiss"
270
+ >
271
+ <IconX aria-hidden="true" />
272
+ </button>
186
273
  </div>
274
+ {!finished ? (
275
+ <div
276
+ className="finalizing-progress"
277
+ aria-label={
278
+ percent === null ? caption : `${caption} ${percent}% complete`
279
+ }
280
+ role="progressbar"
281
+ aria-valuemin={0}
282
+ aria-valuemax={100}
283
+ aria-valuenow={percent ?? undefined}
284
+ >
285
+ <div
286
+ className={
287
+ percent === null
288
+ ? "finalizing-progress-fill finalizing-progress-fill-indeterminate"
289
+ : "finalizing-progress-fill"
290
+ }
291
+ style={percent === null ? undefined : { width: `${percent}%` }}
292
+ />
293
+ </div>
294
+ ) : null}
187
295
  </div>
188
296
  </div>
189
297
  );
@@ -326,19 +326,19 @@ export function MeetingNotification() {
326
326
  const secondaryLabel = hasJoin ? "& open Clips" : null;
327
327
 
328
328
  return (
329
- <div
330
- className="meeting-notification-root"
331
- onMouseEnter={() => {
332
- setShowClose(true);
333
- clearAutoHide();
334
- }}
335
- onMouseLeave={() => {
336
- setShowClose(false);
337
- setMenuOpen(false);
338
- resumeAutoHide();
339
- }}
340
- >
341
- <div className="meeting-notification">
329
+ <div className="meeting-notification-root">
330
+ <div
331
+ className="meeting-notification"
332
+ onMouseEnter={() => {
333
+ setShowClose(true);
334
+ clearAutoHide();
335
+ }}
336
+ onMouseLeave={() => {
337
+ setShowClose(false);
338
+ setMenuOpen(false);
339
+ resumeAutoHide();
340
+ }}
341
+ >
342
342
  <div
343
343
  className={`meeting-notification-bar ${isCalendar ? "meeting-notification-bar-calendar" : "meeting-notification-bar-adhoc"}`}
344
344
  />
@@ -2527,12 +2527,9 @@ body[data-clips-route="recording-pill"] #root {
2527
2527
  /* ------------------------------------------------------------------------- */
2528
2528
 
2529
2529
  /*
2530
- * Full-screen transparent overlay shown after the user clicks Stop and
2531
- * before the browser opens at /r/:id. The Tauri window is transparent +
2532
- * decorations-less (see `show_finalizing` in lib.rs), so we paint a
2533
- * compact card tucked into the bottom-left with a spinner and caption.
2534
- * The window also has ignore_cursor_events(true), so clicks pass through
2535
- * to whatever is beneath and the progress card stays out of the way.
2530
+ * Compact transparent window shown after the user clicks Stop. Keeping the OS
2531
+ * window close to the card's bounds lets its open/dismiss controls stay
2532
+ * interactive without blocking the rest of the screen.
2536
2533
  */
2537
2534
  .finalizing-root {
2538
2535
  position: fixed;
@@ -2540,17 +2537,16 @@ body[data-clips-route="recording-pill"] #root {
2540
2537
  display: flex;
2541
2538
  align-items: flex-end;
2542
2539
  justify-content: flex-start;
2543
- padding: 24px;
2540
+ padding: 10px;
2544
2541
  background: transparent;
2545
- pointer-events: none;
2546
2542
  }
2547
2543
 
2548
2544
  .finalizing-card {
2549
2545
  display: grid;
2550
- grid-template-columns: 20px minmax(0, 1fr);
2546
+ grid-template-columns: 20px minmax(0, 1fr) auto;
2551
2547
  align-items: center;
2552
2548
  gap: 7px 10px;
2553
- width: min(320px, calc(100vw - 48px));
2549
+ width: 100%;
2554
2550
  padding: 12px 14px;
2555
2551
  border-radius: 16px;
2556
2552
  background: rgba(20, 22, 28, 0.94);
@@ -2577,6 +2573,20 @@ body[data-clips-route="recording-pill"] #root {
2577
2573
  animation: finalizing-spin 0.9s linear infinite;
2578
2574
  }
2579
2575
 
2576
+ .finalizing-status-icon {
2577
+ width: 18px;
2578
+ height: 18px;
2579
+ stroke-width: 2.2;
2580
+ }
2581
+
2582
+ .finalizing-status-icon-success {
2583
+ color: #8ce99a;
2584
+ }
2585
+
2586
+ .finalizing-status-icon-failed {
2587
+ color: #ffb4a8;
2588
+ }
2589
+
2580
2590
  @keyframes finalizing-spin {
2581
2591
  to {
2582
2592
  transform: rotate(360deg);
@@ -2592,6 +2602,45 @@ body[data-clips-route="recording-pill"] #root {
2592
2602
  text-align: left;
2593
2603
  }
2594
2604
 
2605
+ .finalizing-actions {
2606
+ display: flex;
2607
+ align-items: center;
2608
+ gap: 2px;
2609
+ }
2610
+
2611
+ .finalizing-action {
2612
+ display: inline-flex;
2613
+ width: 26px;
2614
+ height: 26px;
2615
+ align-items: center;
2616
+ justify-content: center;
2617
+ padding: 0;
2618
+ border: 0;
2619
+ border-radius: 8px;
2620
+ background: transparent;
2621
+ color: rgba(255, 255, 255, 0.64);
2622
+ cursor: pointer;
2623
+ transition:
2624
+ color 140ms ease-out,
2625
+ background-color 140ms ease-out;
2626
+ }
2627
+
2628
+ .finalizing-action:hover {
2629
+ background: rgba(255, 255, 255, 0.1);
2630
+ color: white;
2631
+ }
2632
+
2633
+ .finalizing-action:focus-visible {
2634
+ outline: 2px solid rgba(255, 255, 255, 0.9);
2635
+ outline-offset: 1px;
2636
+ }
2637
+
2638
+ .finalizing-action svg {
2639
+ width: 15px;
2640
+ height: 15px;
2641
+ stroke-width: 2;
2642
+ }
2643
+
2595
2644
  .finalizing-progress {
2596
2645
  grid-column: 1 / -1;
2597
2646
  width: 100%;
@@ -3400,14 +3449,14 @@ body[data-clips-route="recording-pill"] #root {
3400
3449
  * Slots into the top of the popover when an update is in flight or ready.
3401
3450
  * Two visual states:
3402
3451
  * - pending: subtle inline row, just keeps the user informed.
3403
- * - ready: accent color + actions (Later / Restart) since clicking
3452
+ * - ready: accent color + actions (dismiss / Restart) since clicking
3404
3453
  * Restart is the thing we actually want them to do.
3405
3454
  */
3406
3455
  .update-banner {
3407
3456
  display: flex;
3408
3457
  align-items: center;
3409
3458
  gap: 8px;
3410
- margin: 0 12px 8px;
3459
+ margin: 0;
3411
3460
  padding: 8px 10px;
3412
3461
  border-radius: 8px;
3413
3462
  font-size: 12px;
@@ -3424,7 +3473,7 @@ body[data-clips-route="recording-pill"] #root {
3424
3473
  background: var(--surface-strong);
3425
3474
  color: var(--fg);
3426
3475
  border: 1px solid var(--border-strong, var(--border));
3427
- flex-wrap: wrap;
3476
+ flex-wrap: nowrap;
3428
3477
  }
3429
3478
 
3430
3479
  .update-banner--error {
@@ -3466,6 +3515,27 @@ body[data-clips-route="recording-pill"] #root {
3466
3515
  color: var(--fg);
3467
3516
  }
3468
3517
 
3518
+ .update-banner-btn--dismiss {
3519
+ width: 28px;
3520
+ height: 28px;
3521
+ display: inline-flex;
3522
+ align-items: center;
3523
+ justify-content: center;
3524
+ padding: 0;
3525
+ background: transparent;
3526
+ color: var(--fg-muted);
3527
+ }
3528
+
3529
+ .update-banner-btn--dismiss:hover {
3530
+ background: var(--surface-hover, var(--surface));
3531
+ color: var(--fg);
3532
+ }
3533
+
3534
+ .update-banner-btn--dismiss:focus-visible {
3535
+ outline: none;
3536
+ box-shadow: 0 0 0 2px var(--brand-ring);
3537
+ }
3538
+
3469
3539
  .update-banner-btn--primary {
3470
3540
  background: var(--brand);
3471
3541
  color: var(--bg);
@@ -458,42 +458,57 @@ fn stop_countdown_control_tracking() {
458
458
  COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
459
459
  }
460
460
 
461
- /// Full-screen transparent overlay that shows compact bottom-left progress
462
- /// while the recorder flushes its final chunks and awaits the server finalize.
463
- /// Rendered immediately after the user clicks Stop so they don't stare at a
464
- /// blank screen for a few seconds while `recorder.stop()` completes. Ignores
465
- /// cursor events so the progress card does not block the screen. Marked
466
- /// non-sharable for consistency with the other Clips overlays, even though
467
- /// the recording has already ended by the time this appears.
461
+ /// Compact bottom-left status window shown while the recorder flushes its
462
+ /// final chunks and awaits the server finalize. Keeping the native window near
463
+ /// the card's bounds makes its open/dismiss controls clickable without placing
464
+ /// an input-blocking transparent window over the whole monitor.
468
465
  #[tauri::command]
469
466
  pub async fn show_finalizing(app: AppHandle) -> Result<(), String> {
470
467
  dlog!("[clips-tray] show_finalizing invoked");
471
468
  if let Some(existing) = app.get_webview_window(FINALIZING_LABEL) {
472
469
  let _ = existing.close();
473
470
  }
474
- let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
475
- dlog!("[clips-tray] finalizing target size {}x{} physical", mw, mh);
476
- let win = WebviewWindowBuilder::new(&app, FINALIZING_LABEL, build_overlay_url("finalizing"))
477
- .title("Finalizing")
478
- .decorations(false)
479
- .transparent(true)
480
- .always_on_top(true)
481
- .skip_taskbar(true)
482
- .shadow(false)
483
- .visible(false)
484
- // Don't steal focus same rationale as the countdown overlay.
485
- .focused(false)
486
- .build()
487
- .map_err(|e| {
488
- eprintln!("[clips-tray] finalizing build failed: {}", e);
489
- e.to_string()
490
- })?;
491
- let _ = win.set_size(tauri::Size::Physical(PhysicalSize::new(mw, mh)));
492
- let _ = win.set_position(PhysicalPosition::new(mx, my));
493
- let _ = win.set_ignore_cursor_events(true);
471
+ let (mx, my, _mw, mh) = tray_monitor_physical_rect(&app);
472
+ let scale = overlay_scale_factor(&app);
473
+ let content_w: u32 = (336.0 * scale).round() as u32;
474
+ let content_h: u32 = (86.0 * scale).round() as u32;
475
+ let margin: i32 = (14.0 * scale).round() as i32;
476
+ let gutter = overlay_shadow_gutter_physical(&app);
477
+ let w = content_w + gutter * 2;
478
+ let h = content_h + gutter * 2;
479
+ let x = (mx + margin - gutter as i32).max(mx);
480
+ let y = (my + mh as i32 - h as i32 - margin).max(my);
481
+ dlog!("[clips-tray] finalizing target size {}x{} physical", w, h);
482
+ #[allow(unused_mut)]
483
+ let mut builder =
484
+ WebviewWindowBuilder::new(&app, FINALIZING_LABEL, build_overlay_url("finalizing"))
485
+ .title("Finalizing")
486
+ .decorations(false)
487
+ .transparent(true)
488
+ .always_on_top(true)
489
+ .skip_taskbar(true)
490
+ .resizable(false)
491
+ .shadow(false)
492
+ .visible(false)
493
+ // Don't steal focus — same rationale as the countdown overlay.
494
+ .focused(false);
495
+ // This window deliberately stays non-activating, but its Open and Dismiss
496
+ // controls must receive the activating click on macOS instead of requiring
497
+ // a second click after the window becomes key.
498
+ #[cfg(target_os = "macos")]
499
+ {
500
+ builder = builder.accept_first_mouse(true);
501
+ }
502
+ let win = builder.build().map_err(|e| {
503
+ eprintln!("[clips-tray] finalizing build failed: {}", e);
504
+ e.to_string()
505
+ })?;
506
+ let _ = win.set_size(tauri::Size::Physical(PhysicalSize::new(w, h)));
507
+ let _ = win.set_position(PhysicalPosition::new(x, y));
508
+ let _ = win.set_ignore_cursor_events(false);
494
509
  set_capture_excluded(&win);
495
510
  configure_overlay_behavior(&win);
496
- let _ = win.show();
511
+ crate::util::show_without_activation(&win);
497
512
  dlog!("[clips-tray] finalizing shown");
498
513
  Ok(())
499
514
  }