@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/core/src/server/agent-chat-plugin.ts +16 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +19 -7
- package/corpus/templates/analytics/app/hooks/use-chart-tooltip-portal.ts +60 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-20-dashboard-requests-no-longer-stall-without-progress.md +6 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts +5 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +10 -9
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
- 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"
|