Resolve two conflicts in Sources/tart/Commands/Run.swift — both are clean
unions of independent features added on each side:
1. VM setup block: keep this branch's drop-zone directory creation (for
drag-and-drop onto the built-in UI) AND main's guest-provisioning
options parsing (consumed by vm.start(provisioning:)).
2. Top-level decls: keep this branch's VMWindowView SwiftUI view AND
main's `extension Run: MainThreadCommand {}`.
No behavioral overlap. Verified: swift build succeeds; DropProgressCopier
tests pass (8/8).
* Use let for the immutable disk image storage attachment
* Don't bind the unused error when catching connection-pool failures
* Report errors thrown inside tart run's fire-and-forget tasks
We were discarding any error thrown inside these unstructured tasks,
which silently hid failures to run the control socket or to start and
stop the VM, and which the compiler now warns about.
Wrap them in an ErrorReportingTask, which spawns the task and reports
any thrown error to stderr, rather than repeating a do/catch at every
call site. An unstructured task spawned from a synchronous context (a
signal handler or SwiftUI action) has no parent to propagate the error
to, so reporting it is the best we can do.
* Avoid blocking SwiftNIO calls in async guest agent connections
The gRPC channel setup in "tart exec" and the MAC address resolver
created a dedicated event loop group and tore both it and the channel
down with the blocking syncShutdownGracefully() and wait(), which are
unavailable from async contexts (the former is an error in the Swift 6
language mode).
Factor the connection out into a withGuestAgentChannel() helper that
uses the process-wide singleton event loop group, so there is no group
to shut down, and closes the channel with the async close().get().
Exposes Apple's macOS 27 guest provisioning API
(VZMacGuestProvisioningOptions) so a macOS guest can be set up
automatically on the first boot after restore.
The flag takes a comma-separated list of key=value pairs mapping 1:1 to
the API properties (fullName, username, password, logsInAutomatically,
enablesRemoteLogin). It is validated to require a macOS 27+ host and a
macOS VM.
The entire user-facing surface is gated behind
'#if arch(arm64) && compiler(>=6.4)' so the flag doesn't appear in help
on toolchains that lack the macOS 27 SDK, while the runtime
'#available(macOS 27, *)' check gates actual use against the host OS.
When built against the macOS 27 (Xcode 27, Swift 6.4) SDK, "tart run"
brings up the VM window but the guest never boots.
Swift's asynchronous main() entry point implicitly starts an executor
that owns the main thread, and as of Swift 6.4 that executor is no
longer backed by the Dispatch main queue. Running an AppKit/SwiftUI
run loop nested inside it via MainApp.main() leaves the main run loop
unable to drain Swift tasks or DispatchQueue.main, so the task that
starts the VM is never scheduled, even though the window itself
(driven directly by AppKit during launch) still appears.
We now keep Root.main() synchronous, so that a command driving a run
loop can own the main thread at the top level, exactly like a plain
SwiftUI app. With AppKit owning the loop again, MainActor tasks and
the Dispatch main queue drain as before. Such commands opt in through
a new MainThreadCommand protocol; everything else keeps running
asynchronously via a detached task and dispatchMain().
Verified that the guest boots again, and that Ctrl+C still stops the
VM gracefully.
`copyTree`/`copyInto` treated `contentsOfDirectory` errors as an empty
listing (`try? … ?? []`), so a permission/I/O failure inside a dropped
folder was silently ignored and the drop reported success — producing an
incomplete copy in the guest with no warning (data-lossy for users who
expect the whole folder to transfer).
Let the read error propagate instead. copyTree's do/catch already removes
the partial `dst` and rethrows, so an unreadable subtree now aborts the
drop and cleans up rather than half-completing.
Adds a regression test covering an unreadable nested directory.
split(whereSeparator: { $0 == "\n" || $0 == "\r" }) never matches CRLF
because Swift treats "\r\n" as a single Character (grapheme cluster
U+000D, U+000A), so the closure — which compares against the
single-codepoint characters "\n" and "\r" — fires for neither. With
CRLF input the entire stdout becomes one "line" that doesn't have the
tartdrop-dest= prefix, and the parser returns nil.
Switch to String.enumerateLines, which handles LF, CR, and CRLF.
The inner `report` closure captured `copied` from the enclosing scope, but
`copyFile`/`copyInto` hold the same variable as `inout` while running — so
the next progress callback overlapped the still-active inout access and
aborted the process with a fatal access conflict mid-drop. Pass the value
in instead of capturing it.
Covers two of the file-promise trade-offs (A + B from the review):
A. Progress: poll bytes streamed into the (shared) per-item subdir while
the opaque receivePromisedFiles runs, feeding the toast's live size
readout instead of a dead spinner. Best-effort — providers that write
a temp file and atomically rename only become visible at the end.
B. Cancellation: DropCancellationToken gains an onCancel hook; the
promise path cancels its operation queue and unblocks the wait loop
on ⊗ instead of sitting on the 30 s timeout. Cooperative providers
abort; either way we stop waiting and clean up at once.
C (true determinate % via NSItemProvider.loadFileRepresentation) is
intentionally NOT implemented: macOS AppKit drags don't vend
NSItemProvider, and NSFilePromiseReceiver exposes no Progress/cancel
surface, so a real percentage isn't reachable without an undocumented
hack. The size poller is the honest ceiling.
Also: capture the dispatch group locally in RelocationGate.drain to drop
a Sendable-capture warning. Adds DropCancellationTokenTests.
Previously promised files (Photos/Mail/browser drags) were received into
a host-private staging dir and then copied a second time into the shared
drop zone. Now each promise is received directly into its per-item
dropRoot/<uuid>/ subdir, so the source app writes it exactly once.
Plain file/dir drags still stream through copyTree (progress +
cancellation). Promises show an indeterminate bar (no byte progress is
available from the promise API) and relocation uses the actual written
names so provider de-duplication is honored. relocate() now reaps the
subdir by emptiness rather than unconditionally, so a multi-file promise
sharing one subdir isn't deleted out from under its pending siblings.
Introduces DropHandler to own the drop pipeline and addresses every item
from the review:
- Folders / .app bundles / packages: DropProgressCopier.copyTree walks
directories instead of failing with a generic 'Failed to copy'.
- File promises (Photos, Mail, browser image drags) are now accepted and
materialized instead of silently no-opping.
- Multi-file toast race: per-file DropSession id; stale relocation
results for a superseded file are ignored by update/finish/
setFinalDestination.
- Partial files: copyTree removes its partial output on any error, and
the handler drops the now-empty subdir, so the guest never sees a
truncated file.
- Teardown race: in-flight guest relocations register with
RelocationGate; 'tart run' drains it (<=6s) before deleting the drop
zone / exiting.
- Path collisions: each file copies into its own dropRoot/<uuid>/ subdir.
- Reserved name: a user --dir named 'Dropped Files' is now rejected.
- Linux / no agent: relocation is skipped and the toast says 'Copied to
the shared folder' instead of a misleading Finder destination.
- Agent-down timing: success toast holds on a fallback timer that
outlives the 5s RPC deadline so the final destination is always shown.
- Concurrency: relocations run one-at-a-time; copy failures are
coalesced into a single alert instead of a modal storm.
- Zero-byte/unknown size shows just the copied amount, not '0 bytes of ?'.
- Removed dead DropFolderBox + stale doc comment; fixed the
http://-vs-https:// typo in toRemoteOrLocalURL.
Tests: DropProgressCopierTests (replace/cancel/error-cleanup/empty/
directory/size), GuestDropParseTests (stdout parser), and a reserved-name
case in DirectoryShareTests. Product and test target both compile.
DropProgressToast.swift (436 lines) bundled four responsibilities. Split
verbatim into:
- DropCancellationToken.swift - cancel token + DropCopyCancelled sentinel
- DropProgressCopier.swift - chunked file copier
- DropProgressToast.swift - toast state/coordination (lifecycle API)
- DropProgressToast+Panel.swift - AppKit panel construction/positioning
Each file is well under 250 lines and single-responsibility. The only
non-move change is widening the members the +Panel extension references
from private to internal; no behavior, signatures, or logic changed.
Previously the guest-agent relocation script always asked Finder for the
*front* window and dropped there. That meant dropping on the bare Desktop
while a Downloads window happened to be frontmost put the file in
Downloads — the opposite of user intent.
Plumb the drop point (already captured as a normalized 0..1 top-left
coordinate by DropGeometry.normalize) through synthesizeGuestDrop and
GuestDropSynthesis.perform into the in-guest script. The script now
converts the point to guest-screen pixels using the desktop window's
bounds, walks `every Finder window` in front-to-back order, and returns
the folder of the first window whose bounds contain the point. If no
window does, dest_dir stays empty and the existing fallback drops the
file on ~/Desktop — which is correct for a drop on bare Desktop.
The AppleScript body is piped to `osascript -` via a single-quoted bash
variable (rather than a heredoc) so it survives Swift's multi-line
string indentation rules cleanly.
After a host→guest drop completes, the toast now updates from "Done" to
"Copied to <Folder>" (e.g. "Desktop", "Documents") once the guest agent
has finished relocating the file out of the share. When the agent isn't
reachable, the toast falls back to "Copied to Shared Files" so the user
always sees a sensible destination.
Three things had to come together:
1. Toast surfaces the destination
`GuestDropSynthesis.perform` now returns a `GuestDropOutcome` carrying
the basename of the destination folder (the in-guest script appends
a `tartdrop-dest=<basename>` line after `mv`). The relocation runs as
a fire-and-forget Task off the copy queue and patches the toast via a
new `setFinalDestination` method — which uses a shared
`pendingFinalText` slot so a fast relocation result doesn't get
clobbered by the delayed "Done" placeholder.
2. gRPC timeout shortened so failures fit in the toast window
The exec-call timeout drops from 8 s to 5 s. Steady-state calls
finish in well under 500 ms; 5 s leaves headroom for a first-run
osascript blocked on a Finder Automation TCC prompt inside the
guest. The baseline hide on success grows to 2 s so the destination
update has a chance to land before the panel disappears.
3. Dev builds of tart can now host the guest agent
`CI.version` returns `"SNAPSHOT"` for non-tagged builds, which gave
the VM a console port named `tart-version-SNAPSHOT`. The guest agent
parses that suffix as a semver and falls back to `unix.Kill(getppid,
SIGTERM)` when it can't — which fails with EPERM against launchd and
prints "operation not permitted" every 10 s forever. A new
`CI.deviceVersion` always emits a valid semver with major ≥ 2
(`"99.0.0"` for SNAPSHOT) and VM.swift uses it for the port name.
`99.0.0` without a `-prerelease` suffix is required because macOS's
BSD tty layer rejects the dotted+hyphenated form and refuses to
expose the device under `/dev/cu.*`.
Three related fixes to the drop progress toast:
1. Position the toast over the VM window's content (top-right, 44 pt
below the titlebar) instead of floating outside above the window.
This reverts the layout intent of the prior "float above" commit;
the toast remains an NSPanel child-windowed to the VM window so it
tracks z-order and movement.
2. Replace the broken slide-in animation with a fade-in. The previous
animation called `panel.animator().setFrameOrigin(target)`, which is
a silent no-op on NSWindow — the panel jumped to the off-screen-right
start position and stayed there. Use `animator().alphaValue` instead,
which is actually animatable on NSWindow.
3. Sync the "Done" text with the bar reaching 100%. NSProgressIndicator
has an undocumented ~0.3 s smooth-fill animation when doubleValue
jumps, so showing "Done" simultaneously with setting maxValue made the
text lead the bar. Delay "Done" by 0.35 s and extend the hide delay
to 1.05 s so "Done" still gets ~0.7 s of visibility. Also reset the
bar in `hide` so a subsequent drop doesn't briefly flash the previous
final state before resetting to 0.
The notification banner used to sit inside the VM window's top-right
corner, overlapping guest content. Move it outside the window: the
toast's bottom edge now sits 10 pt above the VM window's top edge,
still right-aligned. The slide-in animation is unchanged.
If the VM window is jammed against the top of the screen and there's
no room above for the toast, fall back to the old "inside top-right"
position so we never clip the menu bar.
Two changes to the drop progress HUD:
- Position: was bottom-center of the VM window, now top-right with a
quick (~0.18 s) slide-in from the right edge — reads as a macOS
notification banner. Subsequent files in a multi-file drop retarget
the panel in place without replaying the animation.
- Cancel: small ⊗ close button in the toast's top-right corner. Click
flips a `DropCancellationToken` shared with the chunked copier,
which polls between 1 MiB chunks and throws `DropCopyCancelled` on
the next boundary (sub-100 ms latency on fast disks). The drop
handler removes the half-copied destination, the toast shows
"Cancelled", and any remaining files in a multi-file drop are
skipped instead of starting.
`FileManager.copyItem` is opaque to the user: a 5 GB drop just freezes
the cursor for ten seconds with no visible signal that anything is
happening. Replace it with a chunked `FileHandle` copy (1 MiB chunks,
50 ms-throttled progress callbacks) and render a borderless HUD panel
anchored to the VM window bottom — filename, determinate bar, and a
"[i/N] copied / total" detail line. The panel auto-hides ~0.8 s after
the final byte so the user sees the "Done" state.
Multi-file drops reuse the same panel and increment the [i/N] counter,
since the existing copy loop already serializes files.
The previous guest-drop synthesis only ran `open -R` on the file's
location in the "Dropped Files" share. That just revealed it in the
share, which was the user-visible bug we were trying to fix: dropped
files appeared in `/Volumes/My Shared Files/Dropped Files/` instead
of somewhere natural.
Now the agent's exec script asks Finder for the frontmost window's
POSIX path via `osascript`, moves the file out of the share into that
folder, and then reveals it. If no Finder window is open, the path
isn't writable, or it points back at the share itself, the script
falls back to `~/Desktop`. Filename collisions get suffixed
(`foo.txt` → `foo 2.txt`, …) instead of clobbering.
osascript here runs in the agent's user-GUI session, so the first
drop triggers an Automation→Finder TCC prompt in the guest the user
approves once.
Verified end-to-end via `tart exec` (same RPC path the drop handler
uses): no Finder window → ~/Desktop, Finder on Downloads → Downloads,
Finder on Dropped Files itself → ~/Desktop fallback.
Adds the host-side hook for the forthcoming tart-guest-agent DragAndDrop RPC
that will perform a real drag-and-drop at the cursor's position inside the
guest instead of dumping files into a generic share folder.
- DropGeometry.normalize: pure helper turning a view-local drop point into
top-left (0..1) coordinates the guest agent can map onto its screen.
- VMContainerView.performDragOperation: capture draggingLocation on the
main thread (only valid here), compute normalized point, then dispatch
the copy off-main as before.
- synthesizeGuestDrop: documented no-op stub called from the copy completion
path. When the agent RPC lands, this becomes the real call site; until
then it returns false and the existing share-folder copy is the
user-visible result.
No behavior change for users today.
- Hold a FileLock on the dropzone so a concurrent tart command's
Config.gc() can't delete it from under the running VM, and remove
the directory explicitly on VM exit (defer doesn't fire through
Foundation.exit).
- Extract DirectoryShare.collect() and add the drop zone as a named
"Dropped Files" share so the share-builder logic stays simple.
- Copy dropped files on a background queue so large drops don't
freeze the VM framebuffer (the view that handles the drop also
renders the VM).
- Reject unnamed --dir combined with drag-and-drop with a clear
error pointing at both fixes; document the same in --no-drag-and-drop
help.
- Add DirectoryShare.collect() tests covering empty, named, drop-zone-only,
unnamed-conflict, and custom-mount-tag cases.
* Remove disk v1 support
* fix: address PR review feedback
- add explicit error for legacy disk.v1 media type during pull
- include actionable re-push guidance in runtime error
🤖 Generated with [Codex](https://chatgpt.com/codex)
Co-Authored-By: Codex <codex@openai.com>
* Re-use legacyDiskV1MediaType in error message
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
Restore the applicationDidFinishLaunching method that was accidentally
removed in commit b1e88e1 ("tart run: do not remove 'Edit' menu as its
not present anymore").
That commit intended to remove the Edit menu removal code (since the
menu no longer exists), but also removed the crucial activation code:
- setActivationPolicy(.regular) - tells macOS this is a GUI app
- activate(ignoringOtherApps:) - brings the window to the foreground
Without these calls, the VM runs fine (SSH works) but no window appears
on screen.
Fixes#1181
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>