Zed remote extension fails to start streamd because no binary is installed #127
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Description:
When using the
streamdZed extension through Zed Remote Development in WSL, the language server fails to start:The extension is installed inside the WSL remote environment:
However, its work directory is empty:
The extension implementation currently returns a bare
streamdcommand on non-Windows platforms:In the remote-extension context, Zed resolves this to the extension work path:
but no executable is downloaded, copied, or built there.
Expected behavior:
The extension should install or download an appropriate platform-specific
streamdbinary before returning the language server command. In a WSL remote environment, this should be a Linux executable, for examplex86_64-unknown-linux-gnu.Alternatively, the extension could support a configurable absolute executable path for users who install
streamdthemselves.Suggested fix:
Implement binary installation/download logic in
zed-extension/src/lib.rs, then return the resulting executable path. The release workflow should publish platform-specific artifacts that the extension can retrieve.The current Windows-specific
wsl streamd lspbranch should also be reconsidered: remote Zed already runs the extension in WSL, while local Windows Zed should not require a separate WSL installation unless that is explicitly intended.Implementation Plan
Context
The Zed extension's
language_server_commandcurrently just returns a bare"streamd"command (or, on Windows, shells out towsl streamd lsp) and assumes astreamdbinary is already reachable. Under Zed Remote Development (e.g. connecting to a WSL2 or SSH remote), Zed resolves that command relative to the extension's private work directory (~/.local/share/zed/remote_extensions/work/streamd/streamd), which the extension never populates — nothing downloads, copies, or builds a binary there, so the LSP fails withNo such file or directory.The fix: make the extension self-sufficient. It should auto-download the correct platform-specific prebuilt
streamdbinary into its own work directory and run that, while still preferring an already-installedstreamdfound onPATH. The WSL-specific Windows branch is removed entirely — under Remote Development the extension already executes inside the remote host directly, and local Windows Zed should run the native.exewithout requiring WSL.Confirmed scope decisions:
Os::Macand fail with a clear, friendly error instead of attempting a download that 404s.Worktree::which("streamd")is used to prefer an existing on-PATH install; this is checked before the platform-support check, so users who already havestreamdon PATH (including on macOS/ARM) never hit the "unsupported platform" error.Current state (verified)
zed-extension/src/lib.rs(30 lines): returns bare"streamd"on non-Windows,wsl streamd lspon Windows. No download/cache/install logic, no use ofWorktree::which.zed-extension/extension.toml:version = "0.0.1", hardcoded, never bumped.zed-extension/Cargo.toml: only dependency iszed_extension_api = "0.7"(pinned to0.7.0). Relevant APIs it exposes (confirmed from the vendored crate source):current_platform() -> (Os, Architecture)—Os = Mac|Linux|Windows,Architecture = Aarch64|X86|X8664.download_file(url, file_path, file_type) -> Result<(), String>— file is saved relative to the extension's private working directory;file_typeisGzip|GzipTar|Zip|Uncompressed.make_file_executable(filepath) -> Result<(), String>.set_language_server_installation_status(id, status)where status isNone|Downloading|CheckingForUpdate|Failed(String).Worktree::which(binary_name) -> Option<String>— absolute path if found on PATH.latest_github_release, and no built-in checksum verification.Release artifacts (from
.forgejo/workflows/release.yml, built entirely via Nix from a singlenixrunner — no matrix, no macOS/ARM builder): raw (unarchived, unchecksummed) files uploaded tohttps://git.konstantinfickel.de/kfickel/streamd/releases/download/v<VERSION>/<artifact>:streamd-<VERSION>-linux-x86_64streamd-<VERSION>-windows-x86_64.exestreamd-zed-extension-<VERSION>.zip(built bynix build .#zed-extension-zip, already rebuilt every release job)flake.nix'smkZedExtension(lines 92–125) builds the wasm fromsrc = ./zed-extensiononly (rootCargo.tomlis not in scope for this derivation), withversion = "0.0.1"hardcoded at line 105. The top-levelversionbinding (flake.nix:34, read from rootCargo.toml) is already in lexical scope insidemkZedExtension'sletblock, same as it is formkStreamdMusl/mkStreamdWindows(which already doinherit version;and set derivation-level env vars likeCARGO_BUILD_TARGET).mkZedExtensionZip(lines 232–245) copiesextension.toml/extension.wasmstraight frommkZedExtension's output — so fixing the version at that layer propagates automatically, no separate change needed there.Implementation
1.
zed-extension/src/lib.rsAdd module-level constants:
Add a pure helper (no host imports — trivially unit-testable later if ever wanted):
Rewrite
language_server_command(both params become used, drop the_prefixes) with this control flow:if let Some(path) = worktree.which("streamd") { return Ok(Command { command: path, args: vec!["lsp".into()], env: vec![] }); }let (os, arch) = current_platform();let asset_name = release_asset_name(os, arch)— onErr(msg), callset_language_server_installation_status(language_server_id, &LanguageServerInstallationStatus::Failed(msg.clone()))and returnErr(msg).!Path::new(&asset_name).exists()(cache miss — the version is embedded in the filename, so an upgrade naturally invalidates the cache via a new filename, no explicit cache-busting needed):set_language_server_installation_status(id, &Downloading)download_file(&format!("{RELEASE_BASE_URL}/v{STREAMD_VERSION}/{asset_name}"), &asset_name, DownloadedFileType::Uncompressed)(raw executables, not archives) — on error, setFailedand returnErr.os != Os::Windows, callmake_file_executable(&asset_name)(skip on Windows — no permission bit there) — on error, setFailedand returnErr.std::env::current_dir()?.join(&asset_name)and return it asCommand { command: <absolute_path>, args: vec!["lsp".into()], env: vec![] }. Returning an absolute path (rather than relying on Zed's relative resolution) is the direct fix for the reported bug.if os == Os::Windows { wsl ... } else { streamd ... }branch entirely.Extend the
use zed_extension_api::{...}import to includeArchitecture,DownloadedFileType,LanguageServerInstallationStatus,download_file,make_file_executable(all re-exported at the crate root, no new deps needed inCargo.toml).2.
flake.nix—mkZedExtension(lines 92–125)version = "0.0.1";→inherit version;(use the same root-Cargo.toml-derived binding every other package uses).STREAMD_VERSION = version;as a derivation attribute (same mechanism already used forCARGO_BUILD_TARGETelsewhere in this file) —cargo component buildpicks it up viaoption_env!at compile time.postPatchphase to stamp the real version intoextension.tomlbefore packaging:--replace-fail(not--replace) so the build breaks loudly if the placeholder string ever drifts, instead of silently shipping a stale version.No changes needed to
mkZedExtensionZip— it already copiesextension.toml/extension.wasmfrommkZedExtension's (now-patched) output and already names the zip from the sameversionbinding.3.
zed-extension/extension.tomlLeave
version = "0.0.1"in the source file as-is — it's now an explicit build-time placeholder, overwritten by the NixpostPatchabove for every real release build. Do not hand-maintain it.4.
.forgejo/workflows/release.ymlNo changes needed. It already reruns
nix build .#zed-extension-zipevery release, and its ownVERSION(parsed fromCargo.tomlfor tagging) is read from the same source of truth as the Nixversionbinding — they can't drift.5.
README.md(lines 156–189, "Zed Extension (WSL2)")#### Zed Extension), since it now covers local Windows, local Linux, and Remote Development generally.streamdif present — so step 1 ("install streamd in WSL2 via.deb") becomes optional, kept only as an aside for users who prefer a separately managed install.streamd-zed-extension-<version>.zip, extract,zed: install dev extension) — this project isn't on Zed's marketplace, so manual dev-extension install is still required regardless of this fix..streamd.toml+@completions), and consider adding a note that installing the dev extension locally is sufficient for Remote Development — Zed runs the wasm inside the remote host automatically.Verification plan (no existing test harness for the extension — manual only)
cd zed-extension && cargo component build --release(optionallySTREAMD_VERSION=0.2.6 cargo component build --releaseto exercise the real download path against a real release).nix build .#zed-extension -o resultfrom repo root; confirm./result/extension.toml'sversionmatches rootCargo.toml(not0.0.1).streamdis not on PATH, install as a dev extension, open a Markdown file in a.streamd.tomldirectory, confirm the LSP starts and checkzed: open language server logsfor an absolute resolved path; confirm the binary lands on disk with exec permissions (Linux).which()fast path: put a realstreamdon PATH, restart the LSP, confirm it now uses the on-PATH binary instead of the downloaded copy.streamdpreinstalled, install the dev extension, confirm the LSP starts and the binary lands at~/.local/share/zed/remote_extensions/work/streamd/streamd-<version>-linux-x86_64with exec permissions.streamd-<version>-windows-x86_64.exedirectly, with nowslinvocation anywhere.Critical files
zed-extension/src/lib.rsflake.nix(mkZedExtension, lines 92–125)zed-extension/extension.tomlREADME.md(lines 156–189)zed-extension/Cargo.toml(no dependency changes expected, verify while implementing)Implementation complete — PR #128
Time: ~6 minutes from branch creation to PR open (implementation phase only; refinement/planning happened separately beforehand). Precise end-to-end token usage isn't something my tooling reports to me directly — the two research sub-agents used during refinement consumed ~63k tokens combined; I don't have a comparable figure for the implementation phase itself.
Summary of findings during implementation:
release_asset_name(os, arch)helper (Linux/Windows/macOS/unsupported-arch cases) before implementing it, then implementedrelease_asset_nameand rewrotelanguage_server_commandto make them pass.cargo testcompiles and links fine natively for this crate despitecrate-type = ["cdylib"]and its use ofwit-bindgen-generated host imports (current_platform,download_file, etc.) — no#[cfg(target_arch = "wasm32")]gating was needed, since unusedexternhost-import declarations don't produce link errors when never called from the test path.stringson the actual compiledextension.wasmthatSTREAMD_VERSION(baked in viaoption_env!+ the new NixSTREAMD_VERSIONenv var) and both platform-specific asset-name format strings are present in the binary, confirming the version-injection plumbing throughflake.nixactually works end-to-end, not just in theory.nix build .#zed-extensionconfirmedextension.toml'sversionis correctly patched from the placeholder0.0.1to the real0.2.6(matching rootCargo.toml).nix build .#zed-extension-ziprequired no changes and picked up the patchedextension.tomlautomatically, as anticipated in the plan.nix flake check(clippy, fmt, test, pre-commit) passes cleanly with no changes needed to.forgejo/workflows/release.yml.