@agent-native/core 0.114.6 → 0.114.8

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 (155) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  11. package/corpus/core/src/mcp-client/index.ts +9 -0
  12. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  13. package/corpus/core/src/server/agent-chat-plugin.ts +16 -9
  14. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +19 -7
  15. package/corpus/templates/analytics/app/hooks/use-chart-tooltip-portal.ts +60 -0
  16. package/corpus/templates/analytics/app/routes/chart.tsx +1 -1
  17. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-requests-no-longer-stall-without-progress.md +6 -0
  18. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
  19. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  20. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  21. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  22. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  23. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  24. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  25. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  26. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  27. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  28. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  29. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  30. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  31. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  32. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  33. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  34. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  35. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  36. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  37. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  38. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  45. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  50. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  53. package/corpus/templates/clips/desktop/package.json +1 -0
  54. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  55. package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
  56. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  57. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  58. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  59. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  60. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  61. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  62. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  63. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  64. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  65. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  66. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  67. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  68. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  69. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  70. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  71. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  72. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  73. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  74. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  76. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  77. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  78. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  79. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  80. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  82. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  83. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  89. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  91. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  92. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  93. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  94. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  96. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  97. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  98. package/corpus/toolkit/CHANGELOG.md +6 -0
  99. package/corpus/toolkit/package.json +1 -1
  100. package/dist/cli/mcp-config-writers.d.ts +2 -2
  101. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  102. package/dist/cli/mcp-config-writers.js +6 -6
  103. package/dist/cli/mcp-config-writers.js.map +1 -1
  104. package/dist/cli/mcp.d.ts +14 -0
  105. package/dist/cli/mcp.d.ts.map +1 -1
  106. package/dist/cli/mcp.js +79 -0
  107. package/dist/cli/mcp.js.map +1 -1
  108. package/dist/cli/skills-content/help.d.ts +1 -1
  109. package/dist/cli/skills-content/help.d.ts.map +1 -1
  110. package/dist/cli/skills-content/help.js +4 -3
  111. package/dist/cli/skills-content/help.js.map +1 -1
  112. package/dist/cli/skills-content/index.d.ts +1 -0
  113. package/dist/cli/skills-content/index.d.ts.map +1 -1
  114. package/dist/cli/skills-content/index.js +1 -0
  115. package/dist/cli/skills-content/index.js.map +1 -1
  116. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  117. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  118. package/dist/cli/skills-content/rewind-skill.js +53 -0
  119. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  120. package/dist/cli/skills.d.ts +6 -0
  121. package/dist/cli/skills.d.ts.map +1 -1
  122. package/dist/cli/skills.js +90 -14
  123. package/dist/cli/skills.js.map +1 -1
  124. package/dist/collab/struct-routes.d.ts +1 -1
  125. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  126. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  127. package/dist/mcp/screen-memory-stdio.js +1014 -91
  128. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  129. package/dist/mcp-client/index.d.ts +1 -1
  130. package/dist/mcp-client/index.d.ts.map +1 -1
  131. package/dist/mcp-client/index.js +1 -1
  132. package/dist/mcp-client/index.js.map +1 -1
  133. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  134. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  135. package/dist/mcp-client/screen-memory-local.js +590 -12
  136. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  137. package/dist/observability/routes.d.ts +1 -1
  138. package/dist/progress/routes.d.ts +1 -1
  139. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  140. package/dist/resources/handlers.d.ts +1 -1
  141. package/dist/server/agent-chat-plugin.d.ts +5 -0
  142. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  143. package/dist/server/agent-chat-plugin.js +10 -9
  144. package/dist/server/agent-chat-plugin.js.map +1 -1
  145. package/package.json +2 -2
  146. package/src/cli/mcp-config-writers.ts +6 -4
  147. package/src/cli/mcp.ts +124 -0
  148. package/src/cli/skills-content/help.ts +4 -3
  149. package/src/cli/skills-content/index.ts +1 -0
  150. package/src/cli/skills-content/rewind-skill.ts +52 -0
  151. package/src/cli/skills.ts +115 -13
  152. package/src/mcp/screen-memory-stdio.ts +1360 -104
  153. package/src/mcp-client/index.ts +9 -0
  154. package/src/mcp-client/screen-memory-local.ts +847 -13
  155. package/src/server/agent-chat-plugin.ts +16 -9
@@ -0,0 +1,81 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ readAppState,
4
+ writeAppState,
5
+ } from "@agent-native/core/application-state";
6
+ import { assertAccess } from "@agent-native/core/sharing";
7
+ import { eq } from "drizzle-orm";
8
+ import { z } from "zod";
9
+
10
+ import { isPrivateClip } from "../app/lib/rewind-visibility.js";
11
+ import { getDb, schema } from "../server/db/index.js";
12
+ import { nanoid } from "../server/lib/recordings.js";
13
+ import { assertNoDirectRecordingShares } from "./make-recording-private-for-rewind.js";
14
+
15
+ export const REWIND_EXTENSION_PREFIX = "rewind-extension-request-";
16
+
17
+ export interface RewindExtensionRequest {
18
+ [key: string]: unknown;
19
+ requestId: string;
20
+ recordingId: string;
21
+ seconds: 30 | 300;
22
+ status:
23
+ | "pending"
24
+ | "processing"
25
+ | "ready"
26
+ | "applying"
27
+ | "applied"
28
+ | "failed";
29
+ requestedAt: string;
30
+ updatedAt: string;
31
+ preRollRecordingId?: string;
32
+ actualDurationMs?: number;
33
+ error?: string;
34
+ }
35
+
36
+ export function rewindExtensionKey(recordingId: string): string {
37
+ return `${REWIND_EXTENSION_PREFIX}${recordingId}`;
38
+ }
39
+
40
+ export default defineAction({
41
+ description:
42
+ "Ask the signed-in Clips Alpha app to retrieve an explicit 30-second or five-minute pre-roll from local Rewind for one owned recording.",
43
+ schema: z.object({
44
+ recordingId: z.string(),
45
+ seconds: z.union([z.literal(30), z.literal(300)]),
46
+ }),
47
+ run: async ({ recordingId, seconds }) => {
48
+ await assertAccess("recording", recordingId, "owner");
49
+ const [recording] = await getDb()
50
+ .select({ visibility: schema.recordings.visibility })
51
+ .from(schema.recordings)
52
+ .where(eq(schema.recordings.id, recordingId));
53
+ if (!recording || !isPrivateClip(recording.visibility)) {
54
+ throw new Error(
55
+ "Make this Clip private before adding local Rewind history.",
56
+ );
57
+ }
58
+ await assertNoDirectRecordingShares(recordingId);
59
+ const key = rewindExtensionKey(recordingId);
60
+ const existing = (await readAppState(key)) as RewindExtensionRequest | null;
61
+ if (existing && !["failed", "applied"].includes(existing.status)) {
62
+ return existing;
63
+ }
64
+ if (existing?.status === "applied") {
65
+ throw new Error(
66
+ "This Clip already includes an explicit Rewind pre-roll.",
67
+ );
68
+ }
69
+ const now = new Date().toISOString();
70
+ const request: RewindExtensionRequest = {
71
+ requestId: `rewind-extend-${nanoid()}`,
72
+ recordingId,
73
+ seconds,
74
+ status: "pending",
75
+ requestedAt: now,
76
+ updatedAt: now,
77
+ };
78
+ await writeAppState(key, request);
79
+ return request;
80
+ },
81
+ });
@@ -0,0 +1,47 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ readAppState,
4
+ writeAppState,
5
+ } from "@agent-native/core/application-state";
6
+ import { assertAccess } from "@agent-native/core/sharing";
7
+ import { z } from "zod";
8
+
9
+ import {
10
+ rewindExtensionKey,
11
+ type RewindExtensionRequest,
12
+ } from "./request-rewind-extension.js";
13
+
14
+ export default defineAction({
15
+ description:
16
+ "Update the local Clips Alpha processing state for an explicit Rewind pre-roll request.",
17
+ schema: z.object({
18
+ recordingId: z.string(),
19
+ requestId: z.string(),
20
+ status: z.enum(["processing", "ready", "failed"]),
21
+ preRollRecordingId: z.string().optional(),
22
+ actualDurationMs: z.number().int().positive().optional(),
23
+ error: z.string().max(1200).optional(),
24
+ }),
25
+ run: async (args) => {
26
+ await assertAccess("recording", args.recordingId, "owner");
27
+ const key = rewindExtensionKey(args.recordingId);
28
+ const current = (await readAppState(key)) as RewindExtensionRequest | null;
29
+ if (!current || current.requestId !== args.requestId) {
30
+ throw new Error("Rewind extension request is no longer current.");
31
+ }
32
+ const next: RewindExtensionRequest = {
33
+ ...current,
34
+ status: args.status,
35
+ updatedAt: new Date().toISOString(),
36
+ ...(args.preRollRecordingId
37
+ ? { preRollRecordingId: args.preRollRecordingId }
38
+ : {}),
39
+ ...(args.actualDurationMs
40
+ ? { actualDurationMs: args.actualDurationMs }
41
+ : {}),
42
+ ...(args.error ? { error: args.error } : {}),
43
+ };
44
+ await writeAppState(key, next);
45
+ return next;
46
+ },
47
+ });
@@ -49,6 +49,7 @@ import {
49
49
  readPlaybackSpeedPreference,
50
50
  savePlaybackSpeedPreference,
51
51
  } from "@/lib/playback-speed";
52
+ import { canOfferRewindHistory } from "@/lib/rewind-visibility";
52
53
  import {
53
54
  parseEdits,
54
55
  getExcludedRanges,
@@ -62,6 +63,7 @@ import { computePeaks, type WaveformPeaks } from "@/lib/waveform-peaks";
62
63
  import { ChaptersEditor } from "./chapters-editor";
63
64
  import { defaultSelectionRange } from "./editor-selection";
64
65
  import { EditorToolbar } from "./editor-toolbar";
66
+ import { RewindExtensionDialog } from "./rewind-extension-dialog";
65
67
  import { StitchManager } from "./stitch-manager";
66
68
  import { ThumbnailPicker } from "./thumbnail-picker";
67
69
  import { Timeline } from "./timeline";
@@ -219,6 +221,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
219
221
 
220
222
  const [thumbOpen, setThumbOpen] = useState(false);
221
223
  const [stitchOpen, setStitchOpen] = useState(false);
224
+ const [rewindOpen, setRewindOpen] = useState(false);
222
225
  const [chaptersOpen, setChaptersOpen] = useState(false);
223
226
 
224
227
  const containerRef = useRef<HTMLDivElement | null>(null);
@@ -636,6 +639,10 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
636
639
  onOpenThumbnailPicker={() => setThumbOpen(true)}
637
640
  onOpenChapters={() => setChaptersOpen((v) => !v)}
638
641
  onOpenStitch={() => setStitchOpen(true)}
642
+ onOpenRewind={() => setRewindOpen(true)}
643
+ rewindAlreadyAdded={Boolean(edits.rewindOriginalStartMs)}
644
+ rewindAvailable={canOfferRewindHistory(playerData?.role)}
645
+ rewindRequiresPrivate={recording?.visibility !== "private"}
639
646
  chaptersOpen={chaptersOpen}
640
647
  />
641
648
 
@@ -739,6 +746,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
739
746
  playheadMs={playheadMs}
740
747
  chapters={chapters}
741
748
  splitPoints={splitPoints}
749
+ originalStartMs={edits.rewindOriginalStartMs}
742
750
  onSeek={seek}
743
751
  onClickChapter={(c) => seek(c.startMs)}
744
752
  />
@@ -787,6 +795,23 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
787
795
  onOpenChange={setStitchOpen}
788
796
  seedRecordingId={recordingId}
789
797
  />
798
+ {canOfferRewindHistory(playerData?.role) ? (
799
+ <RewindExtensionDialog
800
+ open={rewindOpen}
801
+ onOpenChange={setRewindOpen}
802
+ recordingId={recordingId}
803
+ durationMs={durationMs}
804
+ videoFormat={videoFormat}
805
+ hasAudio={Boolean(recording.hasAudio)}
806
+ visibility={recording.visibility}
807
+ onVisibilityChanged={async () => {
808
+ await playerDataQuery.refetch();
809
+ }}
810
+ onApplied={async () => {
811
+ await playerDataQuery.refetch();
812
+ }}
813
+ />
814
+ ) : null}
790
815
  </div>
791
816
  );
792
817
  }
@@ -16,6 +16,7 @@ import {
16
16
  IconDownload,
17
17
  IconLoader2,
18
18
  IconTrash,
19
+ IconHistory,
19
20
  } from "@tabler/icons-react";
20
21
  import { useState } from "react";
21
22
  import { toast } from "sonner";
@@ -84,6 +85,10 @@ export interface EditorToolbarProps {
84
85
  onOpenThumbnailPicker: () => void;
85
86
  onOpenChapters: () => void;
86
87
  onOpenStitch: () => void;
88
+ onOpenRewind: () => void;
89
+ rewindAlreadyAdded?: boolean;
90
+ rewindAvailable?: boolean;
91
+ rewindRequiresPrivate?: boolean;
87
92
  chaptersOpen?: boolean;
88
93
  }
89
94
 
@@ -103,6 +108,10 @@ export function EditorToolbar({
103
108
  onOpenThumbnailPicker,
104
109
  onOpenChapters,
105
110
  onOpenStitch,
111
+ onOpenRewind,
112
+ rewindAlreadyAdded,
113
+ rewindAvailable = true,
114
+ rewindRequiresPrivate = false,
106
115
  chaptersOpen,
107
116
  }: EditorToolbarProps) {
108
117
  const t = useT();
@@ -482,6 +491,19 @@ export function EditorToolbar({
482
491
  <IconPuzzle className="mr-2 h-4 w-4" />
483
492
  {t("editorToolbar.stitchClips")}
484
493
  </DropdownMenuItem>
494
+ {rewindAvailable ? (
495
+ <DropdownMenuItem
496
+ disabled={rewindAlreadyAdded}
497
+ onSelect={onOpenRewind}
498
+ >
499
+ <IconHistory className="mr-2 h-4 w-4" />
500
+ {rewindAlreadyAdded
501
+ ? "Rewind history added"
502
+ : rewindRequiresPrivate
503
+ ? "Make private and add Rewind history…"
504
+ : "Add what happened before…"}
505
+ </DropdownMenuItem>
506
+ ) : null}
485
507
  <DropdownMenuSeparator />
486
508
  <DropdownMenuItem onSelect={() => setClearOpen(true)}>
487
509
  <IconTrash className="mr-2 h-4 w-4" />
@@ -490,6 +512,20 @@ export function EditorToolbar({
490
512
  </DropdownMenuContent>
491
513
  </DropdownMenu>
492
514
 
515
+ {rewindAvailable && !rewindAlreadyAdded ? (
516
+ <Button
517
+ size="sm"
518
+ variant="ghost"
519
+ className="hidden shrink-0 gap-1.5 xl:inline-flex"
520
+ onClick={onOpenRewind}
521
+ >
522
+ <IconHistory className="h-4 w-4" />
523
+ {rewindRequiresPrivate
524
+ ? "Make private + add earlier"
525
+ : "Add earlier…"}
526
+ </Button>
527
+ ) : null}
528
+
493
529
  <div className="min-w-3 flex-1" />
494
530
 
495
531
  <Separator orientation="vertical" className="mx-1 h-6" />
@@ -0,0 +1,338 @@
1
+ import {
2
+ agentNativePath,
3
+ appBasePath,
4
+ } from "@agent-native/core/client/api-path";
5
+ import { useActionMutation } from "@agent-native/core/client/hooks";
6
+ import { useT } from "@agent-native/core/client/i18n";
7
+ import { IconHistory, IconLoader2 } from "@tabler/icons-react";
8
+ import { useCallback, useEffect, useRef, useState } from "react";
9
+ import { toast } from "sonner";
10
+
11
+ import { Button } from "@/components/ui/button";
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from "@/components/ui/dialog";
20
+ import { exportConcat } from "@/lib/ffmpeg-export";
21
+ import { uploadFileClient } from "@/lib/upload-file-client";
22
+
23
+ interface RewindExtensionRequest {
24
+ requestId: string;
25
+ recordingId: string;
26
+ seconds: 30 | 300;
27
+ status:
28
+ | "pending"
29
+ | "processing"
30
+ | "ready"
31
+ | "applying"
32
+ | "applied"
33
+ | "failed";
34
+ preRollRecordingId?: string;
35
+ actualDurationMs?: number;
36
+ error?: string;
37
+ }
38
+
39
+ interface RewindExtensionDialogProps {
40
+ open: boolean;
41
+ onOpenChange: (open: boolean) => void;
42
+ recordingId: string;
43
+ durationMs: number;
44
+ videoFormat: "webm" | "mp4";
45
+ hasAudio: boolean;
46
+ visibility: "private" | "org" | "public";
47
+ onVisibilityChanged: () => void | Promise<void>;
48
+ onApplied: () => void | Promise<void>;
49
+ }
50
+
51
+ function wait(ms: number): Promise<void> {
52
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
53
+ }
54
+
55
+ async function readRequest(
56
+ recordingId: string,
57
+ ): Promise<RewindExtensionRequest | null> {
58
+ const params = new URLSearchParams({ recordingId });
59
+ const response = await fetch(
60
+ agentNativePath(
61
+ `/_agent-native/actions/get-rewind-extension-request?${params}`,
62
+ ),
63
+ );
64
+ const json = await response.json().catch(() => null);
65
+ if (!response.ok) {
66
+ throw new Error(json?.error || json?.message || "Could not check Rewind.");
67
+ }
68
+ return (json?.result ?? json)?.request ?? null;
69
+ }
70
+
71
+ export function RewindExtensionDialog({
72
+ open,
73
+ onOpenChange,
74
+ recordingId,
75
+ durationMs,
76
+ videoFormat,
77
+ hasAudio,
78
+ visibility,
79
+ onVisibilityChanged,
80
+ onApplied,
81
+ }: RewindExtensionDialogProps) {
82
+ const t = useT();
83
+ const makePrivateForRewind = useActionMutation(
84
+ "make-recording-private-for-rewind",
85
+ );
86
+ const requestExtension = useActionMutation("request-rewind-extension");
87
+ const applyExtension = useActionMutation("apply-rewind-extension");
88
+ const requestTranscript = useActionMutation("request-transcript" as any);
89
+ const [busy, setBusy] = useState(false);
90
+ const [status, setStatus] = useState<string | null>(null);
91
+ const [progress, setProgress] = useState(0);
92
+ const [privacyConfirmed, setPrivacyConfirmed] = useState(false);
93
+ const mounted = useRef(true);
94
+
95
+ useEffect(() => {
96
+ return () => {
97
+ mounted.current = false;
98
+ };
99
+ }, []);
100
+
101
+ useEffect(() => {
102
+ if (!open && !busy) {
103
+ setStatus(null);
104
+ setProgress(0);
105
+ setPrivacyConfirmed(false);
106
+ }
107
+ }, [busy, open]);
108
+
109
+ const makePrivate = useCallback(async () => {
110
+ setBusy(true);
111
+ setStatus("Making this Clip private…");
112
+ try {
113
+ await makePrivateForRewind.mutateAsync({ recordingId });
114
+ await onVisibilityChanged();
115
+ setPrivacyConfirmed(true);
116
+ setStatus(null);
117
+ toast.success(t("rewindExtension.privateReady"));
118
+ } catch (error) {
119
+ toast.error(error instanceof Error ? error.message : String(error));
120
+ setStatus(null);
121
+ } finally {
122
+ setBusy(false);
123
+ }
124
+ }, [makePrivateForRewind, onVisibilityChanged, recordingId, t]);
125
+
126
+ const addFromRewind = useCallback(
127
+ async (seconds: 30 | 300) => {
128
+ setBusy(true);
129
+ setProgress(0);
130
+ try {
131
+ setStatus("Asking Clips Alpha for local Rewind history…");
132
+ const created = (await requestExtension.mutateAsync({
133
+ recordingId,
134
+ seconds,
135
+ })) as RewindExtensionRequest;
136
+ let request = created;
137
+ for (let attempt = 0; attempt < 160; attempt += 1) {
138
+ if (request.status === "failed") {
139
+ throw new Error(
140
+ request.error || "Clips Alpha could not read Rewind.",
141
+ );
142
+ }
143
+ if (
144
+ request.status === "ready" &&
145
+ request.preRollRecordingId &&
146
+ request.actualDurationMs
147
+ ) {
148
+ break;
149
+ }
150
+ setStatus(
151
+ request.status === "processing"
152
+ ? "Clips Alpha is preparing that local interval…"
153
+ : "Waiting for Clips Alpha…",
154
+ );
155
+ await wait(1_500);
156
+ request = (await readRequest(recordingId)) ?? request;
157
+ }
158
+ if (
159
+ request.status !== "ready" ||
160
+ !request.preRollRecordingId ||
161
+ !request.actualDurationMs
162
+ ) {
163
+ throw new Error(
164
+ "Clips Alpha did not finish the Rewind request in time.",
165
+ );
166
+ }
167
+
168
+ setStatus("Combining the selected history with this Clip…");
169
+ const blob = await exportConcat(
170
+ [
171
+ {
172
+ url: `${appBasePath()}/api/video/${encodeURIComponent(request.preRollRecordingId)}`,
173
+ format: "mp4",
174
+ hasAudio,
175
+ },
176
+ {
177
+ url: `${appBasePath()}/api/video/${encodeURIComponent(recordingId)}`,
178
+ format: videoFormat,
179
+ hasAudio,
180
+ },
181
+ ],
182
+ (next) => mounted.current && setProgress(next.progress),
183
+ );
184
+ setStatus("Saving the longer Clip…");
185
+ const upload = await uploadFileClient(
186
+ blob,
187
+ `rewind-${recordingId}.mp4`,
188
+ );
189
+ if (!upload?.url) {
190
+ throw new Error(
191
+ "Connect video storage before adding Rewind history.",
192
+ );
193
+ }
194
+ await applyExtension.mutateAsync({
195
+ recordingId,
196
+ requestId: request.requestId,
197
+ preRollRecordingId: request.preRollRecordingId,
198
+ videoUrl: upload.url,
199
+ durationMs: durationMs + request.actualDurationMs,
200
+ addedMs: request.actualDurationMs,
201
+ });
202
+ if (hasAudio) {
203
+ void requestTranscript
204
+ .mutateAsync({ recordingId, force: true, regenerate: true })
205
+ .catch(() => {
206
+ toast.info(
207
+ "The longer Clip was saved; its transcript can be regenerated later.",
208
+ );
209
+ });
210
+ }
211
+ await onApplied();
212
+ toast.success(
213
+ `${request.actualDurationMs >= 60_000 ? "Five minutes" : "Thirty seconds"} added from Rewind.`,
214
+ );
215
+ onOpenChange(false);
216
+ } catch (error) {
217
+ toast.error(error instanceof Error ? error.message : String(error));
218
+ setStatus(null);
219
+ } finally {
220
+ if (mounted.current) {
221
+ setBusy(false);
222
+ setProgress(0);
223
+ }
224
+ }
225
+ },
226
+ [
227
+ applyExtension,
228
+ durationMs,
229
+ hasAudio,
230
+ onApplied,
231
+ onOpenChange,
232
+ recordingId,
233
+ requestExtension,
234
+ requestTranscript,
235
+ videoFormat,
236
+ ],
237
+ );
238
+
239
+ return (
240
+ <Dialog open={open} onOpenChange={(next) => !busy && onOpenChange(next)}>
241
+ <DialogContent className="sm:max-w-md">
242
+ <DialogHeader>
243
+ <DialogTitle className="flex items-center gap-2">
244
+ <IconHistory className="h-4 w-4 text-primary" />
245
+ {t("rewindExtension.title")}
246
+ </DialogTitle>
247
+ <DialogDescription>
248
+ {t("rewindExtension.description")}
249
+ </DialogDescription>
250
+ </DialogHeader>
251
+
252
+ {busy ? (
253
+ <div
254
+ className="rounded-lg border bg-muted/30 p-4 text-sm"
255
+ role="status"
256
+ aria-live="polite"
257
+ >
258
+ <div className="flex items-center gap-2 font-medium">
259
+ <IconLoader2
260
+ className="h-4 w-4 animate-spin"
261
+ aria-hidden="true"
262
+ />
263
+ {status}
264
+ </div>
265
+ {progress > 0 ? (
266
+ <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-muted">
267
+ <div
268
+ className="h-full bg-primary transition-[width]"
269
+ role="progressbar"
270
+ aria-label={t("rewindExtension.progressLabel")}
271
+ aria-valuemin={0}
272
+ aria-valuemax={100}
273
+ aria-valuenow={Math.round(progress * 100)}
274
+ style={{ width: `${Math.round(progress * 100)}%` }}
275
+ />
276
+ </div>
277
+ ) : null}
278
+ </div>
279
+ ) : visibility !== "private" && !privacyConfirmed ? (
280
+ <div className="grid gap-3">
281
+ <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
282
+ <strong className="block">
283
+ {t("rewindExtension.privateFirstTitle")}
284
+ </strong>
285
+ <span className="mt-1 block text-muted-foreground">
286
+ {t("rewindExtension.privateFirstDescription")}
287
+ </span>
288
+ </div>
289
+ <Button onClick={() => void makePrivate()}>
290
+ {t("rewindExtension.makePrivateContinue")}
291
+ </Button>
292
+ </div>
293
+ ) : (
294
+ <div className="grid gap-2">
295
+ <Button
296
+ variant="outline"
297
+ className="h-auto justify-start py-3 text-left"
298
+ onClick={() => void addFromRewind(30)}
299
+ >
300
+ <span>
301
+ <strong className="block">
302
+ {t("rewindExtension.add30Seconds")}
303
+ </strong>
304
+ <span className="text-xs font-normal text-muted-foreground">
305
+ Useful when you clicked Record just after the important bit.
306
+ </span>
307
+ </span>
308
+ </Button>
309
+ <Button
310
+ variant="outline"
311
+ className="h-auto justify-start py-3 text-left"
312
+ onClick={() => void addFromRewind(300)}
313
+ >
314
+ <span>
315
+ <strong className="block">
316
+ {t("rewindExtension.add5Minutes")}
317
+ </strong>
318
+ <span className="text-xs font-normal text-muted-foreground">
319
+ {t("rewindExtension.add5MinutesDescription")}
320
+ </span>
321
+ </span>
322
+ </Button>
323
+ </div>
324
+ )}
325
+
326
+ <DialogFooter>
327
+ <Button
328
+ variant="ghost"
329
+ disabled={busy}
330
+ onClick={() => onOpenChange(false)}
331
+ >
332
+ Cancel
333
+ </Button>
334
+ </DialogFooter>
335
+ </DialogContent>
336
+ </Dialog>
337
+ );
338
+ }
@@ -24,24 +24,9 @@ import {
24
24
  import { ScrollArea } from "@/components/ui/scroll-area";
25
25
  import { exportConcat } from "@/lib/ffmpeg-export";
26
26
  import { formatMs } from "@/lib/timestamp-mapping";
27
+ import { uploadFileClient } from "@/lib/upload-file-client";
27
28
  import { cn } from "@/lib/utils";
28
29
 
29
- /** Client-side upload via the framework's auto-mounted `/file-upload` route. */
30
- async function uploadFileClient(
31
- blob: Blob,
32
- filename: string,
33
- ): Promise<{ url: string } | null> {
34
- const form = new FormData();
35
- form.append("file", blob, filename);
36
- const res = await fetch(agentNativePath("/_agent-native/file-upload"), {
37
- method: "POST",
38
- body: form,
39
- });
40
- if (!res.ok) return null;
41
- const json = await res.json();
42
- return json?.url ? { url: json.url as string } : null;
43
- }
44
-
45
30
  export interface StitchManagerProps {
46
31
  open: boolean;
47
32
  onOpenChange: (open: boolean) => void;
@@ -1,3 +1,4 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
1
2
  import { useMemo } from "react";
2
3
 
3
4
  import {
@@ -21,6 +22,8 @@ export interface TimelineProps {
21
22
  chapters?: TimelineChapter[];
22
23
  excludedRanges?: Array<{ startMs: number; endMs: number }>;
23
24
  splitPoints?: number[];
25
+ /** Countdown-complete start after explicit Rewind history was prepended. */
26
+ originalStartMs?: number;
24
27
  onSeek?: (originalMs: number) => void;
25
28
  onClickChapter?: (chapter: TimelineChapter) => void;
26
29
  className?: string;
@@ -44,10 +47,12 @@ export function Timeline({
44
47
  chapters = [],
45
48
  excludedRanges = [],
46
49
  splitPoints = [],
50
+ originalStartMs,
47
51
  onSeek,
48
52
  onClickChapter,
49
53
  className,
50
54
  }: TimelineProps) {
55
+ const t = useT();
51
56
  const ticks = useMemo(() => {
52
57
  if (durationMs <= 0) return [];
53
58
  // Target ~1 tick per 100px at the current zoom, rounded to a human interval.
@@ -133,6 +138,27 @@ export function Timeline({
133
138
  );
134
139
  })}
135
140
 
141
+ {typeof originalStartMs === "number" && originalStartMs > 0 ? (
142
+ <Tooltip>
143
+ <TooltipTrigger asChild>
144
+ <div
145
+ className="absolute top-0 z-10 h-full w-0.5 cursor-help bg-amber-500"
146
+ style={{
147
+ left: (originalStartMs / Math.max(durationMs, 1)) * width - 1,
148
+ }}
149
+ aria-label={`Original Clip start at ${formatMs(originalStartMs)}`}
150
+ >
151
+ <span className="absolute left-1 top-0 whitespace-nowrap rounded-sm bg-amber-500 px-1 text-[9px] font-semibold text-black">
152
+ {t("timeline.clipStartedHere")}
153
+ </span>
154
+ </div>
155
+ </TooltipTrigger>
156
+ <TooltipContent>
157
+ Recording began after the countdown · {formatMs(originalStartMs)}
158
+ </TooltipContent>
159
+ </Tooltip>
160
+ ) : null}
161
+
136
162
  {/* Playhead */}
137
163
  <div
138
164
  className="absolute top-0 h-full pointer-events-none"