@swmansion/argent 0.16.2-next.8 → 0.16.2-next.9
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/dist/tool-server.cjs
CHANGED
|
@@ -143036,6 +143036,9 @@ var screenRecordingSessionBlueprint = {
|
|
|
143036
143036
|
frameStream: null,
|
|
143037
143037
|
lastFrameStreamError: null,
|
|
143038
143038
|
pumpTimer: null,
|
|
143039
|
+
trimStatic: true,
|
|
143040
|
+
framesWritten: 0,
|
|
143041
|
+
trimmedAnyFrames: false,
|
|
143039
143042
|
wallClockStartMs: null,
|
|
143040
143043
|
wallClockEndMs: null,
|
|
143041
143044
|
timeLimitSeconds: null,
|
|
@@ -143486,6 +143489,7 @@ var OUTPUT_FPS2 = 30;
|
|
|
143486
143489
|
var FRAME_INTERVAL_MS = 1e3 / OUTPUT_FPS2;
|
|
143487
143490
|
var MAX_CATCHUP_FRAMES = 5;
|
|
143488
143491
|
var MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
143492
|
+
var STATIC_GRACE_MS = 1e3;
|
|
143489
143493
|
var STREAM_CONNECT_TIMEOUT_MS = 1e4;
|
|
143490
143494
|
var FIRST_FRAME_TIMEOUT_MS = 1e4;
|
|
143491
143495
|
var START_FAILFAST_GRACE_MS = 800;
|
|
@@ -143542,21 +143546,50 @@ function ffmpegArgs(opts) {
|
|
|
143542
143546
|
function framesDue(startedAtMs, nowMs) {
|
|
143543
143547
|
return Math.floor((nowMs - startedAtMs) * OUTPUT_FPS2 / 1e3);
|
|
143544
143548
|
}
|
|
143549
|
+
function sameFrame(a, b) {
|
|
143550
|
+
if (a === b) return true;
|
|
143551
|
+
if (!a || !b) return false;
|
|
143552
|
+
return a.equals(b);
|
|
143553
|
+
}
|
|
143545
143554
|
function startPump(api, stream) {
|
|
143546
143555
|
const child = api.captureProcess;
|
|
143547
|
-
const
|
|
143548
|
-
|
|
143556
|
+
const trim = api.trimStatic;
|
|
143557
|
+
api.framesWritten = 0;
|
|
143558
|
+
api.trimmedAnyFrames = false;
|
|
143559
|
+
let paceBaseMs = api.wallClockStartMs ?? Date.now();
|
|
143560
|
+
let paceBaseFrames = 0;
|
|
143561
|
+
let lastFrame = null;
|
|
143562
|
+
let lastChangeMs = paceBaseMs;
|
|
143563
|
+
let dead = false;
|
|
143549
143564
|
api.pumpTimer = setInterval(() => {
|
|
143550
143565
|
const stdin = child?.stdin;
|
|
143551
143566
|
if (!stdin || !stdin.writable) return;
|
|
143552
143567
|
const frame = stream.latest;
|
|
143553
143568
|
if (!frame) return;
|
|
143554
143569
|
if (stdin.writableLength > MAX_BUFFERED_BYTES) return;
|
|
143555
|
-
const
|
|
143570
|
+
const now = Date.now();
|
|
143571
|
+
if (trim) {
|
|
143572
|
+
if (!sameFrame(frame, lastFrame)) {
|
|
143573
|
+
lastFrame = frame;
|
|
143574
|
+
lastChangeMs = now;
|
|
143575
|
+
}
|
|
143576
|
+
if (now - lastChangeMs > STATIC_GRACE_MS) {
|
|
143577
|
+
dead = true;
|
|
143578
|
+
api.trimmedAnyFrames = true;
|
|
143579
|
+
return;
|
|
143580
|
+
}
|
|
143581
|
+
if (dead) {
|
|
143582
|
+
dead = false;
|
|
143583
|
+
paceBaseMs = now;
|
|
143584
|
+
paceBaseFrames = api.framesWritten;
|
|
143585
|
+
}
|
|
143586
|
+
}
|
|
143587
|
+
const target = paceBaseFrames + framesDue(paceBaseMs, now);
|
|
143588
|
+
const missing = Math.min(target - api.framesWritten, MAX_CATCHUP_FRAMES);
|
|
143556
143589
|
for (let i = 0; i < missing; i++) {
|
|
143557
143590
|
if (!stdin.writable) return;
|
|
143558
143591
|
stdin.write(frame);
|
|
143559
|
-
|
|
143592
|
+
api.framesWritten++;
|
|
143560
143593
|
}
|
|
143561
143594
|
}, FRAME_INTERVAL_MS);
|
|
143562
143595
|
}
|
|
@@ -143659,6 +143692,8 @@ async function startCaptureLocked(api, params) {
|
|
|
143659
143692
|
api.outputFile = outputFile;
|
|
143660
143693
|
api.logoFile = logoFile;
|
|
143661
143694
|
api.watermarkSkipped = watermarkSkipped;
|
|
143695
|
+
api.trimStatic = params.trimStatic;
|
|
143696
|
+
api.framesWritten = 0;
|
|
143662
143697
|
api.captureProcess = child;
|
|
143663
143698
|
api.frameStream = stream;
|
|
143664
143699
|
api.recordingActive = true;
|
|
@@ -143756,6 +143791,7 @@ async function stopCapture(api) {
|
|
|
143756
143791
|
const outputFile = api.outputFile;
|
|
143757
143792
|
const logoFile = api.logoFile;
|
|
143758
143793
|
const startedAtMs = api.wallClockStartMs;
|
|
143794
|
+
const trimStatic = api.trimStatic;
|
|
143759
143795
|
const endedEarly = api.recordingTimedOut || api.recordingExitedUnexpectedly;
|
|
143760
143796
|
const streamError = api.frameStream?.error ?? api.lastFrameStreamError ?? null;
|
|
143761
143797
|
const watermarkSkipped = api.watermarkSkipped;
|
|
@@ -143793,8 +143829,16 @@ async function stopCapture(api) {
|
|
|
143793
143829
|
warning = [warning, `The watermark was not applied (${watermarkSkipped}).`].filter(Boolean).join(" ");
|
|
143794
143830
|
}
|
|
143795
143831
|
const size = await statNonEmptyOutput(outputFile, "screen_recording_stop");
|
|
143796
|
-
const
|
|
143797
|
-
|
|
143832
|
+
const wallClockMs = startedAtMs === null ? null : (api.wallClockEndMs ?? Date.now()) - startedAtMs;
|
|
143833
|
+
const durationMs = trimStatic ? Math.round(api.framesWritten / OUTPUT_FPS2 * 1e3) : wallClockMs;
|
|
143834
|
+
const trimmedMs = trimStatic && wallClockMs !== null && api.trimmedAnyFrames ? Math.max(0, wallClockMs - durationMs) : void 0;
|
|
143835
|
+
return {
|
|
143836
|
+
outputFile,
|
|
143837
|
+
sizeBytes: size,
|
|
143838
|
+
durationMs,
|
|
143839
|
+
...trimmedMs !== void 0 ? { wallClockMs, trimmedMs } : {},
|
|
143840
|
+
...warning ? { warning } : {}
|
|
143841
|
+
};
|
|
143798
143842
|
} catch (err) {
|
|
143799
143843
|
const empty2 = await import_fs17.promises.stat(outputFile).then((s) => s.size === 0).catch(() => false);
|
|
143800
143844
|
if (empty2) await import_fs17.promises.rm(outputFile, { force: true }).catch(() => {
|
|
@@ -143809,6 +143853,8 @@ async function stopCapture(api) {
|
|
|
143809
143853
|
api.outputFile = null;
|
|
143810
143854
|
api.logoFile = null;
|
|
143811
143855
|
api.watermarkSkipped = null;
|
|
143856
|
+
api.framesWritten = 0;
|
|
143857
|
+
api.trimmedAnyFrames = false;
|
|
143812
143858
|
api.wallClockStartMs = null;
|
|
143813
143859
|
api.wallClockEndMs = null;
|
|
143814
143860
|
api.timeLimitSeconds = null;
|
|
@@ -143829,6 +143875,9 @@ var zodSchema51 = external_exports.object({
|
|
|
143829
143875
|
udid: external_exports.string().describe("Target device id from `list-devices` (iOS Simulator UDID or Android serial)."),
|
|
143830
143876
|
timeLimitSeconds: external_exports.number().int().min(1).max(MAX_TIME_LIMIT_SECONDS).optional().describe(
|
|
143831
143877
|
`Auto-stop cap in seconds (default ${DEFAULT_TIME_LIMIT_SECONDS}, max ${MAX_TIME_LIMIT_SECONDS}). Set it to slightly more than the interaction you plan to capture.`
|
|
143878
|
+
),
|
|
143879
|
+
trimStatic: external_exports.boolean().optional().describe(
|
|
143880
|
+
"Default true. Collapse stretches where the screen does not change: the first second of each still stretch is kept, then unchanged frames are dropped until something moves again, so a long recording with brief activity comes back short instead of full of dead air. The returned durationMs is the trimmed length; wallClockMs/trimmedMs report what was removed. Set false to keep a faithful real-time recording."
|
|
143832
143881
|
)
|
|
143833
143882
|
});
|
|
143834
143883
|
var capability26 = {
|
|
@@ -143839,7 +143888,8 @@ function createScreenRecordingStartTool(registry2) {
|
|
|
143839
143888
|
return {
|
|
143840
143889
|
id: "screen-recording-start",
|
|
143841
143890
|
capability: capability26,
|
|
143842
|
-
description: `Start recording the device screen to a video file (h264 mp4,
|
|
143891
|
+
description: `Start recording the device screen to a video file (h264 mp4, 30fps at the device's native resolution).
|
|
143892
|
+
By default stretches where the screen does not change are trimmed out (see trimStatic), so a long session with only brief activity comes back as a short clip instead of minutes of dead air.
|
|
143843
143893
|
The recording keeps running across other tool calls (every result carries a reminder) until \`screen-recording-stop\` is called or timeLimitSeconds elapses \u2014 immediately after starting, set yourself a reminder/wakeup for the expected end of the recording so it is never left running.
|
|
143844
143894
|
Use when the user wants a video of an interaction, animation, or app behavior \u2014 for a single still frame use \`screenshot\` instead.
|
|
143845
143895
|
Returns { status: "recording", timeLimitSeconds, outputFile } \u2014 the video is retrieved later by \`screen-recording-stop\`, not by reading outputFile directly.
|
|
@@ -143887,7 +143937,8 @@ Fails if a recording is already running on the device, the device is not booted,
|
|
|
143887
143937
|
return startCapture(api, {
|
|
143888
143938
|
streamUrl,
|
|
143889
143939
|
timeLimitSeconds,
|
|
143890
|
-
watermark: isFeatureEnabled("video-watermark")
|
|
143940
|
+
watermark: isFeatureEnabled("video-watermark"),
|
|
143941
|
+
trimStatic: params.trimStatic ?? true
|
|
143891
143942
|
});
|
|
143892
143943
|
}
|
|
143893
143944
|
};
|
|
@@ -143908,7 +143959,7 @@ var screenRecordingStopTool = {
|
|
|
143908
143959
|
description: `Stop the screen recording started by \`screen-recording-start\` and retrieve the video: frame capture ends and ffmpeg finalizes the mp4.
|
|
143909
143960
|
Also retrieves the video when the recording already ended on its own (time limit reached, capture process died) \u2014 call it even after the cap fired.
|
|
143910
143961
|
Use when the interaction being captured is finished, or a tool-result note reminds you a recording is still running.
|
|
143911
|
-
Returns { video, durationMs, warning? }; video is a downloadable artifact materialized to a local path.
|
|
143962
|
+
Returns { video, durationMs, wallClockMs?, trimmedMs?, warning? }; video is a downloadable artifact materialized to a local path. When static-frame trimming removed dead air, durationMs is the trimmed video length and wallClockMs/trimmedMs report the real duration and how much was cut.
|
|
143912
143963
|
Fails if no recording (running or finished-but-unretrieved) exists for the given udid.`,
|
|
143913
143964
|
searchHint: "stop end finish screen recording video capture save retrieve",
|
|
143914
143965
|
zodSchema: zodSchema52,
|
|
@@ -143923,6 +143974,8 @@ Fails if no recording (running or finished-but-unretrieved) exists for the given
|
|
|
143923
143974
|
const artifacts = requireArtifacts(ctx);
|
|
143924
143975
|
const video = await artifacts.register(stopped.outputFile, { mimeType: "video/mp4" });
|
|
143925
143976
|
const result = { video, durationMs: stopped.durationMs };
|
|
143977
|
+
if (stopped.wallClockMs !== void 0) result.wallClockMs = stopped.wallClockMs;
|
|
143978
|
+
if (stopped.trimmedMs !== void 0) result.trimmedMs = stopped.trimmedMs;
|
|
143926
143979
|
if (stopped.warning) result.warning = stopped.warning;
|
|
143927
143980
|
return result;
|
|
143928
143981
|
}
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ One recording per device at a time; different devices can record concurrently. R
|
|
|
14
14
|
|
|
15
15
|
## 2. Critical: never leave a recording running
|
|
16
16
|
|
|
17
|
-
A recording does not stop itself before its `timeLimitSeconds` cap,
|
|
17
|
+
A recording does not stop itself before its `timeLimitSeconds` cap, so a forgotten one keeps capturing until the cap fires — holding the recording session, wasting disk, and delaying the video you are waiting on (and with `trimStatic: false` it comes back padded with dead air). Two safety nets exist — use both:
|
|
18
18
|
|
|
19
19
|
1. **Set yourself a reminder the moment the recording starts.** You know the expected capture length (the interaction you are about to drive). Immediately after `screen-recording-start` returns, schedule a wake-up for that expected end time using whatever your harness provides — a built-in reminder/wakeup or scheduled-task tool if you have one, otherwise a background shell running `sleep <expected-seconds>` whose completion notification pulls you back. When it fires, call `screen-recording-stop`. Do not rely on remembering.
|
|
20
20
|
2. **Read the tool-result notes.** While a recording is running, every argent tool result carries a `NOTE:` reminding you it is still going and how to stop it. If the note says the recording already ended (time limit hit), still call `screen-recording-stop` — that is what hands you the file.
|
|
@@ -27,15 +27,17 @@ A recording does not stop itself before its `timeLimitSeconds` cap, and a forgot
|
|
|
27
27
|
2. Call `screen-recording-start` with `udid` and a `timeLimitSeconds` slightly above the expected interaction length (default 180, max 600).
|
|
28
28
|
3. Set the end-of-recording reminder described in §2 — this step is not optional.
|
|
29
29
|
4. Drive the interaction to capture: gestures, navigation, typing (`argent-device-interact`). Prefer `run-sequence` for tight multi-step interactions so tool-call latency does not pad the video.
|
|
30
|
-
5. Call `screen-recording-stop` with the same `udid`. It returns `{ video, durationMs, warning? }`; `video` is an artifact — use its `hostPath` locally or download it via the artifacts endpoint. The video is already final when stop returns (the watermark is stamped during capture, not in a second pass), so stop takes well under a second.
|
|
30
|
+
5. Call `screen-recording-stop` with the same `udid`. It returns `{ video, durationMs, wallClockMs?, trimmedMs?, warning? }`; `video` is an artifact — use its `hostPath` locally or download it via the artifacts endpoint. The video is already final when stop returns (the watermark is stamped during capture, not in a second pass), so stop takes well under a second.
|
|
31
31
|
6. Check `warning`: it reports cap-triggered stops, early encoder exits, a dropped frame stream, and possibly-truncated containers. Verify the file plays (or at least has a sane size) before presenting it to the user.
|
|
32
32
|
|
|
33
|
+
**Static-frame trimming (on by default).** Stretches where the screen does not change are collapsed: the first second of each still stretch is kept so pauses read naturally, then unchanged frames are dropped until something moves again (a change of even a couple of pixels counts). So you can leave a recording running across slow steps, waits, or thinking time without padding the clip with dead air — a 40-second session with 5 seconds of real activity comes back as a ~5-7 second video. When trimming removed anything, stop also returns `wallClockMs` (real elapsed time) and `trimmedMs` (how much was cut); `durationMs` is always the length of the video you actually get. Pass `trimStatic: false` to `screen-recording-start` when you want a faithful real-time recording (e.g. to measure how long something took on screen).
|
|
34
|
+
|
|
33
35
|
---
|
|
34
36
|
|
|
35
37
|
## 4. Platform notes and limits
|
|
36
38
|
|
|
37
39
|
- **What can be recorded**: anything simulator-server drives — iOS simulators, Android emulators, and physical Android devices. The only length limit is `timeLimitSeconds` (max 600).
|
|
38
|
-
- **The timeline is
|
|
40
|
+
- **The timeline is paced to a steady 30 fps**: a device only emits a frame when its screen changes, so captured frames are re-paced onto a fixed timeline rather than bunching up. With static-frame trimming off (`trimStatic: false`) that timeline is wall-clock accurate — a completely still screen still comes back as a full-length video (compressing to almost nothing) and `durationMs` matches the time you actually recorded. With trimming on (the default, see §3) still stretches past the grace window are collapsed, so `durationMs` is the trimmed video length and `wallClockMs` carries the real elapsed time.
|
|
39
41
|
- **Android**: records at the device's native resolution; secure screens (DRM, some password fields) come out black.
|
|
40
42
|
- **Unsupported**: tvOS simulators, physical iPhones, Chromium apps, Vega/Fire TV, and remote (`remote:`-prefixed) simulators — none of them expose a readable frame stream. For a single still frame use `screenshot`; for a replayable interaction script use `argent-create-flow` instead of a video.
|
|
41
43
|
- **ffmpeg is required**: it is the encoder, so `screen-recording-start` fails up front with an install hint if it is missing (`brew install ffmpeg`). It is resolved from `PATH` plus the usual Homebrew prefixes.
|