Files
ps5upload/client/src/lib/useWebviewDrop.ts
T
PhantomPtrandClaude Opus 5.5 05684fd0c0 6.1.0: Files works like FileZilla, per-console queue, fixes (#388)
* fix(connection): a console whose key changed opens the pairing dialog, not a 20 s retry

The helper check (step 2) carried the engine's sentence "a different device answered at
this address", not the ava1_wrong_console token, so it never opened the pairing dialog:
it polled for 20 s and told the user to send the helper again, which cannot help. The
sentence now counts as a pairing problem; step 2 stops polling and opens the dialog, which
offers "Forget the old one and pair this one".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(zip): a resumed whole file is finished when the next file arrives before its commit

After a resume, a file with every byte durable but no Done is in flight with
written == size; its commit writes the descriptor. When the next file's data reached
the sink first, append refused it ("file 6 ended at 1048576 of 1048576 bytes") and the
Stored-zip download failed instead of resuming (seen on main CI). The sink now finishes
that file itself, as commit would; a late commit is a no-op. Regression test added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ci): transfer tests lift their throttle once interrupted; optimize crypto deps in dev

ava1_transfer_integration ran past the 25-minute CI step: resumes after a planned
interruption kept crawling at the test's throttle, and the crypto/hash/compression
crates are 10-50x slower unoptimized. ChaosProxy gets a live set_bytes_per_sec; the
cancel-midway and mid-stream-drop tests lift the cap once the interruption has landed.
Only those dependency crates build at opt-level 3 in dev (our own code stays
debuggable). post_commit_failure_is_not_a_resend is capped so the destination is
created while data still flows. Locally: 251 s -> 98 s for the transfer suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the slow-disk label is not checked on CI runners, where the debug sender is slower than the disk

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* feat(files): add folders and drag-and-drop uploads; queue and activity follow the selected console

- Files: Add folder (uploads it whole into the open folder) and drag-and-drop of files and
  folders from the file manager, with a drop highlight. A dropped .pkg is copied there, not
  offered for install (/files now owns its drops).
- Upload's queue shows only the selected console's items (it showed every console's);
  Activity likewise, plus local-only work.
- New strings translated in all 21 locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* revert(ci): drop the dev opt-level overrides for crypto crates

Lifting the throttle after the planned interruption gives most of the speedup alone
(transfer suite 251 s -> 112 s locally; 98 s with the overrides). With the overrides the
resume-after-cancel tests failed under ASan and coverage on CI only (progress never
crossed the cancel threshold before the upload ended); main passes them. Not worth the
risk for 14 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the dest-ends relay test caps its source so the destination ends it mid-transfer

On a fast runner the uncapped 128 MiB relay finished before the destination's first
durable report (which is what ends its job). The source is now read at 16 MiB/s (~8 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* feat(files): right-click menu, keyboard shortcuts, column sorting, typed path

FileZilla-style browsing on the Files screen:
- right-click a row: Open, Download, Cut, Copy, Paste into (folders), Rename, Copy path, Delete
- double-click opens; Ctrl/Cmd+C/X/V/A, Delete, F2 rename, F5 refresh, Backspace up, Enter, Esc
- sort by Name, Size or Modified (folders first)
- the path bar is editable: type a path and press Enter
Paste can target any folder now (the row menu's Paste into). Strings in all 21 locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ava1): a lane that joins right after Welcome is no longer refused as an unknown session

Both servers sent Welcome before registering the session, and a client opens its lanes the
moment it is welcomed: under load a Join could beat the registration and be refused
("unknown session"), dropping the connection. The console's C server now registers the
session before Welcome goes out; the Rust server's lane waits briefly (2 s, within the join
deadline) for the registration. Seen in CI coverage (close_race, 48 concurrent sessions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* 6.1.0: version and changelog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ava1): a dropped upload job wakes its readers instead of leaking a parked thread

The sender's readers park on the read-ahead budget inside spawn_blocking; only closing it
wakes them, and that ran only on a normal exit. A job future dropped mid-job (a relay whose
destination ended, a torn-down session task) left a reader parked for good: a leaked
blocking thread that also kept its runtime from shutting down. That was the relay test that
hung CI on small runners (reproduced 3/4 on 2 CPUs in Docker; 8/8 pass with the fix). A drop
guard now stops and wakes the readers however the job ends. Regression test added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* docs(changelog): 6.1.0 lists the stopped-copy fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the launches test waits for a launch to start instead of a fixed 80 ms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:40:53 -07:00

76 lines
2.6 KiB
TypeScript

// Desktop drag-and-drop of a path onto a screen, through the webview's drop events (Tauri
// delivers host paths). Not on Android or in the browser build, where pickers cover it.
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { useEffect, useRef, useState } from "react";
import { isAndroid } from "./platform";
import { isTauriEnv } from "./tauriEnv";
import { engineIsOnThisDevice } from "../state/engine";
/** A .pkg is a package to install; AppShell's app-wide drop listener routes those. */
const isPackage = (path: string) => /\.pkg$/i.test(path);
/**
* Calls `onDrop` with the first dropped path while `enabled`; returns whether a drag is over
* the window, for highlighting a drop zone. The subscription is cleaned up even when it
* resolves after unmount, as Upload's is.
*/
export function useWebviewDrop(onDrop: (path: string) => void, enabled: boolean): boolean {
return useWebviewDropCore(
(paths) => {
const first = paths[0];
if (first && !isPackage(first)) onDrop(first);
},
enabled,
);
}
/**
* Every dropped path, packages included, while `enabled` (the Files screen copies whatever is
* dropped into the open folder). Returns whether a drag is over the window.
*/
export function useWebviewDropAll(onDrop: (paths: string[]) => void, enabled: boolean): boolean {
return useWebviewDropCore((paths) => {
if (paths.length > 0) onDrop(paths);
}, enabled);
}
function useWebviewDropCore(onDrop: (paths: string[]) => void, enabled: boolean): boolean {
const [active, setActive] = useState(false);
const latest = useRef({ onDrop, enabled });
// The subscription outlives renders; it reads the newest handler through this ref.
useEffect(() => {
latest.current = { onDrop, enabled };
});
useEffect(() => {
// A dropped path is on this device; a remote engine can't open it.
if (!isTauriEnv() || isAndroid() || !engineIsOnThisDevice()) return;
let unlisten: (() => void) | null = null;
let cancelled = false;
const p = getCurrentWebview().onDragDropEvent((e) => {
if (cancelled) return;
const t = e.payload.type;
if (t === "enter" || t === "over") setActive(latest.current.enabled);
else if (t === "leave") setActive(false);
else if (t === "drop") {
setActive(false);
if (latest.current.enabled) latest.current.onDrop(e.payload.paths ?? []);
}
});
p.then((fn) => {
if (cancelled) fn();
else unlisten = fn;
}).catch(() => {
// The webview went away before the subscription completed: nothing to undo.
});
return () => {
cancelled = true;
unlisten?.();
};
}, []);
return active;
}