5 Commits
Author SHA1 Message Date
3cbd727b85 Tests: the marquee ellipse check walks several dash periods of the curve (#3018)
* Tests: the marquee ellipse check walks several dash periods of the curve

The old check sampled ~12 positions along a 28 px arc of a 4-on/4-off ants
pattern: a margin of one sample, so renderer, antialiasing or dash phase
decided pass or fail. Walk the true ellipse around each edge midpoint over
±0.3 rad instead, with the ±2 physical px band hugging the curve, so the
check sees the dashes wherever the phase puts them (#253 follow-up).

* rustfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:40:44 -04:00
60339cf675 app.open: open directory .pcraft through the automation read root (#2490) (#2581)
* app.open: open directory .pcraft through the automation read root (#2490)

The native on-disk format is a directory, but the automation read path
serves regular files only, so agents could not open the files the app
itself writes. The format crate's storage seam is now public (ByteSource
+ load_bytes_source); the workspace opens the .pcraft subdirectory through
the held capability and loads through it. app.open tries the directory
form for .pcraft paths and falls back to the bytes path for the
single-file (zip) form; every other format is unchanged.

* app.open .pcraft: review fixes (#2581)

read_entry now bounds the read itself (the stat length is only a buffer
hint) and both directory sources reject non-regular entries, so a crafted
bundle with a FIFO entry can no longer block the open and stream unbounded.
The desktop opener runs under the crash guard like its import sibling, the
zip fallback is structural (Ok(None) instead of an error-message match),
a directory-form document no longer arms a save-back target the file-only
automation writer could never replace, and the open status is translated
(Opened {name}, 16 locales).

* read_entry: saturating max+1 so a usize::MAX budget can't overflow

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:12:59 -04:00
Ahmed Hosny 3e3e580217 MCP bridge: reconnect before idle sockets die, retry reads, actionable timeouts (#2487) (#2507)
* MCP bridge: reconnect before idle sockets die, retry reads, actionable timeouts (#2487)

The app closes idle control connections after 30s (IO_TIMEOUT) while the
bridge kept one persistent connection, so the first call after a pause
failed with "operation may have completed". The bridge now drops and
reconnects before sending once the connection has been idle 25s, so a
request is never written to a socket the app may have dropped.

The default call timeout drops from 60s to 55s, strictly below the app's
60s reply deadline, so the bridge's own error — naming the method and the
likely causes — is the one callers see instead of a race between the
app's deadline reply and the caller's own cutoff.

Idempotent read methods (ui.inspect and friends) get one automatic retry
on a transport failure; edits keep #1007 semantics and are never resent.
A timed-out call is never retried (it would only double the wait).

* MCP bridge: caller-vouched read retries, per-failure wording, docs contract (#2487 review)

The retry decision moves from a hand-maintained method list — most of whose
entries were engine command ids that never travel as control-protocol
methods, so they could never match — to the call sites that know a call is
a read: call never resends (#1007), call_read retries once on a transport
failure, and the MCP server opts its pure-read tools in. Failures are
worded by what they prove: a connection that could never be established is
"the request was not sent", not "may have completed"; a read timeout says
a read may be retried safely instead of demanding a state inspection. The
55s client cap, the read-retry exception, and the idle reconnect are now
documented in the control protocol and the security book.
2026-10-11 08:29:35 -04:00
Ahmed Hosny c25630b5fa app.save: encode on the save worker, not the UI thread (#2488) (#2517)
* app.save: encode on the save worker, not the UI thread (#2488)

The automation save ran export+write inline inside the control drain,
freezing the window and every queued control request while a large
document encoded — and overrunning the 60s reply deadline. It now uses
the same #2017 background path as File > Save when background jobs are
on: the encode and the capability-scoped write run on a worker, the
document is locked, and the control reply waits on the job (wait: false
answers {job, pending} at once). Tests and the web keep the inline path.

The save job's result now carries the path alongside the warnings, so
waiting callers get the same {path, warnings} reply both ways.

* app.save: never fire saveDocument hooks from automation saves; review fixes (#2488)

Automation saves complete in the job tick, long after run_waiting lifted
its synchronous event suppression, so the background branch fired the
user's saveDocument script bindings and Image Assets — the exact hooks
docs/control-protocol.md promises never run for automation. SaveJob now
carries an automation flag and records through saved_automation, which
skips document_saved on every path; the inline fallback records through
it too, so both modes behave the same. A regression test installs a
saveDocument binding and asserts it fires for an interactive save and
not for app.save.

The desktop's two save workers collapse into one save_through parameterized
by the write step (drift between interactive and automation saves), the
Saved status/notices are translated (Saved {name}, 16 locales), the pending
reply carries warnings like file.save's, and the success payload is built
at one site.
2026-10-11 02:54:14 -04:00
308c3369c0 Control: timeout errors name the likely causes (#2489) (#2497)
* Control: timeout text derives from the reply deadline (#2489 review)

The server deadline and both timeout replies now share one constant:
REPLY_DEADLINE drives the serve thread's clock and the text is built from
it, so the message can never claim a number the server no longer uses.

* Tests: drop the duplicate joined() helper left by #2782 and #2853 both landing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:27:03 -04:00