@agent-native/core 0.114.9 → 0.114.11

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 (150) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -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/cli/pr-visual-recap-workflow.ts +2 -3
  9. package/corpus/core/src/client/AssistantChat.tsx +7 -3
  10. package/corpus/core/src/client/resources/ResourcesPanel.tsx +1 -1
  11. package/corpus/core/src/extensions/actions.ts +219 -0
  12. package/corpus/core/src/integrations/computer-supervision-store.ts +4 -4
  13. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  14. package/corpus/core/src/integrations/remote-commands-store.ts +5 -5
  15. package/corpus/core/src/integrations/remote-devices-store.ts +3 -3
  16. package/corpus/core/src/integrations/remote-push-store.ts +3 -3
  17. package/corpus/core/src/observability/traces.ts +38 -1
  18. package/corpus/core/src/server/action-routes.ts +23 -1
  19. package/corpus/core/src/server/http-response-telemetry.ts +6 -1
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +19 -0
  21. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  23. package/corpus/templates/analytics/actions/account-deep-dive.ts +41 -6
  24. package/corpus/templates/analytics/changelog/2026-07-20-account-deep-dives-finish-with-partial-data-when-hubspot-lo.md +6 -0
  25. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-email-reports-now-include-an-image-when-a-single-p.md +6 -0
  26. package/corpus/templates/analytics/server/lib/dashboard-report.ts +11 -3
  27. package/corpus/templates/calendar/actions/create-event.ts +5 -0
  28. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +4 -1
  29. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +0 -1
  30. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +309 -421
  31. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +349 -0
  32. package/corpus/templates/calendar/app/i18n-data.ts +110 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +6 -0
  34. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  35. package/corpus/templates/calendar/app/root.tsx +4 -2
  36. package/corpus/templates/calendar/changelog/2026-07-20-events-can-be-updated-inline-with-date-time-timezone-and-rep.md +6 -0
  37. package/corpus/templates/calendar/changelog/2026-07-20-public-booking-pages-no-longer-hydrate-with-a-theme-mismatch.md +6 -0
  38. package/corpus/templates/calendar/shared/api.ts +1 -0
  39. package/corpus/templates/clips/AGENTS.md +7 -0
  40. package/corpus/templates/clips/actions/create-recording.ts +51 -10
  41. package/corpus/templates/clips/actions/get-recording-player-data.ts +34 -5
  42. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  43. package/corpus/templates/clips/actions/list-recordings.ts +15 -4
  44. package/corpus/templates/clips/actions/regenerate-summary.ts +32 -0
  45. package/corpus/templates/clips/actions/search-recordings.ts +28 -6
  46. package/corpus/templates/clips/actions/view-screen.ts +54 -47
  47. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -5
  48. package/corpus/templates/clips/app/components/library/recording-card.tsx +29 -32
  49. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +13 -0
  50. package/corpus/templates/clips/app/components/player/video-player.tsx +48 -0
  51. package/corpus/templates/clips/app/lib/fmp4.ts +26 -4
  52. package/corpus/templates/clips/app/lib/mse-video-loader.ts +13 -2
  53. package/corpus/templates/clips/changelog/2026-07-20-agent-summaries-use-bounded-transcript-context.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-20-clip-summaries-now-finish-without-sending-a-long-transcript-.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-20-clips-agents-only-discover-recordings-you-own-or-have-alread.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-20-hosted-recordings-no-longer-start-with-an-impossible-buffered-upload.md +6 -0
  57. package/corpus/templates/clips/changelog/2026-07-20-meeting-recordings-now-offer-a-cleaner-transcript-view-with-.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-07-20-rewind-capture-status.md +6 -0
  59. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-can-be-shared-with-limited-access-and-cards-sho.md +6 -0
  60. package/corpus/templates/clips/desktop/src/app.tsx +81 -71
  61. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +12 -8
  62. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +103 -0
  63. package/corpus/templates/clips/desktop/src/overlays/pill-logo.tsx +2 -15
  64. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +107 -340
  65. package/corpus/templates/clips/desktop/src/styles.css +37 -145
  66. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +6 -7
  67. package/corpus/templates/clips/server/lib/agent-recording-access.ts +73 -0
  68. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -2
  69. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +18 -0
  70. package/corpus/templates/plan/app/pages/PlansPage.tsx +10 -11
  71. package/corpus/templates/plan/changelog/2026-07-20-bridged-local-plans-now-load-comments-from-the-local-workspace.md +6 -0
  72. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  73. package/dist/agent/engine/ai-sdk-engine.js +27 -4
  74. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  75. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  76. package/dist/agent/engine/anthropic-engine.js +19 -4
  77. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  78. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  79. package/dist/agent/engine/builder-engine.js +40 -7
  80. package/dist/agent/engine/builder-engine.js.map +1 -1
  81. package/dist/agent/engine/first-event-timeout.d.ts +25 -0
  82. package/dist/agent/engine/first-event-timeout.d.ts.map +1 -0
  83. package/dist/agent/engine/first-event-timeout.js +49 -0
  84. package/dist/agent/engine/first-event-timeout.js.map +1 -0
  85. package/dist/cli/pr-visual-recap-workflow.d.ts +2 -2
  86. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  87. package/dist/cli/pr-visual-recap-workflow.js +2 -2
  88. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  89. package/dist/client/AssistantChat.d.ts.map +1 -1
  90. package/dist/client/AssistantChat.js +7 -3
  91. package/dist/client/AssistantChat.js.map +1 -1
  92. package/dist/client/resources/ResourcesPanel.js +1 -1
  93. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  94. package/dist/collab/awareness.d.ts +2 -2
  95. package/dist/collab/awareness.d.ts.map +1 -1
  96. package/dist/collab/routes.d.ts +1 -1
  97. package/dist/collab/struct-routes.d.ts +1 -1
  98. package/dist/extensions/actions.d.ts.map +1 -1
  99. package/dist/extensions/actions.js +193 -1
  100. package/dist/extensions/actions.js.map +1 -1
  101. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  102. package/dist/integrations/computer-supervision-store.js +4 -4
  103. package/dist/integrations/computer-supervision-store.js.map +1 -1
  104. package/dist/integrations/pending-tasks-store.js +1 -1
  105. package/dist/integrations/pending-tasks-store.js.map +1 -1
  106. package/dist/integrations/remote-commands-store.js +5 -5
  107. package/dist/integrations/remote-commands-store.js.map +1 -1
  108. package/dist/integrations/remote-devices-store.js +3 -3
  109. package/dist/integrations/remote-devices-store.js.map +1 -1
  110. package/dist/integrations/remote-push-store.js +3 -3
  111. package/dist/integrations/remote-push-store.js.map +1 -1
  112. package/dist/notifications/routes.d.ts +2 -2
  113. package/dist/observability/routes.d.ts +3 -3
  114. package/dist/observability/traces.d.ts.map +1 -1
  115. package/dist/observability/traces.js +23 -1
  116. package/dist/observability/traces.js.map +1 -1
  117. package/dist/progress/routes.d.ts +1 -1
  118. package/dist/resources/handlers.d.ts +1 -1
  119. package/dist/secrets/routes.d.ts +3 -3
  120. package/dist/server/action-routes.d.ts.map +1 -1
  121. package/dist/server/action-routes.js +23 -1
  122. package/dist/server/action-routes.js.map +1 -1
  123. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  124. package/dist/server/http-response-telemetry.d.ts +2 -0
  125. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  126. package/dist/server/http-response-telemetry.js +5 -1
  127. package/dist/server/http-response-telemetry.js.map +1 -1
  128. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +19 -0
  129. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  130. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  131. package/package.json +2 -2
  132. package/src/agent/engine/ai-sdk-engine.ts +31 -3
  133. package/src/agent/engine/anthropic-engine.ts +20 -3
  134. package/src/agent/engine/builder-engine.ts +56 -9
  135. package/src/agent/engine/first-event-timeout.ts +64 -0
  136. package/src/cli/pr-visual-recap-workflow.ts +2 -3
  137. package/src/client/AssistantChat.tsx +7 -3
  138. package/src/client/resources/ResourcesPanel.tsx +1 -1
  139. package/src/extensions/actions.ts +219 -0
  140. package/src/integrations/computer-supervision-store.ts +4 -4
  141. package/src/integrations/pending-tasks-store.ts +1 -1
  142. package/src/integrations/remote-commands-store.ts +5 -5
  143. package/src/integrations/remote-devices-store.ts +3 -3
  144. package/src/integrations/remote-push-store.ts +3 -3
  145. package/src/observability/traces.ts +38 -1
  146. package/src/server/action-routes.ts +23 -1
  147. package/src/server/http-response-telemetry.ts +6 -1
  148. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +19 -0
  149. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  150. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -277,9 +277,18 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
277
277
  // without this guard that effect would treat the two URLs as the "same
278
278
  // resource" and immediately revert our retry before `.load()` completes.
279
279
  const recoveringFromErrorRef = useRef(false);
280
+ const prevMseModeRef = useRef("");
281
+ // Render-phase mirrors of currentMs / isPlaying so the MSE-fallback effect
282
+ // below can read the pre-failure values. By the time effects run, React has
283
+ // already committed the new <video src>, causing the browser to reset
284
+ // currentTime -> 0 and paused -> true, making the element values useless.
285
+ const currentMsRef = useRef(startMs ?? 0);
286
+ const isPlayingRef = useRef(false);
280
287
  const [activeVideoSrc, setActiveVideoSrc] = useState(resolvedVideoSrc);
281
288
  const [isPlaying, setIsPlaying] = useState(false);
282
289
  const [currentMs, setCurrentMs] = useState(startMs ?? 0);
290
+ currentMsRef.current = currentMs;
291
+ isPlayingRef.current = isPlaying;
283
292
  const [loomStartMs, setLoomStartMs] = useState<number | null>(null);
284
293
  const [volume, setVolume] = useState(1);
285
294
  // Autoplaying players (e.g. the Slack unfurl embed, `?autoplay=1`) must
@@ -392,6 +401,45 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
392
401
  ? undefined
393
402
  : activeVideoSrc;
394
403
 
404
+ // When the MSE pipeline breaks mid-stream (premature 416 or
405
+ // ERR_CONTENT_LENGTH_MISMATCH after the backing GCS object is replaced by
406
+ // the compressed version), the loader calls onFatal which flips mse.mode to
407
+ // "native". This effect detects that transition and:
408
+ // 1. saves the playback position via currentMsRef (v.currentTime is already 0)
409
+ // 2. cache-busts activeVideoSrc so the native <video> path fetches fresh
410
+ // headers rather than a proxy-cached content-length from the old object
411
+ // 3. if the user was playing, arms playAttemptPendingRef so the existing
412
+ // retryPendingPlay call in onLoadedData resumes without user interaction
413
+ useEffect(() => {
414
+ const prev = prevMseModeRef.current;
415
+ prevMseModeRef.current = mse.mode;
416
+ if (prev !== "mse" || mse.mode !== "native") return;
417
+
418
+ const wasPlaying = isPlayingRef.current;
419
+ const posMs = currentMsRef.current > 0 ? currentMsRef.current : null;
420
+ if (posMs != null) resumeAfterReloadMsRef.current = posMs;
421
+
422
+ if (activeVideoSrc) {
423
+ recoveringFromErrorRef.current = true;
424
+ setActiveVideoSrc(
425
+ setUrlSearchParam(activeVideoSrc, "cb", String(Date.now())),
426
+ );
427
+ }
428
+
429
+ if (wasPlaying) {
430
+ const nextId = playAttemptIdRef.current + 1;
431
+ playAttemptIdRef.current = nextId;
432
+ playAttemptPendingRef.current = true;
433
+ setIsPlayPending(true);
434
+ setIsBuffering(true);
435
+ } else {
436
+ setIsPlaying(false);
437
+ setIsBuffering(false);
438
+ }
439
+ setIsPreparing(true);
440
+ setCanPlay(false);
441
+ }, [mse.mode, activeVideoSrc]);
442
+
395
443
  useEffect(() => {
396
444
  if (!resolvedVideoSrc) {
397
445
  setActiveVideoSrc(undefined);
@@ -208,14 +208,36 @@ export function findMoofOffset(bytes: Uint8Array): number {
208
208
  * True when the head of an MP4 shows the fragmented shape: the `hlsf` brand in
209
209
  * `ftyp`, or an `mvex` box inside `moov` (present only in fragmented files).
210
210
  * `headBytes` should be the first few KB of the file.
211
+ *
212
+ * Both checks walk the ISO BMFF box structure rather than scanning raw bytes.
213
+ * Numeric fields inside moov children (timestamps, matrix values, codec config)
214
+ * can accidentally contain the byte sequences "mvex" or "hlsf", producing false
215
+ * positives if we just scan the whole buffer.
211
216
  */
212
217
  export function isFragmentedMp4Head(headBytes: Uint8Array): boolean {
213
218
  if (headBytes.byteLength < 8) return false;
214
- // Must look like an MP4 at all.
219
+
215
220
  if (indexOfAscii(headBytes, "ftyp") !== 4) return false;
216
- if (indexOfAscii(headBytes, "hlsf") !== -1) return true;
217
- if (indexOfAscii(headBytes, "mvex") !== -1) return true;
218
- return false;
221
+
222
+ const ftypSize = readU32(headBytes, 0);
223
+ if (ftypSize < 12) return false;
224
+ const ftypEnd = Math.min(ftypSize, headBytes.byteLength);
225
+
226
+ // Offset 12 is minor_version (uint32), not a brand — start compatible brands at 16.
227
+ if (ftypEnd >= 12 && readType(headBytes, 8) === "hlsf") return true;
228
+ for (let i = 16; i + 4 <= ftypEnd; i += 4) {
229
+ if (readType(headBytes, i) === "hlsf") return true;
230
+ }
231
+
232
+ const boxes = readTopLevelBoxes(headBytes);
233
+ const moov = boxes.find((b) => b.type === "moov");
234
+ if (!moov || moov.size === 0) return false;
235
+ const moovPayloadEnd = Math.min(moov.start + moov.size, headBytes.byteLength);
236
+ const moovPayload = headBytes.subarray(
237
+ moov.start + moov.headerSize,
238
+ moovPayloadEnd,
239
+ );
240
+ return readTopLevelBoxes(moovPayload).some((b) => b.type === "mvex");
219
241
  }
220
242
 
221
243
  /** Cache detection by URL identity so we sniff each asset only once. */
@@ -555,11 +555,22 @@ export class MseVideoLoader {
555
555
  headers: { Range: `bytes=${start}-${end}` },
556
556
  signal: controller.signal,
557
557
  });
558
- // 416 means we asked past the end — treat as a clean end-of-stream.
559
- if (res.status === 416) return { bytes: new Uint8Array(0), eof: true };
558
+ if (res.status === 416) {
559
+ // A 416 at an offset before the known file total means the backing object
560
+ // was replaced with a smaller compressed version while we were streaming.
561
+ // Throw so runPump's catch calls fail() -> onFatal -> native path recovery.
562
+ // When totalKnown is false (cross-origin CDN hides Content-Range), we
563
+ // cannot tell premature from real EOF so we keep the old safe-EOF behaviour.
564
+ if (this.totalKnown && start < this.totalBytes) {
565
+ throw new Error("Range 416 before known EOF: backing file replaced");
566
+ }
567
+ return { bytes: new Uint8Array(0), eof: true };
568
+ }
560
569
  if (!res.ok) {
561
570
  throw new Error(`Range request failed: ${res.status}`);
562
571
  }
572
+ // If the backing file shrank mid-response (ERR_CONTENT_LENGTH_MISMATCH),
573
+ // arrayBuffer() throws here, which also routes through fail() -> onFatal.
563
574
  const buffer = await res.arrayBuffer();
564
575
  const bytes = new Uint8Array(buffer);
565
576
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Agent summaries use bounded transcript context
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Clip summaries now finish without sending a long transcript through the agent chat
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Clips agents only discover recordings you own or have already viewed
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Hosted recordings now stop with a clear retryable storage message instead of failing later during chunk upload.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Meeting recordings now offer a cleaner transcript view with quick chat and browser actions.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Rewind now reports the same live capture status on Home and in Settings without mistaking an idle moment for a permission problem.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Shared clips can be shared with limited access, and cards show who created them and when.
@@ -86,6 +86,7 @@ import {
86
86
  type RecorderStopResult,
87
87
  } from "./lib/recorder";
88
88
  import { REWIND_AGENT_PROMPT } from "./lib/rewind-agent-prompt";
89
+ import { getRewindStatusPresentation } from "./lib/rewind-status";
89
90
  import {
90
91
  loadBool,
91
92
  loadString,
@@ -893,6 +894,7 @@ export function App() {
893
894
  const [homeScreenMemoryStatus, setHomeScreenMemoryStatus] =
894
895
  useState<ScreenMemoryStatus | null>(null);
895
896
  const [homeScreenMemoryBusy, setHomeScreenMemoryBusy] = useState(false);
897
+ const homeScreenMemoryRefreshVersionRef = useRef(0);
896
898
  const [rewindAgentPromptCopied, setRewindAgentPromptCopied] = useState(false);
897
899
  const [agentHandoff, setAgentHandoff] =
898
900
  useState<RewindAgentHandoffRequest | null>(null);
@@ -946,26 +948,54 @@ export function App() {
946
948
  const isRecording = recorder !== null;
947
949
  // Whether the popover window is shown; driven by the visibility effect below.
948
950
  const [popoverVisible, setPopoverVisible] = useState(false);
951
+ const homeRewindPresentation = getRewindStatusPresentation({
952
+ status: homeScreenMemoryStatus,
953
+ config: featureConfig?.screenMemory ?? DEFAULT_SCREEN_MEMORY_CONFIG,
954
+ clipRecordingActive: isRecording || recordingFlowActive,
955
+ });
956
+ const refreshHomeScreenMemoryStatus = useCallback(() => {
957
+ const version = ++homeScreenMemoryRefreshVersionRef.current;
958
+ invoke<ScreenMemoryStatus>("screen_memory_status")
959
+ .then((status) => {
960
+ if (version === homeScreenMemoryRefreshVersionRef.current) {
961
+ setHomeScreenMemoryStatus(status);
962
+ }
963
+ })
964
+ .catch(() => {});
965
+ }, []);
949
966
  useEffect(() => {
950
967
  if (featureConfig?.screenMemory?.enabled !== true) {
968
+ homeScreenMemoryRefreshVersionRef.current += 1;
951
969
  setHomeScreenMemoryStatus(null);
952
970
  return;
953
971
  }
954
972
  let cancelled = false;
955
973
  const refresh = () => {
956
- invoke<ScreenMemoryStatus>("screen_memory_status")
957
- .then((status) => {
958
- if (!cancelled) setHomeScreenMemoryStatus(status);
959
- })
960
- .catch(() => {});
974
+ if (!cancelled) refreshHomeScreenMemoryStatus();
961
975
  };
962
976
  refresh();
963
977
  const timer = window.setInterval(refresh, popoverVisible ? 5_000 : 30_000);
978
+ let unlisten: (() => void) | undefined;
979
+ listen("clips:screen-memory-changed", refresh)
980
+ .then((stopListening) => {
981
+ if (cancelled) {
982
+ stopListening();
983
+ return;
984
+ }
985
+ unlisten = stopListening;
986
+ })
987
+ .catch(() => {});
964
988
  return () => {
965
989
  cancelled = true;
990
+ homeScreenMemoryRefreshVersionRef.current += 1;
966
991
  window.clearInterval(timer);
992
+ unlisten?.();
967
993
  };
968
- }, [featureConfig?.screenMemory?.enabled, popoverVisible]);
994
+ }, [
995
+ featureConfig?.screenMemory?.enabled,
996
+ popoverVisible,
997
+ refreshHomeScreenMemoryStatus,
998
+ ]);
969
999
  const recordShortcutHandlerRef = useRef<() => void>(() => {});
970
1000
  // Mirrors `bubbleActive` (assigned below once it is computed) so device
971
1001
  // probes can synchronously tell whether the camera bubble owns the grant.
@@ -3154,8 +3184,11 @@ export function App() {
3154
3184
  },
3155
3185
  },
3156
3186
  });
3157
- const status = await invoke<ScreenMemoryStatus>("screen_memory_status");
3158
- setHomeScreenMemoryStatus(status);
3187
+ // The native producer can take a moment to finish pausing. Do not keep
3188
+ // the Home switch locked while that status request waits; the existing
3189
+ // change event and bounded poll will reconcile it, and this best-effort
3190
+ // refresh can do the same without blocking the next resume action.
3191
+ refreshHomeScreenMemoryStatus();
3159
3192
  } catch (err) {
3160
3193
  console.error(
3161
3194
  "[clips-tray] update Rewind remembering state failed:",
@@ -3633,34 +3666,13 @@ export function App() {
3633
3666
  <section className="rewind-home-card" aria-label="Rewind status">
3634
3667
  <div className="rewind-home-status">
3635
3668
  <span
3636
- className={`rewind-home-dot ${
3637
- featureConfig?.screenMemory?.enabled === true &&
3638
- featureConfig.screenMemory.paused !== true &&
3639
- homeScreenMemoryStatus?.state === "recording"
3640
- ? "is-live"
3641
- : ""
3642
- }`}
3669
+ className={`rewind-home-dot ${homeRewindPresentation.isLive ? "is-live" : ""}`}
3643
3670
  />
3644
3671
  <div>
3645
- <strong>
3646
- {featureConfig?.screenMemory?.enabled !== true
3647
- ? "Rewind is off"
3648
- : featureConfig.screenMemory.paused === true
3649
- ? "Rewind is paused"
3650
- : homeScreenMemoryStatus?.exclusionActive
3651
- ? "Rewind is protecting a private moment"
3652
- : "Rewind is remembering"}
3653
- </strong>
3654
- {featureConfig?.screenMemory?.enabled !== true ||
3655
- featureConfig.screenMemory.paused ||
3656
- homeScreenMemoryStatus?.exclusionActive ? (
3657
- <p>
3658
- {featureConfig?.screenMemory?.enabled !== true
3659
- ? "Private memory for moments you may need later."
3660
- : featureConfig.screenMemory.paused
3661
- ? "Existing local memory is still available."
3662
- : "An excluded app is being skipped."}
3663
- </p>
3672
+ <strong>{homeRewindPresentation.title}</strong>
3673
+ {!homeRewindPresentation.isLive ||
3674
+ homeRewindPresentation.hasError ? (
3675
+ <p>{homeRewindPresentation.detail}</p>
3664
3676
  ) : null}
3665
3677
  </div>
3666
3678
  <div className="rewind-home-controls">
@@ -4941,6 +4953,7 @@ function Setup({
4941
4953
  );
4942
4954
  const [screenMemoryStatus, setScreenMemoryStatus] =
4943
4955
  useState<ScreenMemoryStatus | null>(null);
4956
+ const screenMemoryStatusRefreshVersionRef = useRef(0);
4944
4957
  const [screenMemoryMessage, setScreenMemoryMessage] = useState<{
4945
4958
  kind: "ok" | "error";
4946
4959
  text: string;
@@ -4976,7 +4989,11 @@ function Setup({
4976
4989
  (sum, segment) => sum + segment.bytes,
4977
4990
  0,
4978
4991
  );
4979
- const screenMemoryRecording = screenMemoryStatus?.state === "recording";
4992
+ const rewindStatusPresentation = getRewindStatusPresentation({
4993
+ status: screenMemoryStatus,
4994
+ config: screenMemory,
4995
+ clipRecordingActive: recordingActive,
4996
+ });
4980
4997
  const captureControlsLocked = recordingActive;
4981
4998
 
4982
4999
  useEffect(() => {
@@ -5172,11 +5189,16 @@ function Setup({
5172
5189
  }
5173
5190
  }
5174
5191
 
5175
- function refreshScreenMemoryStatus() {
5192
+ const refreshScreenMemoryStatus = useCallback(() => {
5193
+ const version = ++screenMemoryStatusRefreshVersionRef.current;
5176
5194
  invoke<ScreenMemoryStatus>("screen_memory_status")
5177
- .then(setScreenMemoryStatus)
5195
+ .then((status) => {
5196
+ if (version === screenMemoryStatusRefreshVersionRef.current) {
5197
+ setScreenMemoryStatus(status);
5198
+ }
5199
+ })
5178
5200
  .catch(() => {});
5179
- }
5201
+ }, []);
5180
5202
 
5181
5203
  function refreshRewindEgressLog() {
5182
5204
  invoke<RewindEgressEvent[]>("rewind_list_evidence_egress", { limit: 20 })
@@ -5257,6 +5279,7 @@ function Setup({
5257
5279
  const status = await invoke<ScreenMemoryStatus>(
5258
5280
  "screen_memory_delete_all",
5259
5281
  );
5282
+ screenMemoryStatusRefreshVersionRef.current += 1;
5260
5283
  setScreenMemoryStatus(status);
5261
5284
  setScreenMemoryMessage({ kind: "ok", text: "Rewind cleared." });
5262
5285
  } catch (err) {
@@ -5424,13 +5447,10 @@ function Setup({
5424
5447
  useEffect(() => {
5425
5448
  let cancelled = false;
5426
5449
  const refresh = () => {
5427
- invoke<ScreenMemoryStatus>("screen_memory_status")
5428
- .then((status) => {
5429
- if (!cancelled) setScreenMemoryStatus(status);
5430
- })
5431
- .catch(() => {});
5450
+ if (!cancelled) refreshScreenMemoryStatus();
5432
5451
  };
5433
5452
  refresh();
5453
+ const timer = window.setInterval(refresh, 5_000);
5434
5454
  const unlistens: Array<() => void> = [];
5435
5455
  const track = (p: Promise<() => void>) => {
5436
5456
  p.then((u) => {
@@ -5448,6 +5468,8 @@ function Setup({
5448
5468
  track(listen("clips:screen-memory-changed", refresh));
5449
5469
  return () => {
5450
5470
  cancelled = true;
5471
+ screenMemoryStatusRefreshVersionRef.current += 1;
5472
+ window.clearInterval(timer);
5451
5473
  unlistens.forEach((u) => {
5452
5474
  try {
5453
5475
  u();
@@ -5456,7 +5478,7 @@ function Setup({
5456
5478
  }
5457
5479
  });
5458
5480
  };
5459
- }, []);
5481
+ }, [refreshScreenMemoryStatus]);
5460
5482
 
5461
5483
  // Load model status on mount and keep it current via events.
5462
5484
  useEffect(() => {
@@ -6044,17 +6066,16 @@ function Setup({
6044
6066
  </div>
6045
6067
  <div className="rewind-settings-status">
6046
6068
  <span
6047
- className={`rewind-home-dot ${screenMemoryRecording ? "is-live" : ""}`}
6069
+ className={`rewind-home-dot ${rewindStatusPresentation.isLive ? "is-live" : ""}`}
6048
6070
  />
6049
- <span>
6050
- {screenMemoryStatus?.exclusionActive
6051
- ? screenMemoryStatus.coverage
6052
- : screenMemoryRecording
6071
+ <span className="rewind-settings-status-copy">
6072
+ <strong>{rewindStatusPresentation.title}</strong>
6073
+ <span>
6074
+ {rewindStatusPresentation.kind === "recording" &&
6075
+ !rewindStatusPresentation.hasError
6053
6076
  ? `${screenMemorySegments.length} retained segment${screenMemorySegments.length === 1 ? "" : "s"} · ${formatStorageBytes(screenMemoryTotalBytes)}`
6054
- : screenMemory.paused
6055
- ? "Paused. Existing memory remains available."
6056
- : screenMemoryStatus?.lastError ||
6057
- "Waiting for screen-recording permission."}
6077
+ : rewindStatusPresentation.detail}
6078
+ </span>
6058
6079
  </span>
6059
6080
  </div>
6060
6081
  <div className="setup-section-heading">Privacy</div>
@@ -6550,17 +6571,11 @@ function Setup({
6550
6571
  <div className="setup-section rewind-settings-entry">
6551
6572
  <div className="rewind-settings-entry-main">
6552
6573
  <span
6553
- className={`rewind-home-dot ${screenMemoryRecording ? "is-live" : ""}`}
6574
+ className={`rewind-home-dot ${rewindStatusPresentation.isLive ? "is-live" : ""}`}
6554
6575
  />
6555
6576
  <div>
6556
6577
  <strong>Rewind</strong>
6557
- <p className="setup-hint">
6558
- {!screenMemory.enabled
6559
- ? "Off"
6560
- : screenMemory.paused
6561
- ? "Paused · existing local memory is still available"
6562
- : "Remembering locally"}
6563
- </p>
6578
+ <p className="setup-hint">{rewindStatusPresentation.title}</p>
6564
6579
  </div>
6565
6580
  </div>
6566
6581
  <button type="button" className="secondary" onClick={onOpenRewind}>
@@ -6738,21 +6753,16 @@ function Setup({
6738
6753
  bundle ID cannot be detected.
6739
6754
  </p>
6740
6755
  <div className="whisper-status">
6741
- {screenMemoryRecording ? (
6756
+ {rewindStatusPresentation.isLive ? (
6742
6757
  <IconCircleCheck size={13} className="whisper-status-icon" />
6743
6758
  ) : (
6744
6759
  <IconAlertTriangle size={13} className="whisper-status-icon" />
6745
6760
  )}
6746
6761
  <span>
6747
- {screenMemoryStatus?.exclusionActive
6748
- ? screenMemoryStatus.coverage
6749
- : screenMemoryRecording
6750
- ? `Rewind is retaining local coverage: ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.`
6751
- : screenMemory.paused
6752
- ? "Capture paused. Existing local coverage remains available."
6753
- : screenMemoryStatus?.lastError
6754
- ? screenMemoryStatus.lastError
6755
- : "Capture status is waiting for screen-recording permission."}
6762
+ {rewindStatusPresentation.kind === "recording" &&
6763
+ !rewindStatusPresentation.hasError
6764
+ ? `Rewind is retaining local coverage: ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.`
6765
+ : rewindStatusPresentation.detail}
6756
6766
  </span>
6757
6767
  </div>
6758
6768
  {screenMemorySegments[0] ? (
@@ -828,14 +828,18 @@ export function useMeetingTranscription({
828
828
  );
829
829
 
830
830
  unlistens.push(
831
- listen<{ meetingId: string }>("clips:open-meeting", (ev) => {
832
- if (!ev.payload?.meetingId) return;
833
- openExternal(
834
- `${normalizedServerUrl}/meetings/${ev.payload.meetingId}`,
835
- ).catch((err) =>
836
- console.warn("[clips-popover] open meeting in web failed:", err),
837
- );
838
- }),
831
+ listen<{ meetingId: string; openChat?: boolean }>(
832
+ "clips:open-meeting",
833
+ (ev) => {
834
+ if (!ev.payload?.meetingId) return;
835
+ const query = ev.payload.openChat ? "?chat=1" : "";
836
+ openExternal(
837
+ `${normalizedServerUrl}/meetings/${ev.payload.meetingId}${query}`,
838
+ ).catch((err) =>
839
+ console.warn("[clips-popover] open meeting in web failed:", err),
840
+ );
841
+ },
842
+ ),
839
843
  );
840
844
 
841
845
  return () => {
@@ -0,0 +1,103 @@
1
+ import type { ScreenMemoryConfig, ScreenMemoryStatus } from "../shared/config";
2
+
3
+ export type RewindPresentationKind =
4
+ | "off"
5
+ | "paused"
6
+ | "recording"
7
+ | "excluded"
8
+ | "idle"
9
+ | "error"
10
+ | "unavailable";
11
+
12
+ export interface RewindStatusPresentation {
13
+ kind: RewindPresentationKind;
14
+ title: string;
15
+ detail: string;
16
+ isLive: boolean;
17
+ hasError: boolean;
18
+ }
19
+
20
+ export function getRewindStatusPresentation({
21
+ status,
22
+ config,
23
+ clipRecordingActive = false,
24
+ }: {
25
+ status: ScreenMemoryStatus | null;
26
+ config: ScreenMemoryConfig;
27
+ clipRecordingActive?: boolean;
28
+ }): RewindStatusPresentation {
29
+ const runtimeConfig = status?.config ?? config;
30
+
31
+ if (!runtimeConfig.enabled || status?.state === "disabled") {
32
+ return {
33
+ kind: "off",
34
+ title: "Rewind is off",
35
+ detail: "Private memory for moments you may need later.",
36
+ isLive: false,
37
+ hasError: false,
38
+ };
39
+ }
40
+
41
+ if (runtimeConfig.paused || status?.state === "paused") {
42
+ return {
43
+ kind: "paused",
44
+ title: "Rewind is paused",
45
+ detail: "Existing local memory is still available.",
46
+ isLive: false,
47
+ hasError: false,
48
+ };
49
+ }
50
+
51
+ if (status?.exclusionActive) {
52
+ return {
53
+ kind: "excluded",
54
+ title: "Rewind is protecting a private moment",
55
+ detail: status.coverage || "An excluded app is being skipped.",
56
+ isLive: false,
57
+ hasError: false,
58
+ };
59
+ }
60
+
61
+ if (status?.available === false) {
62
+ return {
63
+ kind: "unavailable",
64
+ title: "Rewind is unavailable",
65
+ detail: "Rewind capture is not available on this device.",
66
+ isLive: false,
67
+ hasError: false,
68
+ };
69
+ }
70
+
71
+ if (status?.state === "recording") {
72
+ return {
73
+ kind: "recording",
74
+ title: "Rewind is remembering",
75
+ detail:
76
+ status.lastError ||
77
+ status.coverage ||
78
+ "Rewind is retaining local coverage.",
79
+ isLive: true,
80
+ hasError: Boolean(status.lastError),
81
+ };
82
+ }
83
+
84
+ if (status?.lastError) {
85
+ return {
86
+ kind: "error",
87
+ title: "Rewind needs attention",
88
+ detail: status.lastError,
89
+ isLive: false,
90
+ hasError: true,
91
+ };
92
+ }
93
+
94
+ return {
95
+ kind: "idle",
96
+ title: "Rewind is enabled but not currently capturing",
97
+ detail: clipRecordingActive
98
+ ? "Rewind will resume when this Clip ends."
99
+ : "No new local coverage is being retained right now.",
100
+ isLive: false,
101
+ hasError: false,
102
+ };
103
+ }
@@ -9,25 +9,12 @@ export function PillLogo({ className }: { className?: string }) {
9
9
  >
10
10
  <path
11
11
  d="M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z"
12
- fill="white"
12
+ fill="currentColor"
13
13
  />
14
14
  <path
15
15
  d="M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z"
16
- fill="url(#pill-logo-grad)"
16
+ fill="currentColor"
17
17
  />
18
- <defs>
19
- <linearGradient
20
- id="pill-logo-grad"
21
- x1="101.702"
22
- y1="67.4791"
23
- x2="113.672"
24
- y2="-37.4275"
25
- gradientUnits="userSpaceOnUse"
26
- >
27
- <stop stopColor="#00B5FF" />
28
- <stop offset="1" stopColor="#48FFE4" />
29
- </linearGradient>
30
- </defs>
31
18
  </svg>
32
19
  );
33
20
  }