Repository navigation
feat: open the active workspace in VS Code, local or over SSH (#63) - #65
Merged
Merged
Conversation
A project row and the file viewer now hand the workspace to a desktop
editor. A remote SSH project opens through VS Code Remote-SSH already at
the right path, instead of reconnecting and hunting for the folder by hand.
Two ways exist to do this from a web app, and the studio needs both,
because it is not always running on the machine the browser is on:
* the `code` CLI, spawned by the server. Detectable — it either resolves
on $PATH or it does not — but it opens a window on the SERVER host,
which is wrong inside Docker, over a tunnel, or from a phone.
* a `vscode://` deep link, followed by the browser. Lands on the machine
with the screen, by definition. Nothing about it is detectable: no
browser reports whether a protocol handler exists or ran.
`POST /api/editor/open` prefers the first and falls back to the second, so
a workstation opens its own window and a container hands the browser a
link — one rule, no `process.platform` test, correct in every deployment.
The SPA states the condition every time it takes the deep-link path rather
than claiming an error it cannot observe.
`editor-links.js` is the pure half. The two URI shapes are NOT the same
string and conflating them silently breaks one path:
browser : vscode://vscode-remote/ssh-remote+user@host/srv/app
CLI : --folder-uri vscode-remote://ssh-remote+user@host/srv/app
Notes on the guards:
- $PATH is walked in-process, never through `which`/`where`. A subprocess
would need a shell on Windows, and the launched value is a user-chosen
filesystem path — the BatBadBut re-parse delegate-terminal.js already had
to work around. `_EXEC_EXTS` excludes `.CMD` deliberately: Node refuses to
spawn one without `shell: true`, so accepting it would turn every Windows
launch into a throw instead of the deep-link fallback that works there.
- Authorisation reuses `resolveFilesWorkdir()` — "you may open in an editor
whatever you may browse" — which also supplies the default-WORKDIR
fallback and `isRemote` from the PROJECT RECORD. Inferring remoteness from
the path would hand a local POSIX-looking workdir to Remote-SSH.
- A `~/project` remote workdir is refused by name: a URI has no shell, so
Remote-SSH would look for a directory literally called `~`.
- Per-file open is local-only. The remote FOLDER link works; VS Code's URL
handler opens a remote FILE uri as a folder
(microsoft/vscode-remote-release#4333).
- The editor is a fixed catalog (VS Code, Insiders, VSCodium, Cursor,
Windsurf), not a binary-name field: the value becomes both a URI scheme
and an argv[0]. Settings gets its row from `config-resolve.js`.
Tests: `test/editor-links.test.js` (57) pins the URI shapes literally plus
the wiring; `test/editor-open-api.test.js` (22) boots a real server with
$PATH pointed at an EMPTY directory, which makes the `opened:'client'`
fallback deterministic and guarantees the suite never launches an editor
window on a developer's desktop.
Closes #63
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes #63.
What you get
A project row and the file viewer hand the workspace to your desktop editor. A remote SSH project opens through VS Code Remote-SSH already at the right path — no reconnecting, no hunting for the folder.
Selectable in Settings → UI: VS Code, Insiders, VSCodium, Cursor, Windsurf.
Why there are two launch paths
The studio is not always running on the machine the browser is on, and each way of opening an editor is broken in the case the other handles:
code <path>spawned by the server$PATHeither resolves it or notvscode://…deep link followed by the browserPOST /api/editor/openprefers the CLI and falls back to the deep link. A resolvablecodebinary means the server is a workstation, so it opens the window itself and can report failure honestly; no binary means Docker / headless / Windows, and there the browser followsvscode://. One rule, noprocess.platformtest, correct in every deployment shape.Because the deep-link outcome is genuinely unobservable, the SPA states the condition every time it takes that path ("if nothing opened, …") instead of claiming an error it cannot see.
The two URI shapes are not the same string
In the first, the editor's scheme is the scheme and
vscode-remoteis the URI authority — that is how the desktop URL handler routes it. In the second,vscode-remoteis the scheme. Collapse them and one path silently stops working. Both are pinned literally in the tests.Guards
$PATHis walked in-process, never throughwhich/where. A subprocess would need a shell on Windows, and the launched value is a user-chosen filesystem path — the BatBadBut re-parsedelegate-terminal.jsalready had to work around._EXEC_EXTSexcludes.CMDon purpose: Node refuses to spawn one withoutshell: true, so accepting it would turn every Windows launch into a throw instead of the deep-link fallback that works there.WORKDIRfallback andisRemotefrom the project record — inferring remoteness from the path would hand a local POSIX-looking workdir to Remote-SSH.~/projectremote workdir is refused by name. A URI has no shell, so Remote-SSH would look for a directory literally called~. Resolving it would need an SSH round trip.argv[0].Verification
npm test— 65 files, exit 0, 0 failures.test/editor-links.test.js— 57 assertions: both URI shapes literally, port handling, percent-encoding (a#that would truncate the path, a:that must survive for a Windows drive letter),~/backslash/NUL/newline refusals, traversal containment on both sides, the fork catalog, plus source-level pins on the wiring (resolveFilesWorkdiris the authoriser, noshell:,.CMDstays out,loadMergedConfigresolveseditorwith||).test/editor-open-api.test.js— 22 assertions against a real booted server: local + remote links end to end, the host and non-standard port survivingdata/projects.json→ URI, a 403 for an unregistered workdir, 400s for traversal / NUL / non-string, the default-workspace fallback, and the configured editor reaching both the URI scheme and/api/version.$PATHpoints at an empty directory, which makesopened:'client'deterministic and guarantees the suite never launches an editor window on a developer's desktop.Not shipped, deliberately
"Reveal current file" and "Open terminal in VS Code" from the issue's Additional Enhancements — neither has a URI or CLI primitive that does the right thing across the local/remote split. Remote per-file open is blocked upstream (above).
🤖 Generated with Claude Code