Skip to content

feat(ios): animate fold with timed hinge keyframes - #2763

Merged
thymikee merged 5 commits into
codex/headless-duo-foldfrom
codex/duo-fold-trajectories
Sep 22, 2026
Merged

thymikee merged 5 commits into
codex/headless-duo-foldfrom
codex/duo-fold-trajectories

Conversation

@thymikee

@thymikee thymikee commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds timed hinge keyframes to fold across CLI, Node, and MCP, building on #2762:

await client.command.fold({keyframes: [
  {atMs: 0, angle: 0},
  {atMs: 1667, angle: 160},
  {atMs: 3333, angle: 100},
  {atMs: 5000, angle: 180},
]});

Accepts 2–64 keyframes over at most 60 seconds, with linear interpolation, reversals, and holds. The final timestamp bounds motion; preparation and readback are additional. One cancellable guest process uses absolute deadlines at approximately 60 Hz and skips missed frames. Completion verifies the final angle, including stable intermediate poses. Presets remain supported. Recordings preserve keyframes for replay.

36 files; 871 additions / 106 deletions relative to the parent. Scope includes shared input/schema, Apple execution, tests, and documentation.

Validation

Head: f5357444ecb4dc174dbad38c717faead994f46c6.

  • pnpm check:affected --run: passed all runnable checks; GitHub-owned checks pending.
  • Shared TypeScript/native golden cases, interpolation, property tests, provider-backed daemon routing, transport budgets, recording, and final-angle verification covered. Removing final-angle verification made its regression test fail.
  • Live Duo: saved .ad replay passed with final 1.3° readback. Cancellation removed the guest helper and held 69.8° beyond the original deadline. CLI/Node trajectories, holds and reversals also verified. Review reply includes raw evidence. Sessions/claims cleaned up.
  • Duo coverage remains local until GitHub Actions supports the runtime. Private simulator HID compatibility remains dependent on Xcode/runtime versions.

@thymikee
thymikee added this pull request to stack #2764 September 22, 2026 16:48
@thymikee thymikee changed the title codex/duo fold trajectories feat(ios): animate fold with timed hinge keyframes Sep 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-22 18:46 UTC

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.77 MB 4.78 MB +6.5 kB
Package (unpacked) 4.77 MB 4.78 MB +6.5 kB
Package (download) 1.42 MB 1.43 MB +2.1 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 18.5 ms 20.9 ms +2.4 ms
CLI --help 52.7 ms 61.6 ms +8.8 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at e676092. The code has two blocking defects, so this is not ready to merge as is.

A recorded fold --keyframes '[...]' action does not survive the round trip to a saved script. formatPortableActionLine routes fold through appendGenericActionScriptArgs (https://github.com/callstack/agent-device/blob/e676092/packages/ad-script/src/internal/script-utils.ts#L254), which only writes positionals and the series flags, not keyframes, and the script parser has no --keyframes handling either. So the saved .ad line is a bare fold, and replaying it throws INVALID_ARGS: fold requires a pose argument. The existing test only checks the in-memory SessionAction, not the parsed script line, so it would not catch this. Any flag marked recorded: true should round-trip through both the writer and the parser — can formatPortableActionLine serialize keyframes as a quoted --keyframes <json> for fold, with the parser reading it back, and can a test cover action -> formatted line -> parsed action -> daemon request?

For keyframe input, awaitHingePose requires the observed angle to fall in the same category as the target angle (https://github.com/callstack/agent-device/blob/e676092/packages/platform-apple/src/foldable/pose.ts#L963), but the category boundaries sit at 1 and 179 degrees while the angle tolerance is 0.5 degrees. A target just inside a boundary (say 1.3) with a readback just outside it (say 0.9) is within tolerance but a different category, so the wait fails with fold-pose-unverified: did not reach the half-open pose even though the fold landed at the requested angle. For keyframe targets, can success be decided by angle tolerance (plus the stable-pair check when the angle is interior), with the reported pose derived from the observed angle rather than required to match the target's category? A test with target 1.3 and readback 0.9 would show this.

Not blocking: the comment next to the 210_000 timeout still says "one macOS helper press (30s)" and omits the 60s of motion, parseFoldInput's pose check is always true, parseFoldKeyframesJson has an unreachable branch, and keyframes.m returns 1 on a sample mismatch without naming the case — these can be taken or left.

Is it worth parsing the fold intent once at the daemon boundary and passing a typed input down, so setAppleFoldPose and the script writer stop re-parsing and re-validating? The keyframe help text in cli-help repeats commands.md and client-api.md almost word for word — would a pointer instead of the full paragraph bring the size growth closer to the 3kB threshold? Could the field-metadata helper absorb the hand-built mutually-exclusive wrapper if it supported that case directly? Aside from those, the new surface (one contracts parser, one native loop) looks about as small as the feature allows.

Once the script round trip is fixed, a live replay on the Duo simulator of a saved .ad file containing fold --keyframes '<json>' would confirm both fixes: the saved line should show the --keyframes JSON, and the replay step result should report hingeAngleDegrees within 0.5 degrees of the final keyframe. The live CLI, Node, and cancellation runs quoted in the PR body have no attached output to check against.

Does aborting the simulator spawn also stop the fold process inside the guest? The only evidence so far is the quoted 60.8 degree result. The category-boundary failure above comes from reading the thresholds and tolerance; I did not see it on a device.

Smoke Tests failed while waiting for the fixture home screen on a non-foldable simulator in a route that only calls wait, open, and snapshot; this diff only touches the fold command, the fold pose helper, and the fold timeout, so the failure looks unrelated, but a rerun would confirm that.

The next thing to fix is the fold keyframes round trip through the .ad script writer and parser, then the angle-based success check for custom fold targets.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed at f5357444ecb4dc174dbad38c717faead994f46c6.

  • Saved scripts: the fold writer emits quoted --keyframes JSON and the parser preserves it. The provider-backed regression now exercises recorded action → formatted line → parsed action → daemon request → simulator HID dispatch and verified result. It failed before the fix.
  • Boundary angles: custom targets use angle tolerance and stable consecutive reads for interior targets, independently of pose categories. The result's pose comes from the observed angle. Regressions for 1.3→0.9, 178.7→179.1, and 0.8→1.2 all failed before the fix and pass now; preset verification remains unchanged.
  • Cancellation: your concern was justified. A stronger probe showed abrupt SIGKILL of simctl could orphan the animation. The transport now requests SIGTERM with a bounded 1s escalation, allowing simctl to terminate the guest. The regression checks that policy; the live probe below used the production implementation.
  • Removed redundant pose/JSON checks and the native owner's duplicate intent parsing; improved native fixture failure diagnostics; shortened CLI help. The timeout comment already describes the 30s build plus up to 60s motion. I kept the one-off mutually-exclusive schema local rather than expanding shared metadata machinery for a single case. Native input validation remains necessary at the separate process boundary.

Live evidence on the Duo / Xcode 27.1, captured during this review:

context platform=ios device="iPhone Duo" kind=simulator theme=unknown
open "com.apple.mobilesafari"
fold --keyframes "[{\"atMs\":0,\"angle\":180},{\"atMs\":1000,\"angle\":1.3}]"
fold "open"
close

The saved file replayed all 4 steps successfully in 19.2s. Replaying the same saved keyframe line without the final reopening step completed 3 steps in 14.8s, with sessionActive:false. Independent CoreDevice readback immediately afterwards:

Angle: 1,3°  Mech: 1,3°  Velocity:+0,0°/s  AngleValid:Y

The CoreDevice stream ends via its documented timeout; that sample is the observed result. The original recorded fold response also reported pose:"half-open", hingeAngleDegrees:1.3.

Production cancellation probe (a 5s 0→180 sequence, abort requested at 2s; process inventory filtered to the exact temporary helper path):

{"elapsedMs":2001,"phase":"before-abort","guestProcessIds":["83047"]}
{"phase":"aborted","elapsedMs":2347,"message":"request canceled","guestProcessIds":[]}
{"phase":"readback","angle":69.8,"elapsedMs":7795,"guestProcessIds":[]}
{"phase":"readback","angle":69.8,"elapsedMs":12965,"guestProcessIds":[]}
{"restoredAngle":180}

Temporary sessions/claims were cleaned up, and the Duo was restored open with Safari running. pnpm check:affected --run passed on the stated head. The prior iOS smoke log fails while waiting for a readable fixture snapshot; the new push starts a fresh smoke run. CI results on the new head are pending.

@thymikee
thymikee merged commit abba01e into main Sep 22, 2026
18 checks passed
@thymikee
thymikee deleted the codex/duo-fold-trajectories branch September 22, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant