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>
* Avoid duplicate progress updates in CI logs
* Update Sources/tart/Logging/ProgressObserver.swift
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
---------
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
* ASIF is available only starting from macOS 26 (Tahoe)
* Remove testRawFormatIsAlwaysSupported() test
* Fix testASIFFormatSupport() test to check for macOS 26+
* feat: prioritize pruning of old SHA when pulling updated tags
When pulling a new version of a tagged image (e.g., ghcr.io/cirruslabs/macos-runner:sonoma),
set the access date of the previous SHA to epoch time (1970-01-01). This ensures that the
old SHA will be prioritized for pruning, even if it was accessed more recently than other
cached images.
This helps manage disk space more efficiently by automatically cleaning up superseded
versions of frequently-updated tagged images.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* format
* Review comments
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Add disk image format selection with ASIF support
* fixed goreleaser-pro
* Fix ASIF disk format compatibility issues
- Use .uncached caching mode for ASIF disks to avoid Virtualization framework compatibility issues
- Improve caching mode selection logic for better maintainability
- Fix compiler warning by changing var to let for attachment variable
This resolves VM startup failures when using ASIF disk format by ensuring proper disk attachment configuration.
* Update goreleaser installation to use tap-specific formula
Change from 'brew install --cask goreleaser-pro' to 'brew install --cask goreleaser/tap/goreleaser-pro' for proper installation from the official goreleaser tap.
* Remove VS Code configuration and add to gitignore
- Remove .vscode/launch.json from repository
- Add .vscode/ to .gitignore to prevent VS Code settings from being tracked
* Implement ASIF disk resize using diskutil
- Add support for resizing ASIF disk images using diskutil image resize
- Detect disk format from VM config and route to appropriate resize method
- Use diskutil image info to get current ASIF disk size and validate resize
- Remove restriction that prevented ASIF disk resizing in Set command
- Add FailedToResizeDisk error case for proper error handling
- Maintain backward compatibility with raw disk resizing
- Add comprehensive size validation to prevent data loss
* Update Sources/tart/Commands/Create.swift
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
* Update Sources/tart/DiskImageFormat.swift
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
* Update Sources/tart/DiskImageFormat.swift
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
* Fix test naming and remove redundant test cases
- Rename testFormatArgument to testCaseInsensitivity for clarity
- Remove redundant 'raw' and 'invalid' test cases already covered in testFormatFromString
- Remove testFormatDescriptions test as it's not very useful
Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152093510
* Remove canCreate property and simplify DiskImageFormat
- Remove canCreate property since it's the same as isSupported
- Remove description property entirely as it's not used
- Fix displayName for RAW format (remove UDIF reference)
- Remove checkDiskutilASIFSupport helper function
Addresses review comments:
- https://github.com/cirruslabs/tart/pull/1094#discussion_r2152109450
- https://github.com/cirruslabs/tart/pull/1094#discussion_r2152115610
- https://github.com/cirruslabs/tart/pull/1094#discussion_r2152124330
* Update Create command validation and help text
- Simplify ArgumentParser help text to let it show possible values automatically
- Remove canCreate validation since property was removed
- Simplify error message for unsupported disk formats
Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152113480
* Add disk format validation to Run command
- Add validation to ensure ASIF disk format is supported on current system
- Check disk format compatibility before attempting to run VM
Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152109450
* Use proper namespaced constant for OCI label
- Add diskFormatLabelAnnotation constant in Manifest.swift
- Use org.cirruslabs.tart.disk.format namespace for consistency
- Use variable shadowing instead of new variable name for labels
Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152163515
* Remove special ASIF caching mode
- Remove .uncached caching mode for ASIF disks
- Use default caching logic for all disk formats
- Testing shows .cached mode works fine on macOS 26.0
Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152133589
* Improve code structure in VMDirectory
- Use guard let instead of nested if let for better readability
- Reduce nesting in resizeASIFDisk function
- Improve error handling flow
Addresses review comment: https://github.com/cirruslabs/tart/pull/1094#discussion_r2152141916
* diskFormatLabel
* reverted caching mode
* Use PropertyListDecoder
---------
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
* tart exec: explain that Tart Guest Agent is required
Also handle decrease the connection timeout to 1 second
and provide a hint to the user.
* execute() can be made private
* Include error.localizedDescription
* Introduce "tart exec" command as an alternative to SSH
* Simplify control socket machinery by using NIO async/await primitives
* No reason to print the "vm" object directly, just refer to it as "VM"
* Log to Apple’s Unified Logging System
* Enable clipboard sharing on macOS too
And document which packages need to be installed on these operating
systems.
* We now use Tart Guest Agent
Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>
---------
Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>