feat(ssh): tunnel cards, in-place tunnel creation, startup commands - #377
Merged
Conversation
The SSH panel's Tunnels tab was read-only: rules could only be created from inside a host's drawer, which is backwards when what you are doing is "I need a tunnel" rather than "I am editing this host". It now has a New Tunnel drawer that picks the host the tunnel rides — the bastion, when the target is behind one — and writes the rule back as one of that host's forwarding rules. Rules can also be edited and deleted from the card. Every write sends the whole host record, because UpdateSSHHost lets the caller's value win and a partial payload would clear the rest. The tab itself moves from a row list to the same card grid the Hosts and Keys tabs use, with the state pill and buttons stacked in the card's right column so the route text keeps a full line to itself. Hosts gain a startup command: the lines typed at whatever prompts a bastion shows before it hands over a shell. JumpServer is the case it was built for — it asks for the asset at `Opt>`, then for the system user at `ID>`, neither of which is a shell command you could pass on a command line. Each line is written and followed by a *separate* carriage return, because "text\r" in one write reads as a paste to a program in raw mode. SSHConnectReq carries the field as `json:"-"`: only NewSshSessionByID fills it from a saved host, so an ad-hoc connect cannot use it to type arbitrary input into a remote shell. Two smaller fixes in the same panel. The tag field now commits what is in it when it loses focus — leaving by any route other than Enter or comma silently threw the tag away. And the key drawer can reveal the stored private key behind an eye button (fetched on demand, never on open), with a copy button beside it; the panel only mounts on desktop, so the PEM goes from the OS keychain to a webview in the same process and never touches the relay.
internal/relay/web-dist/ is built from web/src AND desktop/frontend/src, so the SSH panel changes in the previous commit left it stale and CI's "verify embed has no drift" gate failed. Rebuilt with the pinned toolchain (Node 20.20.2, Go 1.23.12) that the gate requires — only the content hashes and the files that reference them move.
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.
Summary
Opt>and then the system user atID>. Each line is followed by a separate carriage return —"text\r"in one write reads as a paste to a program in raw mode (the bug this repo has re-fixed three times on the quick-template path, fix quick input enter dispatch #63 / chore(desktop): remove the legacy quickInput plugin (superseded by QuickTemplate) #110 / fix(templates): split text and Enter so Codex reads them as keystrokes, not paste #129).RevealSSHKeybinding, never on open), with a copy button beside it.Notes on two deliberate choices
SSHConnectReq.StartupCommandis taggedjson:"-". It types arbitrary input into a remote shell, so onlyNewSshSessionByIDfills it from a saved host — a marshalled field would make "type this into that machine" part of the bound-method surfaceNewSshSessionexposes to the frontend.Every tunnel write sends the whole host record.
UpdateSSHHostlets the caller's value win for UI-owned fields, so a payload built from the rule alone would clear the rest of the host — the same shape as the item-25 bug that strippedproxy_jump.Test plan
go test ./desktop/...passes (4 packages)go vet ./desktop/...cleannpx vitest runpasses (177 files / 2347 tests;SshHostsPanel.test.ts93 → 108)npx vue-tsc --noEmitcleanvite buildsucceeds