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

Open
#256 1 comment 0 reactions 0 assignees View on GitHub

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 ]], with draggingEntered:/performDragOperation: at 1186/1191 (and again for webview surfaces at 7722/8405).
  • GTK host: src/platform/linux/gtk_host.c:2278g_signal_connect(target, "drop", G_CALLBACK(native_sdk_on_file_drop), win), handler at 2240.
  • Platform vocabulary: src/platform/types.zig — the file_drops capability (:101), FileDropEvent (:1524), WindowOptions.drop_files (:2119), PlatformEvent.files_dropped (:2248), and splitDropPaths with its max_drop_paths_bytes bound (:2294, :248).
  • Runtime: src/runtime/api.zigAppEvent.files_dropped (:340) and canvas_widget_file_drop (:347) with CanvasWidgetFileDropEvent (:145).
  • Widget routing: src/primitives/canvas/events.zig:386WidgetFileDropEvent; routeFileDropEvent targets widgets carrying semantics.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:374 parses the drop-files widget action (canvas 9 drop-files /tmp/report.csv /tmp/chart.png, :563), and automation_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.zig wires on_command, key, frame, appearance, chrome, and env channels (if (comptime @hasDecl(core, "commandMsg")) ... options.on_command = core.commandMsg; at :88–91) — grep the file for drop: 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 no on-drop, and nothing in markup can set semantics.actions.drop_files on 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_files on 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/count numbers) 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 envMsgs precedent.
  • Per-widget targeting (a markup on-drop that sets semantics.actions.drop_files and routes through the existing routeFileDropEvent) 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 for keyMsg.

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.