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>
* [docs] publish press release about licensing violation
Highlighting that this is an exceptional case since the company did contact us about a license, and we explicitly declined due to conflict of interests.
* Fixed linting
* Use ghcr.io/cirruslabs/macos-tahoe-base:latest
* CI: "Test on Sequoia" can be named just "Test"
* integration-test: can use latest requests now that the bug is fixed
* 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>
"How Tart is different from Anka" is not a question, and thus should not have a question mark. This PR proposes to change it into a question, but an equally valid fix is to drop the question mark.