@agent-native/core 0.109.1 → 0.109.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 (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/integrations.mdx +87 -253
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/cli/create.ts +18 -12
  6. package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
  7. package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
  8. package/corpus/core/src/client/require-session.tsx +19 -3
  9. package/corpus/core/src/client/use-action.ts +145 -2
  10. package/corpus/core/src/client/use-session.ts +79 -48
  11. package/corpus/core/src/db/client.ts +13 -7
  12. package/corpus/core/src/db/create-get-db.ts +20 -13
  13. package/corpus/core/src/db/request-telemetry.ts +154 -0
  14. package/corpus/core/src/deploy/build.ts +63 -3
  15. package/corpus/core/src/integrations/index.ts +1 -0
  16. package/corpus/core/src/integrations/plugin.ts +9 -3
  17. package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
  18. package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
  19. package/corpus/core/src/integrations/remote-push-store.ts +275 -26
  20. package/corpus/core/src/server/better-auth-instance.ts +11 -3
  21. package/corpus/core/src/server/core-routes-plugin.ts +36 -14
  22. package/corpus/core/src/server/framework-request-handler.ts +11 -1
  23. package/corpus/core/src/server/http-response-telemetry.ts +233 -31
  24. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  25. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
  26. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
  27. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
  28. package/corpus/templates/clips/AGENTS.md +10 -0
  29. package/corpus/templates/clips/actions/create-dictation.ts +29 -4
  30. package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
  31. package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
  32. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +40 -0
  33. package/corpus/templates/clips/actions/update-dictation.ts +9 -1
  34. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
  35. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -2
  36. package/corpus/templates/clips/app/routes/share.$shareId.tsx +13 -2
  37. package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
  41. package/corpus/templates/clips/desktop/src/app.tsx +17 -0
  42. package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
  43. package/corpus/templates/clips/desktop/src/lib/recorder.ts +60 -35
  44. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +12 -0
  45. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  46. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +14 -0
  47. package/corpus/templates/clips/server/db/schema.ts +9 -1
  48. package/corpus/templates/clips/server/lib/builder-media-compression.ts +59 -7
  49. package/corpus/templates/clips/server/lib/media-verification-state.ts +17 -0
  50. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -0
  51. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
  52. package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
  53. package/corpus/templates/plan/shared/plan-content.ts +3 -0
  54. package/corpus/templates/tasks/AGENTS.md +3 -0
  55. package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
  56. package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
  57. package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
  58. package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
  59. package/dist/cli/create.d.ts +10 -1
  60. package/dist/cli/create.d.ts.map +1 -1
  61. package/dist/cli/create.js +17 -13
  62. package/dist/cli/create.js.map +1 -1
  63. package/dist/client/blocks/library/diagram.config.d.ts +2 -0
  64. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
  65. package/dist/client/blocks/library/diagram.config.js +8 -1
  66. package/dist/client/blocks/library/diagram.config.js.map +1 -1
  67. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  68. package/dist/client/blocks/library/diagram.js +9 -4
  69. package/dist/client/blocks/library/diagram.js.map +1 -1
  70. package/dist/client/require-session.d.ts.map +1 -1
  71. package/dist/client/require-session.js +6 -3
  72. package/dist/client/require-session.js.map +1 -1
  73. package/dist/client/use-action.d.ts.map +1 -1
  74. package/dist/client/use-action.js +117 -1
  75. package/dist/client/use-action.js.map +1 -1
  76. package/dist/client/use-session.d.ts.map +1 -1
  77. package/dist/client/use-session.js +72 -50
  78. package/dist/client/use-session.js.map +1 -1
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/db/client.d.ts.map +1 -1
  81. package/dist/db/client.js +7 -1
  82. package/dist/db/client.js.map +1 -1
  83. package/dist/db/create-get-db.d.ts +3 -6
  84. package/dist/db/create-get-db.d.ts.map +1 -1
  85. package/dist/db/create-get-db.js +9 -2
  86. package/dist/db/create-get-db.js.map +1 -1
  87. package/dist/db/request-telemetry.d.ts +23 -0
  88. package/dist/db/request-telemetry.d.ts.map +1 -0
  89. package/dist/db/request-telemetry.js +95 -0
  90. package/dist/db/request-telemetry.js.map +1 -0
  91. package/dist/deploy/build.d.ts +3 -0
  92. package/dist/deploy/build.d.ts.map +1 -1
  93. package/dist/deploy/build.js +45 -1
  94. package/dist/deploy/build.js.map +1 -1
  95. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  96. package/dist/integrations/index.d.ts +1 -0
  97. package/dist/integrations/index.d.ts.map +1 -1
  98. package/dist/integrations/index.js +1 -0
  99. package/dist/integrations/index.js.map +1 -1
  100. package/dist/integrations/plugin.d.ts.map +1 -1
  101. package/dist/integrations/plugin.js +8 -3
  102. package/dist/integrations/plugin.js.map +1 -1
  103. package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
  104. package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
  105. package/dist/integrations/remote-push-delivery-job.js +44 -0
  106. package/dist/integrations/remote-push-delivery-job.js.map +1 -0
  107. package/dist/integrations/remote-push-delivery.d.ts +11 -0
  108. package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
  109. package/dist/integrations/remote-push-delivery.js +265 -0
  110. package/dist/integrations/remote-push-delivery.js.map +1 -0
  111. package/dist/integrations/remote-push-store.d.ts +33 -0
  112. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  113. package/dist/integrations/remote-push-store.js +195 -26
  114. package/dist/integrations/remote-push-store.js.map +1 -1
  115. package/dist/observability/routes.d.ts +5 -5
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/server/better-auth-instance.js +7 -3
  119. package/dist/server/better-auth-instance.js.map +1 -1
  120. package/dist/server/core-routes-plugin.d.ts +8 -0
  121. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  122. package/dist/server/core-routes-plugin.js +23 -8
  123. package/dist/server/core-routes-plugin.js.map +1 -1
  124. package/dist/server/framework-request-handler.d.ts.map +1 -1
  125. package/dist/server/framework-request-handler.js +9 -1
  126. package/dist/server/framework-request-handler.js.map +1 -1
  127. package/dist/server/http-response-telemetry.d.ts +3 -2
  128. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  129. package/dist/server/http-response-telemetry.js +160 -27
  130. package/dist/server/http-response-telemetry.js.map +1 -1
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  133. package/docs/content/integrations.mdx +87 -253
  134. package/package.json +1 -1
  135. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
@@ -384,12 +384,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
384
384
  ) : (
385
385
  <>
386
386
  <div className="px-3 py-3">
387
- <Button
388
- className="w-full gap-1.5"
389
- variant="outline"
390
- size="sm"
391
- asChild
392
- >
387
+ <Button className="w-full gap-1.5" size="sm" asChild>
393
388
  <NavLink to="/record">
394
389
  <IconPlayerRecord className="h-4 w-4" />
395
390
  {t("navigation.newRecording")}
@@ -376,6 +376,7 @@ export default function RecordingPage() {
376
376
  }, [recordingId, playerDataForbidden, navigate]);
377
377
 
378
378
  const recording = playerDataQ.data?.recording;
379
+ const verificationPending = recording?.verificationPending === true;
379
380
  const role = playerDataQ.data?.role as
380
381
  | "owner"
381
382
  | "admin"
@@ -781,13 +782,22 @@ export default function RecordingPage() {
781
782
  setProcessingTimeout(false);
782
783
  return;
783
784
  }
785
+ if (verificationPending) {
786
+ setProcessingTimeout(false);
787
+ return;
788
+ }
784
789
  const timeoutMs =
785
790
  recording.status === "processing"
786
791
  ? PROCESSING_STUCK_TIMEOUT_MS
787
792
  : UPLOAD_STUCK_TIMEOUT_MS;
788
793
  const handle = setTimeout(() => setProcessingTimeout(true), timeoutMs);
789
794
  return () => clearTimeout(handle);
790
- }, [recording?.status, recording?.videoUrl, recordingId]);
795
+ }, [
796
+ recording?.status,
797
+ recording?.videoUrl,
798
+ recordingId,
799
+ verificationPending,
800
+ ]);
791
801
 
792
802
  usePlayerShortcuts({ playerRef, chapters });
793
803
 
@@ -873,7 +883,8 @@ export default function RecordingPage() {
873
883
  isNativeSaveFailureReason(rawFailureReason);
874
884
  // Give a long-running desktop save an actionable recovery state without
875
885
  // claiming the upload failed while its bounded final request is still live.
876
- const stuckFailure = !explicitFailure && processingTimeout;
886
+ const stuckFailure =
887
+ !explicitFailure && !verificationPending && processingTimeout;
877
888
  const isFailure = explicitFailure || waitingForStorage || nativeSaveFailed;
878
889
  const showRecoveryState = isFailure || stuckFailure;
879
890
  const displayReason = explicitFailure
@@ -478,6 +478,7 @@ export default function ShareRoute() {
478
478
  });
479
479
 
480
480
  const recording = dataQ.data?.data?.recording;
481
+ const verificationPending = recording?.verificationPending === true;
481
482
  const comments = dataQ.data?.data?.comments ?? [];
482
483
  const reactions = dataQ.data?.data?.reactions ?? [];
483
484
  const chapters = dataQ.data?.data?.chapters ?? [];
@@ -560,6 +561,10 @@ export default function ShareRoute() {
560
561
  setProcessingTimeout(false);
561
562
  return;
562
563
  }
564
+ if (verificationPending) {
565
+ setProcessingTimeout(false);
566
+ return;
567
+ }
563
568
 
564
569
  const timeoutMs =
565
570
  recording.status === "processing"
@@ -567,7 +572,12 @@ export default function ShareRoute() {
567
572
  : UPLOAD_STUCK_TIMEOUT_MS;
568
573
  const handle = setTimeout(() => setProcessingTimeout(true), timeoutMs);
569
574
  return () => clearTimeout(handle);
570
- }, [recording?.id, recording?.status, recording?.videoUrl]);
575
+ }, [
576
+ recording?.id,
577
+ recording?.status,
578
+ recording?.videoUrl,
579
+ verificationPending,
580
+ ]);
571
581
 
572
582
  usePlayerShortcuts({ playerRef });
573
583
 
@@ -712,7 +722,8 @@ export default function ShareRoute() {
712
722
  const storageSetupFailure = isStorageSetupFailureReason(rawFailureReason);
713
723
  const loomStorageSetupFailure =
714
724
  storageSetupFailure && isLoomRecordingSource(recording);
715
- const stuckFailure = !explicitFailure && processingTimeout;
725
+ const stuckFailure =
726
+ !explicitFailure && !verificationPending && processingTimeout;
716
727
  const isFailure = explicitFailure || storageSetupFailure || stuckFailure;
717
728
  const canManageStorage = viewerCanEdit;
718
729
  const signInHref = buildSignInHref(`/r/${recording.id}`);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Capture dictation, background meeting audio, and camera videos from the Agent Native iOS and Android app with automatic recovery and resumable Clips sync.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Delayed upload verification now keeps local backups retryable and avoids showing a false processing failure.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-18
4
+ ---
5
+
6
+ New recording is now emphasized as the primary action in the library sidebar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-18
4
+ ---
5
+
6
+ Recordings now remain safely recoverable until the stored media size is verified.
@@ -1835,6 +1835,23 @@ export function App() {
1835
1835
  );
1836
1836
  }, []);
1837
1837
 
1838
+ useEffect(() => {
1839
+ let unlisten: (() => void) | null = null;
1840
+ let cancelled = false;
1841
+ listen("clips:pending-uploads-changed", () => {
1842
+ void loadPendingUploads();
1843
+ })
1844
+ .then((stop) => {
1845
+ if (cancelled) stop();
1846
+ else unlisten = stop;
1847
+ })
1848
+ .catch(() => {});
1849
+ return () => {
1850
+ cancelled = true;
1851
+ unlisten?.();
1852
+ };
1853
+ }, [loadPendingUploads]);
1854
+
1838
1855
  useEffect(() => {
1839
1856
  if (popoverView === "meetings" && popoverVisible) {
1840
1857
  void fetchUpcomingMeetings();
@@ -0,0 +1,19 @@
1
+ export async function reconcileProcessingBackup(args: {
2
+ waitForReady: () => Promise<{ status?: string } | null>;
3
+ onReady: () => Promise<unknown>;
4
+ onUnresolved: () => Promise<unknown>;
5
+ onPollError?: (error: unknown) => void;
6
+ }): Promise<"ready" | "unresolved"> {
7
+ try {
8
+ const recovered = await args.waitForReady();
9
+ if (recovered?.status === "ready") {
10
+ await args.onReady();
11
+ return "ready";
12
+ }
13
+ } catch (error) {
14
+ args.onPollError?.(error);
15
+ }
16
+
17
+ await args.onUnresolved();
18
+ return "unresolved";
19
+ }
@@ -78,6 +78,7 @@ import {
78
78
  createPauseTransitionQueue,
79
79
  type PauseTransitionQueue,
80
80
  } from "./pause-transition";
81
+ import { reconcileProcessingBackup } from "./processing-backup-recovery";
81
82
  import { buildCaptureTitle, type CaptureTitleResult } from "./recording-title";
82
83
  import { singleFlight } from "./single-flight";
83
84
  import {
@@ -86,6 +87,7 @@ import {
86
87
  type TranscriptionCapture,
87
88
  } from "./transcription-capture";
88
89
  import {
90
+ parseFinalizeReceipt,
89
91
  verifyFinalizeReceipt,
90
92
  type FinalizeReceipt,
91
93
  } from "./upload-verification";
@@ -744,6 +746,7 @@ export async function dismissBrowserRecordingBackup(
744
746
  async function markBrowserRecordingBackupError(
745
747
  recordingId: string,
746
748
  error: string,
749
+ incrementRetryCount = true,
747
750
  ): Promise<void> {
748
751
  const meta = await getBrowserRecordingBackupMeta(recordingId);
749
752
  if (!meta) return;
@@ -751,7 +754,26 @@ async function markBrowserRecordingBackupError(
751
754
  ...meta,
752
755
  lastAttemptAt: new Date().toISOString(),
753
756
  lastError: error,
754
- retryCount: meta.retryCount + 1,
757
+ retryCount: incrementRetryCount ? meta.retryCount + 1 : meta.retryCount,
758
+ });
759
+ }
760
+
761
+ const MEDIA_VERIFICATION_BACKUP_ERROR =
762
+ "The server could not finish verifying this upload. The local copy is still saved; retry to continue.";
763
+
764
+ async function flagBrowserBackupAfterProcessing(recordingId: string) {
765
+ await markBrowserRecordingBackupError(
766
+ recordingId,
767
+ MEDIA_VERIFICATION_BACKUP_ERROR,
768
+ false,
769
+ );
770
+ await emit("clips:pending-uploads-changed").catch(() => {});
771
+ }
772
+
773
+ async function flagNativeBackupAfterProcessing(recordingId: string) {
774
+ await invoke("native_fullscreen_recording_mark_upload_error", {
775
+ recordingId,
776
+ error: MEDIA_VERIFICATION_BACKUP_ERROR,
755
777
  });
756
778
  }
757
779
 
@@ -760,24 +782,26 @@ function scheduleBrowserBackupCleanupAfterProcessing(args: {
760
782
  recordingId: string;
761
783
  authToken?: string;
762
784
  }): void {
763
- void waitForReadyRecordingAfterFinalizeError({
764
- uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
765
- recordingId: args.recordingId,
766
- authToken: args.authToken,
767
- preferAuthenticated: true,
768
- timeoutMs: 12 * 60 * 1000,
769
- })
770
- .then((recovered) => {
771
- if (recovered?.status === "ready") {
772
- return deleteBrowserRecordingBackup(args.recordingId);
773
- }
774
- })
775
- .catch((err) => {
785
+ void reconcileProcessingBackup({
786
+ waitForReady: () =>
787
+ waitForReadyRecordingAfterFinalizeError({
788
+ uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
789
+ recordingId: args.recordingId,
790
+ authToken: args.authToken,
791
+ preferAuthenticated: true,
792
+ timeoutMs: 12 * 60 * 1000,
793
+ }),
794
+ onReady: () => deleteBrowserRecordingBackup(args.recordingId),
795
+ onUnresolved: () => flagBrowserBackupAfterProcessing(args.recordingId),
796
+ onPollError: (err) => {
776
797
  console.warn(
777
798
  "[clips-recorder] background backup cleanup check failed:",
778
799
  err,
779
800
  );
780
- });
801
+ },
802
+ }).catch((err) => {
803
+ console.warn("[clips-recorder] background backup flag failed:", err);
804
+ });
781
805
  }
782
806
 
783
807
  export function scheduleNativeBackupCleanupAfterProcessing(args: {
@@ -785,23 +809,26 @@ export function scheduleNativeBackupCleanupAfterProcessing(args: {
785
809
  recordingId: string;
786
810
  authToken?: string;
787
811
  }): void {
788
- void waitForReadyRecordingAfterFinalizeError({
789
- uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
790
- recordingId: args.recordingId,
791
- authToken: args.authToken,
792
- preferAuthenticated: true,
793
- timeoutMs: 12 * 60 * 1000,
794
- })
795
- .then((recovered) => {
796
- if (recovered?.status === "ready") {
797
- return invoke("native_fullscreen_recording_dismiss_upload", {
798
- recordingId: args.recordingId,
799
- });
800
- }
801
- })
802
- .catch((err) => {
812
+ void reconcileProcessingBackup({
813
+ waitForReady: () =>
814
+ waitForReadyRecordingAfterFinalizeError({
815
+ uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
816
+ recordingId: args.recordingId,
817
+ authToken: args.authToken,
818
+ preferAuthenticated: true,
819
+ timeoutMs: 12 * 60 * 1000,
820
+ }),
821
+ onReady: () =>
822
+ invoke("native_fullscreen_recording_dismiss_upload", {
823
+ recordingId: args.recordingId,
824
+ }),
825
+ onUnresolved: () => flagNativeBackupAfterProcessing(args.recordingId),
826
+ onPollError: (err) => {
803
827
  console.warn("[clips-recorder] native backup cleanup check failed:", err);
804
- });
828
+ },
829
+ }).catch((err) => {
830
+ console.warn("[clips-recorder] native backup flag failed:", err);
831
+ });
805
832
  }
806
833
 
807
834
  async function recoverAcceptedRecordingAfterFinalizeError({
@@ -884,7 +911,7 @@ async function postBackupChunk(
884
911
  `Upload retry failed (${res.status}): ${body.slice(0, 200)}`,
885
912
  );
886
913
  }
887
- return body ? (JSON.parse(body) as FinalizeReceipt) : null;
914
+ return parseFinalizeReceipt(body);
888
915
  }
889
916
 
890
917
  async function resetBrowserRecordingBackupUpload(
@@ -3965,9 +3992,7 @@ async function startRecordingInner(
3965
3992
  `Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
3966
3993
  );
3967
3994
  }
3968
- const receipt = bodyText
3969
- ? (JSON.parse(bodyText) as FinalizeReceipt)
3970
- : null;
3995
+ const receipt = parseFinalizeReceipt(bodyText);
3971
3996
  const receiptStatus = verifyFinalizeReceipt(receipt, {
3972
3997
  bytes: backupBytes,
3973
3998
  durationMs,
@@ -14,6 +14,18 @@ export interface LocalRecordingProof {
14
14
 
15
15
  export type FinalizeReceiptStatus = "processing" | "ready";
16
16
 
17
+ export function parseFinalizeReceipt(body: string): FinalizeReceipt | null {
18
+ if (!body) return null;
19
+ try {
20
+ const parsed: unknown = JSON.parse(body);
21
+ return parsed && typeof parsed === "object"
22
+ ? (parsed as FinalizeReceipt)
23
+ : null;
24
+ } catch {
25
+ throw new Error("Upload returned an invalid finalization response");
26
+ }
27
+ }
28
+
17
29
  export function verifyFinalizeReceipt(
18
30
  receipt: FinalizeReceipt | null,
19
31
  local: LocalRecordingProof,
@@ -141,6 +141,7 @@ pub fn run() {
141
141
  native_screen::native_fullscreen_recording_rotate_segment,
142
142
  native_screen::native_fullscreen_pending_uploads,
143
143
  native_screen::native_fullscreen_recording_retry_upload,
144
+ native_screen::native_fullscreen_recording_mark_upload_error,
144
145
  native_screen::native_fullscreen_recording_dismiss_upload,
145
146
  native_screen::native_fullscreen_open_drafts_folder,
146
147
  // local-only always-on screen memory compatibility helpers
@@ -2626,6 +2626,20 @@ pub async fn native_fullscreen_recording_retry_upload(
2626
2626
  }
2627
2627
  }
2628
2628
 
2629
+ #[tauri::command]
2630
+ pub async fn native_fullscreen_recording_mark_upload_error(
2631
+ app: AppHandle,
2632
+ recording_id: String,
2633
+ error: String,
2634
+ ) -> Result<(), String> {
2635
+ let mut saved = read_saved_recording_metadata(&app, &recording_id)?;
2636
+ saved.last_attempt_at = Some(now_iso());
2637
+ saved.last_error = Some(error);
2638
+ write_saved_recording_metadata(&app, &saved)?;
2639
+ let _ = app.emit("clips:pending-uploads-changed", ());
2640
+ Ok(())
2641
+ }
2642
+
2629
2643
  #[tauri::command]
2630
2644
  pub async fn native_fullscreen_recording_dismiss_upload(
2631
2645
  app: AppHandle,
@@ -586,7 +586,15 @@ export const dictations = table("clips_dictations", {
586
586
  // are text-only since native recognition runs on-device.
587
587
  audioUrl: text("audio_url"),
588
588
  source: text("source", {
589
- enum: ["fn-hold", "cmd-shift-space", "manual", "other", "fn", "custom"],
589
+ enum: [
590
+ "fn-hold",
591
+ "cmd-shift-space",
592
+ "manual",
593
+ "mobile",
594
+ "other",
595
+ "fn",
596
+ "custom",
597
+ ],
590
598
  })
591
599
  .notNull()
592
600
  .default("fn-hold"),
@@ -9,7 +9,7 @@ import {
9
9
  resolveBuilderPrivateKey,
10
10
  runWithRequestContext,
11
11
  } from "@agent-native/core/server";
12
- import { and, eq } from "drizzle-orm";
12
+ import { and, eq, isNull } from "drizzle-orm";
13
13
 
14
14
  import type { MediaWorkerCallback } from "../../shared/media-worker-contract.js";
15
15
  import { getDb, schema } from "../db/index.js";
@@ -958,7 +958,12 @@ export async function runBuilderMediaCompressionSweepOnce(): Promise<void> {
958
958
  key?: unknown;
959
959
  value?: unknown;
960
960
  }>) {
961
- const sessionId = typeof row.session_id === "string" ? row.session_id : "";
961
+ const sessionId =
962
+ typeof row.session_id === "string" ? row.session_id.trim() : "";
963
+ const key = typeof row.key === "string" ? row.key : "";
964
+ const recordingId = key.startsWith(BUILDER_MEDIA_COMPRESSION_STATE_PREFIX)
965
+ ? key.slice(BUILDER_MEDIA_COMPRESSION_STATE_PREFIX.length)
966
+ : "";
962
967
  const rawValue = typeof row.value === "string" ? row.value : "";
963
968
  let value: Record<string, unknown> | null = null;
964
969
  try {
@@ -967,14 +972,61 @@ export async function runBuilderMediaCompressionSweepOnce(): Promise<void> {
967
972
  continue;
968
973
  }
969
974
  const state = parseState(value);
970
- if (!state || isFinalStatus(state.status) || !isDue(state)) continue;
971
- const ownerEmail = state.ownerEmail || sessionId;
972
- if (!ownerEmail) continue;
975
+ if (
976
+ !sessionId ||
977
+ !recordingId ||
978
+ !state ||
979
+ state.recordingId !== recordingId ||
980
+ isFinalStatus(state.status) ||
981
+ !isDue(state)
982
+ ) {
983
+ continue;
984
+ }
973
985
  try {
986
+ const [recording] = await getDb()
987
+ .select({
988
+ ownerEmail: schema.recordings.ownerEmail,
989
+ orgId: schema.recordings.orgId,
990
+ videoUrl: schema.recordings.videoUrl,
991
+ })
992
+ .from(schema.recordings)
993
+ .where(
994
+ and(
995
+ eq(schema.recordings.id, recordingId),
996
+ ownerEmailMatches(schema.recordings.ownerEmail, sessionId),
997
+ eq(schema.recordings.status, "ready"),
998
+ isNull(schema.recordings.trashedAt),
999
+ ),
1000
+ )
1001
+ .limit(1);
1002
+ const expectedTarget = extractBuilderMediaTarget(
1003
+ recording?.videoUrl ?? "",
1004
+ );
1005
+ if (
1006
+ !recording ||
1007
+ !expectedTarget ||
1008
+ state.sourceUrl !== expectedTarget.sourceUrl ||
1009
+ (state.mediaWorker &&
1010
+ (state.mediaWorker.jobId !==
1011
+ mediaWorkerCompressionJobId(recordingId) ||
1012
+ state.mediaWorker.outputUrl !== expectedTarget.compressedUrl))
1013
+ ) {
1014
+ continue;
1015
+ }
1016
+
1017
+ const trustedState: BuilderMediaCompressionState = {
1018
+ ...state,
1019
+ ...expectedTarget,
1020
+ ownerEmail: recording.ownerEmail,
1021
+ orgId: recording.orgId,
1022
+ };
974
1023
  await runWithRequestContext(
975
- { userEmail: ownerEmail, orgId: state.orgId ?? undefined },
1024
+ {
1025
+ userEmail: recording.ownerEmail,
1026
+ orgId: recording.orgId ?? undefined,
1027
+ },
976
1028
  async () => {
977
- await processCompressionState({ ...state, ownerEmail });
1029
+ await processCompressionState(trustedState);
978
1030
  },
979
1031
  );
980
1032
  } catch (err) {
@@ -1,3 +1,5 @@
1
+ import { appStateGet } from "@agent-native/core/application-state";
2
+
1
3
  export const MEDIA_VERIFICATION_STATE_PREFIX = "recording-media-verification-";
2
4
 
3
5
  export type MediaVerificationMarker = {
@@ -36,3 +38,18 @@ export function parseMediaVerificationMarker(
36
38
  }
37
39
  return state as MediaVerificationMarker;
38
40
  }
41
+
42
+ export async function isMediaVerificationPending(args: {
43
+ ownerEmail: string;
44
+ recordingId: string;
45
+ recordingStatus: string;
46
+ }): Promise<boolean> {
47
+ if (args.recordingStatus !== "processing") return false;
48
+
49
+ const value = await appStateGet(
50
+ args.ownerEmail,
51
+ mediaVerificationStateKey(args.recordingId),
52
+ ).catch(() => null);
53
+ const marker = parseMediaVerificationMarker(value);
54
+ return marker?.recordingId === args.recordingId;
55
+ }
@@ -42,6 +42,7 @@ import {
42
42
  } from "../../../shared/transcript-segments.js";
43
43
  import { resolveTranscriptPresentation } from "../../../shared/transcript-status.js";
44
44
  import { getDb, schema } from "../../db/index.js";
45
+ import { isMediaVerificationPending } from "../../lib/media-verification-state.js";
45
46
  import { resolvePlayerThumbnailUrl } from "../../lib/player-thumbnail-url.js";
46
47
  import { resolvePlayerVideoUrl } from "../../lib/player-video-url.js";
47
48
  import {
@@ -398,6 +399,11 @@ export default defineEventHandler(async (event) => {
398
399
  // Referer of any outbound link the share page renders.
399
400
  setResponseHeader(event, "Referrer-Policy", "no-referrer");
400
401
  const transcriptPresentation = resolveTranscriptPresentation(transcript);
402
+ const verificationPending = await isMediaVerificationPending({
403
+ ownerEmail: rec.ownerEmail,
404
+ recordingId,
405
+ recordingStatus: rec.status,
406
+ });
401
407
 
402
408
  return {
403
409
  recording: {
@@ -416,6 +422,7 @@ export default defineEventHandler(async (event) => {
416
422
  hasAudio: Boolean(rec.hasAudio),
417
423
  hasCamera: Boolean(rec.hasCamera),
418
424
  status: rec.status,
425
+ verificationPending,
419
426
  uploadProgress: rec.uploadProgress,
420
427
  failureReason: rec.failureReason,
421
428
  // Don't leak the password to clients; just indicate whether one was set.
@@ -42,6 +42,7 @@ import {
42
42
  } from "h3";
43
43
 
44
44
  import { getDb, schema } from "../../../../db/index.js";
45
+ import { isMediaVerificationPending } from "../../../../lib/media-verification-state.js";
45
46
  import {
46
47
  getEventOwnerContext,
47
48
  ownerEmailMatches,
@@ -115,7 +116,11 @@ export default defineEventHandler(async (event: H3Event) => {
115
116
  const db = getDb();
116
117
 
117
118
  const [existing] = await db
118
- .select({ id: schema.recordings.id })
119
+ .select({
120
+ id: schema.recordings.id,
121
+ status: schema.recordings.status,
122
+ videoUrl: schema.recordings.videoUrl,
123
+ })
119
124
  .from(schema.recordings)
120
125
  .where(
121
126
  and(
@@ -129,6 +134,22 @@ export default defineEventHandler(async (event: H3Event) => {
129
134
  return { error: "Recording not found" };
130
135
  }
131
136
 
137
+ if (existing.status === "ready" && existing.videoUrl) {
138
+ setResponseStatus(event, 409);
139
+ return { error: "Recording is already ready" };
140
+ }
141
+
142
+ if (
143
+ await isMediaVerificationPending({
144
+ ownerEmail,
145
+ recordingId,
146
+ recordingStatus: existing.status,
147
+ })
148
+ ) {
149
+ setResponseStatus(event, 409);
150
+ return { error: "Recording is still being verified" };
151
+ }
152
+
132
153
  const cleared = await deleteAppStateByPrefix(
133
154
  `recording-chunks-${recordingId}-`,
134
155
  );
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Diagrams can opt into a polished clean rendering mode without sketch styling.
@@ -367,6 +367,8 @@ export type PlanDiagramBlock = PlanBlockBase & {
367
367
  */
368
368
  html?: string;
369
369
  css?: string;
370
+ /** `design` forces clean HTML/CSS rendering without the sketch overlay. */
371
+ renderMode?: PlanVisualCanvasMode;
370
372
  caption?: string;
371
373
  /** Outer surface frame. `auto` lets the host choose the right default. */
372
374
  frame?: PlanVisualFrame;
@@ -1396,6 +1398,7 @@ const diagramDataSchema: z.ZodType<PlanDiagramBlock["data"]> = z
1396
1398
  message: "Diagram css must not include document or script tags.",
1397
1399
  })
1398
1400
  .optional(),
1401
+ renderMode: visualCanvasModeSchema.optional(),
1399
1402
  caption: z.string().trim().max(600).optional(),
1400
1403
  frame: visualFrameSchema.optional(),
1401
1404
  nodes: z.array(diagramNodeSchema).max(80).optional(),
@@ -38,6 +38,7 @@ Tasks is a task-list-first agent-native app. The task list at `/tasks` is the de
38
38
  | `update-visible-task-fields` | POST | Replace which custom fields appear on task cards (max 3) |
39
39
  | `view-screen` | — | Read navigation, UI bulk selection, visible tasks, and inbox snapshot |
40
40
  | `navigate` | — | Move UI to a view: `tasks`, `inbox`, `fields`, `extensions`, `team` (`home`/`ask` → `tasks`) |
41
+ | `render-task-list-inline` | — | Render an interactive task-list widget inline in chat without leaving the current view |
41
42
 
42
43
  ## Store Functions And Transactions
43
44
 
@@ -128,6 +129,8 @@ Default navigation shape on `/tasks`:
128
129
  - Capture in chat → `create-inbox-item` by default; use `create-task` only when the user asks to add directly to the task list.
129
130
  - Call `view-screen` before ambiguous edits when the user says "this task", "these tasks", "this inbox item", or "the list".
130
131
  - On `/tasks` or `/inbox`, `view-screen` returns `list` (with `items`), optional `selectedItem` (`inListSnapshot`), and optional `selection` (`selectedItems`, `selectedIdsNotInVisibleList`) when bulk-select is active.
132
+ - When the user asks to see, review, or manage tasks while `navigation.view` is not `tasks`, call `render-task-list-inline` instead of navigating away. Pass `includeDone: true` when completed tasks should be included. The widget can add tasks and toggle completion through the existing task actions.
133
+ - When the user is already on `/tasks`, use `view-screen` and the native task list for task-list context unless the user explicitly asks for an inline widget.
131
134
  - Prefer `selection.selectedItems` when the user has UI rows selected; fall back to `selectedItem` for a single deep-link highlight.
132
135
  - `delete-task`, `bulk-delete-tasks`, and `delete-inbox-item` only after explicit user confirmation in chat.
133
136
  - Use `navigation.includeDone` and `list` from `view-screen` to match what the user sees on `/tasks`.