Follow symlinks to directories in WSL Tab completion, plus a host-attribute probe (APP-3993) - #14755
Open
warp-agent-staging[bot] wants to merge 5 commits into
Open
Follow symlinks to directories in WSL Tab completion, plus a host-attribute probe (APP-3993)#14755warp-agent-staging[bot] wants to merge 5 commits into
warp-agent-staging[bot] wants to merge 5 commits into
Conversation
Instrumentation only, not a fix. WSL symlinked directories do not tab-complete as directories, but no factory runner can execute a WSL path, so this adds debug-level, greppable diagnostics so the requester can build a dogfood build, reproduce on their WSL host, and return logs that reveal the mechanism. Instruments the local directory-listing path: - app/src/completer/mod.rs list_directory_entries_internal: the SessionType branch taken, the guest directory vs the converted native host path, the read_dir error (kind) instead of swallowing it, and the resulting entry count. - crates/warp_completer path.rs EngineDirEntry::try_from: the raw is_dir/is_file/is_symlink flags per entry, and — in the symlink branch — replaces unwrap_or(false) with a match that logs metadata() Ok(is_dir) vs Err(kind), preserving the identical classification result. All lines are debug level (off by default), use safe_* for anything carrying a path or file name (redacted in release), and share the prefix [APP-3993 symlink-completion]. Classification behavior is unchanged; a new warp_completer test asserts try_from still classifies a local symlink-to-dir as Directory and a symlink-to-file as File. Co-Authored-By: Warp Agent <agent@warp.dev>
…n round-trip Reporter logs (from the instrumentation this branch added) established the mechanism: a WSL session lists via SessionType::Local, the entry is a symlink, but value.path().metadata() returns Err(NotFound) because the target is an IO_REPARSE_TAG_LX_SYMLINK the Windows host cannot follow. std::fs::read_link cannot read that reparse tag either (Rust std only handles IO_REPARSE_TAG_SYMLINK / MOUNT_POINT), so host-side resolution is impossible and unwrap_or(false) bucketed the symlink as a file. Fix (surgical, guest-side): after the host std::fs listing, if the session is emulated (WSL/MSYS2) and any symlink was left unclassified as a directory, ask the guest which immediate children are directories with `cd <dir> && find -L . -maxdepth 1 -type d -print0` (the same -L shape that fixed the remote path in #14746) and upgrade the matching symlink entries. Nothing from the listing is interpolated into the command, so there is no filename-quoting or injection surface. Latency: the command runs only for emulated sessions and only when an unresolved directory symlink is present; it is awaited on the async completion path (the sibling WarpifiedRemote branch already awaits a guest command here) and bounded with with_timeout, degrading to the host classification (symlink shown as a file) on timeout/error rather than stalling completion. Results are cached. Plain local macOS/Linux is unaffected. Keeps one temporary debug-gated read_link probe so the reporter's verification run also records read_link's actual result on their WSL host. Adds unit tests for the output parsing and the directory-symlink upgrade (the Err-from-metadata case); the local try_from behavior-preservation test is retained. Co-Authored-By: Warp Agent <agent@warp.dev>
…nd trip `FileTypeExt::is_symlink_dir()` reads FILE_ATTRIBUTE_DIRECTORY on the link entry itself, so it is not affected by the NotFound that `metadata()` hits on a WSL LX symlink over \\wsl$. Whether that bit is actually set there is unknown, and the existing log cannot answer it: std's `FileType::is_dir()` is `!is_symlink() && is_directory()`, false by construction for any name-surrogate reparse point. Record, for every symlink entry on Windows, `is_symlink_dir()` / `is_symlink_file()` from both the directory enumeration and an `fs::symlink_metadata()` of the link, plus the raw `MetadataExt::file_attributes()` bits. The guest round trip stays in place until a dogfood run reports back. Classification is unchanged: `metadata()` Ok still yields `is_dir()` and Err still yields false. Co-Authored-By: Warp Agent <agent@warp.dev>
…l-symlink-logging
…tion The wasm `Timer` holds a non-Send future, so awaiting `with_timeout` made `PathCompletionContext::list_directory_entries` non-Send and broke both wasm CI gates. Move the guest classification into a `cfg(windows)` module, leaving every other target with the plain host listing it had before. Narrow the runtime gate to `is_wsl()`. There is no MSYS2 report or repro on this issue, and MSYS2's symlink modes are either real NTFS symlinks the host resolves or plain files that never reach this path, so the guest round trip would have run there for nothing. Co-Authored-By: Warp Agent <agent@warp.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes WSL Tab completion of a symlink that points at a directory (
cd link<Tab>completed it as a file instead of a directory). This is the WSL half of #4498; the remote/SSH half was fixed in #14746 (merged) and plain local macOS/Linux was already correct, so this completes the issue.Mechanism (confirmed from instrumented logs on the reporter's real WSL host): a WSL session lists via
SessionType::Local, andstd::fs::read_dircorrectly reports the entry as a symlink, butvalue.path().metadata()returnsErr(NotFound)— the target is anIO_REPARSE_TAG_LX_SYMLINKthat the Windows host cannot follow over the\\wsl$(Plan 9) filesystem.std::fs::read_linkcan't read that reparse tag either (Rust std only handlesIO_REPARSE_TAG_SYMLINK/MOUNT_POINT), so the oldunwrap_or(false)bucketed the symlink as a file. (A prior host-side fallback shipped under APP-5190 failed for exactly this reason and was removed.)Fix (guest-side, Windows-only): the WSL mechanism lives in
app/src/completer/wsl_symlinks.rs, compiled only under#[cfg(windows)]. After the hoststd::fslisting, if the session is WSL and at least one symlink was left unclassified as a directory, it asks the guest which immediate children are directories withcd <dir> && find -L . -maxdepth 1 -type d -print0— the same-Lshape that fixed the remote path in #14746 — and upgrades the matching symlink entries. Nothing from the listing is interpolated into the command, so there is no filename-quoting or injection surface. Every non-Windows target keeps the exact host listing it had before this PR, and broken links / loops stay non-directories.Also in this PR: a probe that may let the guest round trip be deleted entirely. See "Open question" below — the fix as written is what ships if the probe comes back negative.
Open question — can a host-only check replace the guest round trip?
std::os::windows::fs::FileTypeExt::is_symlink_dir()was never evaluated when this fix was written. In std's Windows implementation it reduces tois_symlink() && is_directory(), andis_directory()reads theFILE_ATTRIBUTE_DIRECTORYbit on the link entry itself — it never follows the target, so theNotFoundthat killsmetadata()does not apply to it. Verified against the pinned toolchain's source (rust 1.92.0,library/std/src/sys/fs/windows.rs):FileType::new(attributes, reparse_tag)setsis_directory = attributes & FILE_ATTRIBUTE_DIRECTORY != 0andis_symlink = FILE_ATTRIBUTE_REPARSE_POINT && (reparse_tag & 0x20000000)(the name-surrogate bit).is_dir()is!is_symlink && is_directory— false by construction for any name-surrogate reparse point, which is why the existing instrumentation could never answer this.is_symlink_dir()isis_symlink && is_directory;is_symlink_file()isis_symlink && !is_directory.DirEntry::file_type()is built entirely from theWIN32_FIND_DATAWthe enumeration already returned, so on Windows that check costs no syscall at all.What is not established: whether the
\\wsl$redirector actually setsFILE_ATTRIBUTE_DIRECTORYon anIO_REPARSE_TAG_LX_SYMLINK. That is a property of the redirector, not of std, and no runner in the fleet can execute it. This PR does not answer it — the reporter's next dogfood run does.So this PR adds a probe (Windows-only,
debug-gated, behavior-preserving) that records, for every symlink entry, the raw evidence:is_symlink_dir()/is_symlink_file()from the directory enumeration (entry_*) and from anfs::symlink_metadata()of the link (link_*) — a redirector may fill the two differently, andsymlink_metadataopens the link withFILE_FLAG_OPEN_REPARSE_POINT, which can itself fail, so its failure is recorded too;MetadataExt::file_attributes()bits, so the answer does not rest on a derived boolean;read_linkoutcome, retained deliberately so the same run records its real behavior instead of leaving us on inference.If that run shows
FILE_ATTRIBUTE_DIRECTORYset, the fix collapses to a couple of lines inEngineDirEntry::try_from— no guest command, no 3s timeout, no shell on the typing hot path — which is strictly better. Until then the guest round trip stays.Rework changes
Three review findings addressed in one cycle.
with_timeoutmadePathCompletionContext::list_directory_entriesnon-Send: on wasm,warpui::r#async::Timerholds aPin<Box<dyn Future<Output = Instant>>>with noSendbound, and#[async_trait]requires aSendfuture, soFormatting + Clippy (wasm)andVerify compilation with release flags (wasm)both failed withE0277. The whole guest mechanism moved out ofapp/src/completer/mod.rsinto a new#[cfg(windows)] mod wsl_symlinks, so no non-Windows target compiles the timeout at all; theSessionType::Localarm now selects between the WSL path and the original upstream one-line listing withcfg_if!. Both wasm gates verified green locally — see Testing.is_wsl()only. There is no MSYS2 report, log, or repro on this issue, APP-5190, or Follow symlinks with Tab autocomplete (Linux client) #4498 — it was added for symmetry with WSL, not from evidence. It is also unlikely to be affected: withMSYS=winsymlinks:nativestrictMSYS2 creates real NTFS symlinks the host follows correctly (sometadata()succeeds and the entry is never a candidate), and its default copy /:lnk/:sysmodes produce plain files or directories that are not reparse points at all (sofile_type.is_symlink()is false and the entry is never a candidate either). Keeping it would have run an unverified guest command in an out-of-scope environment for no benefit.None-degrades-rather-than-stalls contract, the-Land injection-surface reasoning, the.entryfindemits, the guard that only unresolved symlinks are upgraded, and the probe's purpose. No comments remain in any test file.The probe's behavior-preserving property and the
read_linkline are unchanged.Linked Issue
Closes #4498
ready-to-specorready-to-implement.Testing
What was executed here
cargo clippy --locked --target wasm32-unknown-unknown --profile release-wasm-debug_assertions -- -D warnings→ exit 0./script/wasm/bundle --channel oss --nouniversal --check-only→ exit 0 (the onlywarplib warning is pre-existing, inapp/src/terminal/model/grid/resize.rs)./script/format --check;./script/check_no_inline_test_modules;cargo clippy -p warp --all-targets --tests -- -D warnings;cargo clippy -p warp_completer --all-targets --tests -- -D warnings— all pass.cargo nextest run -p warp completer::(12/12) andcargo nextest run -p warp_completer engine::path(19/19).crates/warp_completer/.../path_tests.rs::test_engine_dir_entry_classifies_symlink_targets(behavior preservation):try_fromstill classifies a symlink-to-dir asDirectoryand a symlink-to-file asFile. The probe only reads; classification ismetadata()Ok→is_dir(),Err→false, exactly as before.test_parse_directory_names,test_upgrade_directory_symlinks— the latter is the regression for the Err-from-metadatapath) moved intowsl_symlinks_tests.rswith the module, so they are Windows-only now and run in CI'sRun Windows testsjob rather than in the Linux/macOS runs. That matches the mechanism they cover, which only exists on Windows.#[cfg(windows)], so Linux clippy does not cover it. It was compile-checked separately against real Windows std for the pinned toolchain (rustc --target x86_64-pc-windows-msvc, edition 2024, including the call-site borrow/cfg shape) — clean, no warnings. CI's Windows Clippy job also compiles it..awaited on the async completion task, exactly like the siblingSessionType::WarpifiedRemotebranch of the same function which already awaits a guestfindfor every remote path completion, so it does not block the render/UI thread. It is bounded withwith_timeout(3s); on timeout, error, or non-success it returns the host classification (today's behavior) rather than stalling, andlist_directory_entriescaches the result.What could NOT be executed
No Windows or WSL runner exists in the fleet, so the WSL code path was never run. Nothing here should be read as verification that WSL Tab completion is fixed, or as an answer to the
is_symlink_dir()question above. The reporter's dogfood run is the only real verification../script/runSteps for the reporter
factory/app-3993-wsl-symlink-logging, head79293ba). Dogfood matters: thesafe_*macros only emit the detailedfull:arm on dogfood; a release build logs the redacted arm and hides the path.RUST_LOG=warp_completer::completer::engine::path=debug,warp::completer=debugin the environment before startingWarp.exe(the default level isInfo, so the probe is silent without this).mkdir realdir && ln -s realdir linkdir, then typecd linkand press Tab. Note whetherlinkdir/completes as a directory (trailing separator, offered forcd).[APP-3993 symlink-completion]lines fromwarp.log(or the whole log-bundle zip) around that Tab press. One line per symlink entry, of the form:[APP-3993 symlink-completion] path=… metadata=Err(NotFound) read_link=Err(…) entry_symlink_dir=… entry_symlink_file=… link_symlink_dir=… link_symlink_file=… link_attributes=0x…The decisive fields are
entry_symlink_dir/link_symlink_dirand the rawlink_attributesforlinkdir.0x10(FILE_ATTRIBUTE_DIRECTORY) set inlink_attributesmeans the guest round trip can be deleted and replaced with a host-only check.Screenshots / Videos
Not applicable — no rendered UI surface, and the affected path cannot be exercised on any runner here.
Agent Mode
CHANGELOG-BUG-FIX: Tab autocomplete now follows symlinks to directories in WSL sessions.
Refs #4498
Originating thread: https://warpdev.slack.com/archives/C0BDQDW8V5E/p1785963086148259
Conversation: https://staging.warp.dev/conversation/fc706517-d258-4bba-abdf-5e503c445682
Run: https://oz.staging.warp.dev/runs/019fd965-3ef5-72dc-b946-89c45e8d8063
This PR was generated with Oz.