@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.
@@ -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 startedAt = api.wallClockStartMs ?? Date.now();
143548
- let written = 0;
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 missing = Math.min(framesDue(startedAt, Date.now()) - written, MAX_CATCHUP_FRAMES);
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
- written++;
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 durationMs = startedAtMs === null ? null : (api.wallClockEndMs ?? Date.now()) - startedAtMs;
143797
- return { outputFile, sizeBytes: size, durationMs, ...warning ? { warning } : {} };
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, constant 30fps at the device's native resolution).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.16.2-next.8",
3
+ "version": "0.16.2-next.9",
4
4
  "description": "MCP server for iOS Simulator and Android Emulator control",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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, and a forgotten one wastes disk and returns a video full of dead air. Two safety nets exist — use both:
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 wall-clock accurate**: a device only emits a frame when its screen changes, so captured frames are re-paced onto a steady 30 fps timeline. A recording of a completely still screen is still a full-length video (and compresses to almost nothing), and `durationMs` matches the time you actually recorded.
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.