Skip to content

release: v0.5.5#55

Merged
BlueShork merged 33 commits into
mainfrom
release/v0.5.5
Jun 14, 2026
Merged

release: v0.5.5#55
BlueShork merged 33 commits into
mainfrom
release/v0.5.5

Conversation

@BlueShork

Copy link
Copy Markdown
Owner

Release v0.5.5 — bug-fix release focused on iOS (simulator + physical iPhone), Android 16, inspect mode, and settings.

Highlights (user-facing)

iOS

  • Running flows on the iOS Simulator no longer hangs when the inspector is open.
  • Much faster simulator reconnect (warm driver reuse).
  • Physical iPhone preview falls back to a screenshot mirror when the USB mirror stays silent; inspecting a physical device no longer freezes.
  • Clearer first-connect feedback + driver startup timeout aligned to the readiness budget.

Android

  • Fixed the frozen mirror on Android 16 (scrcpy 3.3.4).

General

  • Web Browser target gated behind an opt-in beta toggle.
  • Settings reorganized into labelled subgroups.
  • Console "Clear" works in Simple mode.
  • Inspect mode resets cleanly on device change (no stuck loading).
  • No spurious "update failed" popup on startup.

Release mechanics

  • Version bumped to 0.5.5 (package.json, tauri.conf.json, Cargo.toml, LICENSE Change Date).
  • CHANGELOG_EN.md updated.
  • macOS aarch64 build is signed + notarized; updater artifacts (.app.tar.gz + .sig) and latest.json are ready to attach to the GitHub release once merged.

Once merged, the release is tagged v0.5.5 and published as Latest so the in-app updater endpoint resolves to it.

BlueShork and others added 30 commits June 9, 2026 20:45
…tex token)

jsonwebtoken 10 ships no crypto backend by default (default = ["use_pem"]),
so the 9->10 bump compiled fine but RS256 signing panic!'d at runtime; with
panic = "abort" in release that aborted the whole app the moment Billy
requested a Vertex access token. Debug builds swallowed the panic via tokio
unwind, which is why it only crashed in production.

Adds a regression test that signs with a throwaway RSA key so the actual
signer path runs (existing tests only covered pre-signing error paths).
The native simulator preview pushed raw RGBA frames at 60 Hz with a 900px
cap (~1.5 MB/frame, up to ~90 MB/s) through the Tauri IPC channel, saturating
the webview main thread and making typing in the YAML editor lag.

- SIM_FPS 60 -> 30, SIM_MAX_DIM 900 -> 700 (~75% less IPC traffic)
- drop the redundant 1.5 MB per-frame copy on the JS side (ImageData accepts
  an offset view over a plain ArrayBuffer)
- align the sim_capture smoke test with the real preview params
- Extract the fps badge into its own component so the periodic fps updates
  from streamStore re-render only the badge, not the whole Toolbar subtree
- Move View-menu panel entries into per-item components subscribing to their
  own visibility flag, removing Toolbar's subscription to the visible record
- Memoize ChatMessage: during streaming only the appended-to message changes
  identity, so other messages now skip their ReactMarkdown re-render
After a crash or SIGKILL, kill_on_drop never fires and the studio JVM
(holding the XCTest driver on :22087) outlives the app. Stale instances
then fight the fresh one for the driver and the inspector hangs on a
zombie runner for the whole readiness budget (observed: 4 orphan studios
for the same UDID, inspector unusably slow).

Mirror the Android path's kill_orphan_studios, scoped to the target UDID
so a legitimate Android studio keeper is never touched.
… guards

Root cause of the production-only SIGABRT when interacting with a booted
simulator (symbolized: attach() displayClass msg_send, sim_capture/mod.rs):
CoreSimulator's ROCK proxies throw ObjC exceptions when the XCTest runner
churns the device IO state. sim_capture wraps those sends in
objc2::exception::catch, which needs the exception to UNWIND through Rust
frames to reach its @catch — but panic = "abort" compiles every Rust frame
as non-unwindable, so the exception aborted the whole app before the catch
fired ("panic in a function that cannot unwind"). Debug builds unwind,
which is why this never reproduced outside production.

Also adds a matrix of manual repro tests (screen churn, restart cycles,
XCTest touch injection, concurrent simctl screenshots, concurrent
double-attach) and dedupes the booted-udid helper.
…atch_unwind

The real crash, finally symbolized and reproduced in a test:
  NSInvalidArgumentException: -[ROCKImmutableProxy displayClass]:
  unrecognized selector sent to instance

Once the XCTest runner (maestro studio) touches the simulator's device IO,
the descriptor proxies become ROCKImmutableProxy instances that throw on
some selectors. The raw ObjC exception unwound into Rust and aborted the
process before objc2::exception::catch could fire — dropping panic=abort
was necessary but not sufficient.

- objc2  feature: every msg_send is wrapped in @try/@catch at
  the C level, converting thrown exceptions into catchable Rust panics
- sim_capture: fragile proxy sends now use a  (catch_unwind)
  helper, so displayClass/framebufferSurface throws fall back gracefully
  (capture keeps working via the ioSurface path — verified by the
  capture_with_touch_injection repro test)
- poll thread gets a catch_unwind backstop: worst case the preview stops,
  the app never dies
…per)

The bridge JVM (maestro studio) can outlive the XCTest runner it launched:
the process stays alive while nothing listens on :22087 anymore. The keeper
only checked is_process_alive, so it was reused forever and every tap /
Home press failed with 'driver unreachable' until a manual reconnect.

ensure_ios_keeper now also requires is_healthy(): once a keeper has been
ready, a /status probe (cached 5s) must still answer; otherwise the keeper
is recycled — the respawn path also culls the zombie studio via the
orphan killer, and the runner gets reinstalled/relaunched.
…assthrough

The bridge-install button (and any other download, incl. Vertex token
exchange) failed with an IPC error on a corporate machine while GitHub
worked fine in the browser. Two gaps:

- reqwest was built with plain rustls-tls (bundled Mozilla roots only), so
  a TLS-inspection proxy's MITM CA — trusted by the OS keychain, hence the
  browser — was rejected by the app. Switch to rustls-tls-native-roots.
- Finder-launched apps don't inherit shell proxy vars; enrich
  HTTP(S)_PROXY / ALL_PROXY / NO_PROXY from the login shell like PATH.
Plugging in (and connecting) a physical iPhone made the app unusable:
PHYSICAL_SCREENSHOT_INTERVAL_MS was 0, so /screenshot PNGs were polled
back-to-back, and each multi-MB frame crossed the Tauri event bridge as a
JSON number array — 4-15 MB of JSON parsed token-by-token on the webview
main thread, continuously.

- base64-encode PNG frames in ios_frame/web_frame payloads (one string
  token, ~4x smaller, orders of magnitude faster to parse); decode at the
  ipc.ts boundary
- physical poll interval 0 -> 150 ms (~3 fps real-world either way, but
  the main thread gets breathing room between frames)
…ed capture

The fps ceiling was the on-device capture itself: XCUIScreen.screenshot +
pngRepresentation is ~200-300 ms per frame, serialized one request at a
time. Two levers, both server-side supported already:

- /screenshot?compressed=true: the runner's jpegData(0.5) encode is much
  faster than PNG and the payload ~10x smaller (frontend now types the
  Blob by sniffing the JPEG magic)
- pipeline the polling across 2 concurrent lanes so capture overlaps
  transfer; a shared ticket + newest-emitted watermark drops frames that
  complete out of order so the preview never steps backwards

~3 fps -> roughly 8-12 fps expected; per-lane 50 ms pause keeps the
webview main thread responsive (frames are small now).
…sibility

Probe for the future high-fps physical preview: enables CoreMediaIO's
AllowScreenCaptureDevices opt-in, finds the plugged iPhone's muxed DAL
device via AVFoundation, runs a capture session for 5s and reports
fps + frame size. Run with the iPhone plugged in and unlocked:

  cargo run --manifest-path src-tauri/Cargo.toml --example avf_probe

Expects a one-time CAMERA permission prompt for the terminal. If this
yields 30-60 fps, it becomes the physical analogue of sim_capture
(wired through the existing preview Channel); the /screenshot poll
stays as the fallback.
Wire the avf_probe-validated capture path (27.2 fps @ native pixels on an
iPhone 17 Pro) into the live preview as the physical analogue of
sim_capture:

- new avf_capture module: CMIO AllowScreenCaptureDevices opt-in, camera
  TCC request/check, muxed DAL device matched by uniqueID/UDID (or the
  only one present), AVCaptureSession + BGRA video-data output whose
  delegate downscales (nearest-neighbour, longer side <= 700) and
  try_sends Frames; all !Send AVF objects live on a dedicated thread
  (sim_capture's threading model), ObjC exceptions contained the same way
- preview.rs: CaptureSource enum picks AVF for physical keepers, the
  existing drain/Channel/frontend pipeline is reused unchanged
- src-tauri/Info.plist: NSCameraUsageDescription (without it TCC kills
  the app on first AVF touch); macOS exposes the device SCREEN as a
  camera-class device, the phone camera itself is never used
- objc2-av-foundation promoted from dev-dependency to macOS dependency

Fallback unchanged: camera denied / no DAL device / any error -> the
pipelined JPEG /screenshot poller (~8-12 fps) keeps the preview alive.
…driver

upgrade_ios_preview gated every native preview behind wait_until_ready,
but on a first physical connect the driver BUILD takes ~10 min while the
readiness budget is 180 s — the upgrade timed out, fell back to the
/screenshot poller (which needs the same driver), and the preview stayed
blank for the entire build.

The AVF mirror is driver-independent (the frame IS the device screen),
so physical keepers now skip the wait entirely: the screen shows within
seconds of plugging in, while the driver keeps building in the background
for inspect/tap. The simulator path still waits (its crop needs
device_info from a ready driver).
…USB mirror

The bundle is signed with the hardened runtime (notarization requirement),
under which camera-class device access needs the
com.apple.security.device.camera entitlement on top of the Info.plist
usage description. Without it AVFoundation listed ZERO capture devices and
never showed the TCC prompt — the mirror failed with 'no USB
screen-capture device appeared' while the unsigned avf_probe worked fine.
setSampleBufferDelegate:queue: does NOT retain the delegate; ours was a
local dropped at the end of setup, deallocating the delegate and its
frame Sender — the channel closed instantly ('preview frame channel
closed' right after attach) and the mirror stayed blank. The capture
thread now holds a LiveCapture { session, delegate } for the whole run.
The iPhone's muxed DAL device can take 10s+ to (re)appear after plugging
in or after a previous capture session died; one failed attempt used to
strand the preview on the JPEG poller until a manual reconnect. Discovery
budget 10s -> 20s, and the frontend retries the upgrade up to 5 times
(5s apart) instead of fire-and-forget-once.
Same zombie class as simulator studios: after a crash/SIGKILL the bridge
outlives the app AND keeps its port bound. Each leftover instance holds a
600x port, so every fresh bridge binds the NEXT free one (observed: nine
orphans squatting 6001-6009) while the keeper's HTTP client polls
PHYSICAL_BRIDGE_PORT forever — the mirror streamed fine but taps/inspect
were dead even though the live bridge printed Ready (on 6009). Cull any
bridge for the target UDID before spawning so the fresh one always gets
6001.
The first inspect right after the bridge rebinds its port commonly hits
one transient 'driver unreachable'; the immediate retry succeeds. Retry
once internally instead of making the user click Inspect twice.
maestro-ios-device's fatal() uses fmt.Printf, so its failure reason lands
on STDOUT — the install command only relayed stderr, yielding the useless
'setup failed: ' with no detail. Relay the tail of both streams.
Bundled locally via @fontsource/manrope (400-800) so the desktop app
stays offline-capable; wired as the Tailwind sans stack and into the
pre-hydration splash. Mono stack unchanged for the console/editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed silent (startup) update check set phase to "error", which opened
the UpdateDialog regardless of the silent flag — so a flaky network or a
404ing update endpoint popped "Update failed" on every launch. Silent
checks now just warn and fall back to not-available; only user-initiated
checks surface the error.
scrcpy v3.3.1 crashes on Android 16: IDisplayWindowListener gained an
onDisplayAnimationsDisabledChanged method the bundled server doesn't
implement, so DisplaySizeMonitor throws AbstractMethodError, the server
thread aborts after a few frames, and the mirror freezes on the first
frame (video stream closed by peer). Upstream scrcpy #6362, fixed in
v3.3.3.

Bumps the bundled server jar, SCRCPY_VERSION/JAR_FILENAME, and the
download script's pin + SHA-256. v3.3.4 stays backward-compatible with
Android 15 / One UI 7 and uses no changed server args, so physical
Android and emulator both work; iOS doesn't use scrcpy at all.
Web support is beta and unstable, so the synthetic Web Browser (Chromium)
target is now hidden from the device list unless the user opts in via a
new "Web Browser target (beta)" setting (off by default). The backend
still always returns the target; the device list filters it out client-
side. The active connection is never hidden, so a user can still
disconnect web after toggling the setting off.
…ubgroups

General was a grab-bag mixing appearance, editor, updates, and app
behavior. Introduce a SettingsSubgroup primitive and regroup:
- General → Appearance / Editor / Application
- Device & Performance → Mirroring / Inspector / Monitoring / Beta targets

Move the Web Browser target toggle out of General into Device's "Beta
targets" group (it's a device target, not a general preference) and give
it a beta badge matching the experimental one. No behavior change.
The console renders 'logs' in Technical mode but 'steps' in Simple mode
(the default). clearLogs only reset 'logs', so in Simple mode the Clear
button appeared to do nothing. Replace it with clearConsole, which clears
logs, steps, exitCode and stopRequested — a full reset regardless of
mode. Clear is now disabled while running (wiping steps mid-run would
drop the live step list) and when both views are already empty.
… run

On an iOS simulator, the maestro studio driver and `maestro test` both
need the XCTest driver on :22087 and can't coexist. run_flow stops the
keeper before launching the test, but an inspector auto-dump (or a tap)
would call ensure_ios_keeper and re-spawn a competing `maestro studio`,
deadlocking both on :22087 so the run never starts.

Add an ios_sim_run_active flag (AppState): set on sim-run spawn, cleared
on the runner's exit. ensure_ios_keeper refuses to re-warm a simulator
keeper while it's set; the inspector also pauses background dumps while a
run is in flight. Physical iOS (reuses the bridge) and Android are
unaffected.
The first inspect on a cold iOS simulator blocks ~1-2 min on the XCTest
driver cold-start. Two improvements:

- Bump MAESTRO_DRIVER_STARTUP_TIMEOUT 120s -> 180s to match our own
  readiness budget. At 120s maestro studio threw IOSDriverTimeoutException
  while we were still waiting, so a slow-but-fine cold start failed.
- Inspector shows 'Starting iOS simulator driver... first inspect can take
  ~1-2 min' instead of a silent 'Dumping hierarchy...' that reads as a
  freeze, and the timeout error is now actionable.
…reconnect

A plain disconnect now leaves a simulator's `maestro studio` keeper
running instead of killing it, so reconnecting the same booted sim reuses
the already-warm XCTest driver (seconds, not the ~1-2 min cold start).

teardown_all_sessions/teardown_ios take a keep_ios_sim_warm flag:
disconnect passes true, quit passes false (no orphans). Physical bridges
are never kept warm. A lingering warm keeper is retired by
ensure_ios_keeper (different iOS device) or at the top of connect_device
(switching to Android/Web), since only one studio can hold :22087.
upgrade_ios_preview retired the screenshot poller the moment the native
capture *attached*, assuming attach == frames flowing. On a physical
iPhone whose AVF USB capture attaches but never delivers sample buffers
(seen on iPhone 17 Pro / iOS 26.5.1), the poller was killed before the
driver was even ready, leaving the mirror blank forever and the device
unusable.

spawn_ios_preview now returns a receiver that fires on the first frame
actually pushed to the channel. The poller is retired only then; if the
capture stays mute the receiver resolves Err when the task ends and the
screenshot poller keeps mirroring via /screenshot once the driver is up.
Simulators deliver a framebuffer frame immediately, so their behavior is
unchanged. Disconnect still retires the poller via the same state slot.
On a physical iPhone the screenshot poller IS the preview, hitting
/screenshot on the single :22087 forward every 50ms. With the previous
commit keeping that poller alive for a mute-AVF device, the flood
saturated the maestro-ios-device bridge so an inspect's /status and
/hierarchy were starved — inspect hung on 'waiting for iOS driver'.

Add an ios_inspect_active flag: enter_inspect_mode sets it (reset via a
drop guard on every return) and the physical screenshot worker skips
polling while it's set, yielding the bridge to the hierarchy dump. The
preview briefly freezes during the dump, then resumes. Simulators are
unaffected (their poller uses simctl, not the bridge).
Inspector state is global, not per-device, and nothing reset it when the
connected device changed — switching devices left a stale tree and could
strand 'loading' on a dump that targeted the previous device, so the
Inspect control sat in a permanent spinner.

- disable() now also clears 'loading' and the dumpInFlight guard (a
  background dump's .then early-returns once disabled and never cleared
  loading itself).
- App resets the inspector whenever the connected device serial changes
  (switch / connect / disconnect).
@sonarqubecloud

sonarqubecloud Bot commented Jun 14, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
9.6% Coverage on New Code (required ≥ 80%)
3.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@BlueShork BlueShork merged commit ed9fd58 into main Jun 14, 2026
8 of 9 checks passed
@BlueShork BlueShork deleted the release/v0.5.5 branch June 14, 2026 23:41
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant