You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(upload): Ctrl+V clipboard paste with preview, confirm, and progress on the web page (#72)
Pasting an image on the upload page now stages it instead of doing nothing:
a preview panel shows the image, its size, and which project it will be
sent to, and nothing uploads until the user confirms with Send. Send stays
disabled until a project pill is picked (the destination line says so),
picking a pill live-updates the target, and the upload itself runs over
XHR so a real progress bar + percentage track it, ending in a confirmed
"Pasted into <project> ✓" state (or a Retry-able error state). Cancel
discards the staged image; pastes are ignored mid-flight so an in-progress
upload can't be swapped out from under itself.
Verified live in a real Chrome (DevTools protocol) against the built page:
staging, gating, destination update, progress mid-states, success state,
auto-clear, error/Retry path, and Cancel all behave as specified.
Tests: a unit pin for the paste-UI page contract (panel, dest, bar,
Send/Cancel, paste listener), and a new browser-tier Playwright test that
synthesizes a ClipboardEvent with a real PNG and proves the full flow ends
with the byte-identical paste-*.png on disk plus live tmux injection.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,7 +12,7 @@ Open every project in its own terminal, launch an AI agent in it, and auto-tile
12
12
- The `needs_ssh` tier is CI-only by design: `tests/e2e/test_ssh.py` (dry-run pins) + `tests/e2e/test_ssh_real.py` (REAL wire — every test traverses the loopback sshd that `.github/actions/setup-ssh-server` provisions, exporting `MDTEST_SSH_PORT`/`MDTEST_SSH_KEY`/`MDTEST_SSH_HOST` and an `mdssh` alias in the runner's real `~/.ssh/config`). Locally these skip because the env vars are absent — **never install/enable an SSH server on a dev machine to run them**. Tests that seed the ssh user's real HOME (the Windows attach flagship — attach can't inject `--config` remotely) are additionally gated on `GITHUB_ACTIONS=true` / `MDTEST_ALLOW_REAL_HOME=1`. macOS window-over-SSH legs are a loud `::warning` skip (see DESIGN.md known debt).
13
13
- dist (packaged-install) locally: `pip install build` (not a dev extra; CI installs it per-job) then `pytest tests/dist/ -m dist` — builds a wheel, installs it into a pristine no-extras venv, and drives the real installed `multideck` entry point (proves the exact `pip install` user env; import-sweep catches dev/optional-dep leaks). Window-spawn leg is win32-only and skips elsewhere; wheel build + import sweep + virgin first-run + socket-real serve run on every OS.
14
14
- Real-PTY menu tier (`tests/e2e/test_pty_menu.py`, marker `pty`, rides the existing `end-to-end` CI job on all 3 OSes): drives the installed `python -m multideck` under a genuine pseudo-terminal — pexpect on POSIX, pywinpty/ConPTY on Windows (`pywinpty>=2.0; sys_platform=='win32'` in the `dev` extra) — via the uniform driver `tests/e2e/_pty.py`, asserting on the plain on-screen text (ANSI stripped; children run `NO_COLOR=1`) and, for first-run, the valid config written to disk. This is the only test of the real `sys.stdin.isatty()` interactive path (`CliRunner` fakes the tty). Locally it runs anywhere pexpect/pywinpty resolve.
15
-
- Browser-upload tier (`tests/e2e/test_upload_browser.py`, marker `browser`, dedicated `browser-upload` ubuntu job, CI-only, gated on `MDTEST_BROWSER=1`): a real headless Chromium (Playwright — the `browser` extra `["playwright>=1.40"]`, NOT in `dev`, provisioned only by that job via `playwright install --with-deps chromium`) drives the real mobile upload page against a real `multideck serve` on loopback and asserts the on-disk upload is byte-identical + the page title/form contract. Locally the module skips cleanly (no `MDTEST_BROWSER`, no Playwright). Honest gap: it symlinks real `tmux` as `psmux` and stands up a real tmux session as the multiplexer shim (Linux has no `psmux` binary) — see DESIGN.md.
15
+
- Browser-upload tier (`tests/e2e/test_upload_browser.py`, marker `browser`, dedicated `browser-upload` ubuntu job, CI-only, gated on `MDTEST_BROWSER=1`): a real headless Chromium (Playwright — the `browser` extra `["playwright>=1.40"]`, NOT in `dev`, provisioned only by that job via `playwright install --with-deps chromium`) drives the real mobile upload page against a real `multideck serve` on loopback and asserts the on-disk upload is byte-identical + the page title/form contract + the Ctrl+V clipboard-paste confirm flow (synthetic ClipboardEvent → staged preview with Send gated on project selection → XHR upload → byte-identical `paste-*.png` on disk). Locally the module skips cleanly (no `MDTEST_BROWSER`, no Playwright). Honest gap: it symlinks real `tmux` as `psmux` and stands up a real tmux session as the multiplexer shim (Linux has no `psmux` binary) — see DESIGN.md.
16
16
- The `needs_tailscale` tier is CI-only by design (`tests/e2e/test_tailnet_real.py` + the non-required `tailnet` ubuntu job): it joins a REAL ephemeral, tag-scoped Tailscale node (`tailscale/github-action`, SHA-pinned, OAuth via `TS_OAUTH_CLIENT_ID`/`TS_OAUTH_SECRET` + `tag:ci`) and exercises `tailnet.py`'s live `tailscale` probes (`ip4`/`magicdns_host`/`probe`) plus `multideck serve`'s Tailscale-facing default bind (`/health` answers on the real Tailscale IP; the LAN wildcard is proven unbound). Gated on `MDTEST_TAILSCALE=1` — locally it skips because the var is absent; **never install/join Tailscale on a dev machine to run it** (same posture as `needs_ssh`). The OAuth secrets don't exist yet: the job detects their absence and skips LOUDLY (`::warning`, job stays green, forks never fail) — once the user adds them it goes fully live with zero code change (one-time setup steps in the job comment). Excluded from the `end-to-end` job's SSH-less run exactly like `needs_ssh`.
17
17
- The `monitor_lab` tier is CI-only by design (`tests/platform/test_monitor_lab_tiling.py` + the `monitor_lab` CI job, windows-latest): it INSTALLS the parsec-vdd virtual-display driver to fabricate a mixed-DPI multi-monitor topology, then drives real `--go` tiling across the virtual monitors and asserts each window rect lands in its `compute_grid` cell (physical pixels). Gated on `MDTEST_MONITOR_LAB=1` — locally it skips because the var is absent; **never install the driver / set the var on a dev machine to run it** (same posture as `needs_ssh`). The lab engine is `tests/platform/monitor_lab.py` (stdlib ctypes, win32-only, import-harmless on POSIX). The offline half — `tests/unit/test_monitor_lab_topologies.py` feeding committed golden topologies (`tests/platform/fixtures/topologies/*.json`) into `compute_grid` — runs everywhere in the normal unit gate. Two more CI-only Windows interaction tiers ride in the platform-integration windows leg (same never-on-a-dev-box posture): the `wt_cold` tier (`tests/platform/test_wt_cold_start.py`, gated `MDTEST_WT_COLD=1`) pins the first-ever `wt` launch on the fresh runner against the default settle+retry tiling budget — coldness is guaranteed by CI step ordering, so it must stay the first step that can spawn wt; and the `interaction` tier (`tests/platform/test_real_hotkey.py`, gated `MDTEST_INTERACTION=1`) drives the REAL Alt+V chain — product-spawned listener, real `SendInput` chord, real CF_DIB clipboard, real `serve` + psmux inject — and would stomp a dev machine's clipboard/keystrokes, so never set the var locally. The windows leg also installs pinned psmux (v3.3.6 release zip, sha256-verified, into `%LOCALAPPDATA%\psmux`) so `tests/platform/test_real_psmux.py` (lifecycle + full attach chain in a real wt window) runs instead of skipping. Riding in the same CI job (shared `lab` fixture + driver install via `tests/platform/conftest.py`): the doctor-replay tier — `multideck doctor --json` emits monitor topology under a `monitors` key, `tests/platform/doctor_replay.py` plans an achievable lab topology from a bug-report blob, `tests/platform/test_doctor_replay.py` replays committed sample reports (`tests/platform/fixtures/doctor_reports/*.json`) live, and `tests/unit/test_doctor_replay_offline.py` pins the parse/plan/slot math (incl. negative origins) everywhere.
18
18
- Real-writer contract tier (`tests/e2e/test_state_sink_contract.py`, marker `node_contract`, rides the existing `end-to-end` job on all 3 OSes — node is preinstalled on every hosted runner, so no CI change): `npm install`s the pinned real agent-state writer package (`ai-agent-notifier@1.0.6` — see `AINS_VERSION`) into a tmp prefix, drives its REAL hook (`src/notify.mjs`) under real node with realistic Claude `Stop`/`Notification`/`SessionStart` events and a fully-redirected HOME, then checks multideck's real `agent_state` reader against it. It PINS a headline finding (F-NC-001): the package writes ZERO `~/.multideck/state/` records (it is a pure notifier; there is no `state-sink.mjs`) — so the tier currently asserts the store stays empty and flips RED if a wired hook ever starts writing records. Skips cleanly if node/npm or the npm registry are unavailable; tmp-dir only, never touches real `~/.multideck`/`~/.claude`.
`multideck serve` (or `uploadServer: true` during launch) starts a small HTTP server; `multideck mobile` prints the phone URL + a QR code you can install as a home-screen app (the QR code needs the optional `qr` extra: `pip install multideck[qr]`). Pick a project on the phone, upload an image, and its path is pasted into that project's session. The Alt+V hotkey (Windows) does the same for whatever `md:` session is focused.
145
+
`multideck serve` (or `uploadServer: true` during launch) starts a small HTTP server; `multideck mobile` prints the phone URL + a QR code you can install as a home-screen app (the QR code needs the optional `qr` extra: `pip install multideck[qr]`). Pick a project on the phone, upload an image, and its path is pasted into that project's session. On a desktop browser you can also **Ctrl+V** an image from the clipboard: the page stages it with a preview showing which project it will go to, waits for you to confirm with **Send**, and shows live upload progress until the "pasted into …" confirmation. The Alt+V hotkey (Windows) does the same for whatever `md:` session is focused.
146
146
147
147
This works **over Tailscale**: the server binds only the loopback and your machine's Tailscale IP — never the LAN wildcard — and `attach`/`mobile`/`termius` shell out to the `tailscale` CLI to resolve hosts. Devices must be on your tailnet; there is deliberately no auth token, since the bind set is the access control. To bind something else (e.g. LAN-wide), use the escape hatch: `multideck serve --host 0.0.0.0`.
0 commit comments