vercel-labs / vercel-labs/native
file drops stop one tier short of TypeScript cores: both hosts deliver `files_dropped`, but the ts-core wiring has no `dropMsg` channel
Nobody has claimed this yet.
- Dominant language
- Zig
- Stars
- 7.7k
- Forks
- 314
- Avg merge
- 5h
- Merged PRs (30d)
- 13
Description
The desktop hosts fully implement file drag/drop — macOS registers for NSPasteboardTypeFileURL and answers performDragOperation, GTK wires a GtkDropTarget — and the platform/runtime tiers carry the event all the way to widget routing. But the TypeScript-core wiring surface stops one tier short: the entry contract (commandMsg/keyMsg/frameMsg/appearanceMsg/chromeMsg/envMsgs) has no drop channel, so a ts-core app can never hear a dropped file. The feature is finished everywhere except the tier most apps are written in.
The mechanism
The delivery pipeline exists end to end below the core:
- macOS host:
src/platform/macos/appkit_host.m:7393—[window registerForDraggedTypes:@[ NSPasteboardTypeFileURL ]], withdraggingEntered:/performDragOperation:at 1186/1191 (and again for webview surfaces at 7722/8405). - GTK host:
src/platform/linux/gtk_host.c:2278—g_signal_connect(target, "drop", G_CALLBACK(native_sdk_on_file_drop), win), handler at 2240. - Platform vocabulary:
src/platform/types.zig— thefile_dropscapability (:101),FileDropEvent(:1524),WindowOptions.drop_files(:2119),PlatformEvent.files_dropped(:2248), andsplitDropPathswith itsmax_drop_paths_bytesbound (:2294, :248). - Runtime:
src/runtime/api.zig—AppEvent.files_dropped(:340) andcanvas_widget_file_drop(:347) withCanvasWidgetFileDropEvent(:145). - Widget routing:
src/primitives/canvas/events.zig:386—WidgetFileDropEvent;routeFileDropEventtargets widgets carryingsemantics.actions.drop_files, with tests pinning the routing discipline (widget_layout_tests.zig:2327— "widget file drop route ignores missing paths disabled and non-drop targets"). - Automation already speaks it:
src/runtime/automation_commands.zig:374parses thedrop-fileswidget action (canvas 9 drop-files /tmp/report.csv /tmp/chart.png, :563), andautomation_widget_dispatch.zig:708(dispatchAutomationCanvasWidgetFileDrop) synthesizes the platform event — so the test seam for drops exists today.
And then the ts-core tier drops it on the floor:
src/app_runner/ts_core_main.zigwireson_command, key, frame, appearance, chrome, and env channels (if (comptime @hasDecl(core, "commandMsg")) ... options.on_command = core.commandMsg;at :88–91) — grep the file fordrop: nothing.packages/core/src/emitter.ts/checker.ts— no drop channel in the transpiler either.- The markup handler vocabulary (
on-press,on-toggle,on-input,on-dismiss,on-hold,on-scroll,on-resize, ...) has noon-drop, and nothing in markup can setsemantics.actions.drop_fileson a widget.
There is no workaround channel: commandMsg(name) carries a bare name and no payload, the clipboard is a dead end for this (a Finder-copied file puts no plain-text path on the pasteboard, so Cmd.clipboardRead reads nothing), and a ts-core tree cannot add a Zig view to catch the event (src/core.ts + src/main.zig in one tree is a teaching error, by design).
What it costs
"Drag an image into the app" is table stakes for any media tool, and a ts-core app cannot build it. Concrete case: an image-generation studio app whose composer takes a reference image for img2img. The obvious interaction — drag a PNG from Finder onto the composer — is unimplementable at this tier. The shipped fallback is a spawned osascript choose file dialog: it works, but it is a click-through picker standing in for the interaction users reach for first, and every ts-core media app will re-discover this wall.
The ask
An app-shell channel mirroring the existing wiring exports, gated exactly like commandMsg is today (@hasDecl at ts_core_main.zig:88):
// each dropped file dispatches the named arm as one journaled Msg:
// a record with exactly one Uint8Array field (the POSIX path bytes) —
// the same delivery discipline envMsgs already established.
export const dropMsg = "file_dropped";
- Presence of the export sets
WindowOptions.drop_fileson the shell window, so apps that don't declare it keep today's behavior. - One Msg per path (multi-file drops dispatch N times, or the record gains
index/countnumbers) keeps the arm shape inside the existing checked-record vocabulary — no new payload kind needed. - Deliveries journal as ordinary Msgs, so recorded sessions replay byte-identically offline — again the
envMsgsprecedent. - Per-widget targeting (a markup
on-dropthat setssemantics.actions.drop_filesand routes through the existingrouteFileDropEvent) would be a welcome second step, but the app-level channel is the 90% win — the model can decide what a drop means from its own state, exactly as it does forkeyMsg.
The automation seam (widget-action <view> <id> drop-files <paths>) means the feature ships already testable: a ts-core app's e2e suite could drive drops the day the channel exists.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/app_runner/ts_core_main.zig and compare its existing message wiring with src/platform/types.zig, src/runtime/api.zig, and the widget routing files. Then inspect packages/core/src/emitter.ts and checker.ts for the TypeScript contract and use the existing automation drop-files seam as the test entry point. Done means the agreed app-level dropMsg behavior is wired and covered by replayable tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, zig
- Domain
- desktop, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100