This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(tui): route ssh:// peer attach through label + --session (#53)

The interactive picker's attach-remote path built `host.url + "/" +
session.name` and shelled to `pty-relay connect <that>`. For token URLs
(http/https) that works — pty-relay's parseToken picks the session name
out of the URL path. For ssh:// peers it collapses:

pty-relay connect ssh://user@host/session-name

pty-relay's `connect(tokenUrlOrLabel)` treats non-token URLs as
known-hosts labels, does `hosts.find(h => h.label === that-string)`,
misses, and exits 1 with "No known host". Even if resolveHost matched
by URL, the ssh branch takes the session name from `--session` /
`--spawn`, not from the URL path — the path segment is invisible to it.

Route around the seam by teaching the TUI to invoke pty-relay's
canonical ssh-peer shape:

pty-relay connect <host.label> --session <session.name>

Token URLs still get the existing path-append form (parseToken owns
that convention). Extracted `buildAttachRemoteArgs` so both branches are
unit-testable next to `buildSpawnRemoteArgs`.

A parallel fix in pty-relay (parse the session name from an ssh:// URL
path so `pty-relay connect ssh://user@host/name` also works) is in
flight as a mirror — the two together make the seam robust regardless
of which side the caller is on.

authored by

Nathan and committed by
GitHub
(Jul 3, 2026, 3:25 PM +0200) 072791cc 448824a6

+46 -6
+14 -5
src/tui/interactive.ts
··· 513 513 doAttach(session.name); 514 514 } 515 515 516 + /** Build the argv for attaching to a remote session. ssh:// peers have no 517 + * path-in-URL convention — pty-relay's connect looks them up by label + 518 + * a `--session <name>` flag. Token URLs still take the session name as 519 + * a path segment (parseToken handles that). Exported for unit testing. */ 520 + export function buildAttachRemoteArgs(host: RelayHost, session: RemoteSession): string[] { 521 + if (host.url.startsWith("ssh://")) { 522 + return ["connect", host.label, "--session", session.name]; 523 + } 524 + const url = host.url.replace(/#.*$/, "") + "/" + session.name + 525 + (host.url.includes("#") ? "#" + host.url.split("#").slice(1).join("#") : ""); 526 + return ["connect", url]; 527 + } 528 + 516 529 function doAttachRemote(host: RelayHost, session: RemoteSession): void { 517 530 if (!relayBin) return; 518 531 pauseApp(); 519 532 520 - // Build the connect URL: base URL + /session-name 521 - const url = host.url.replace(/#.*$/, "") + "/" + session.name + 522 - (host.url.includes("#") ? "#" + host.url.split("#").slice(1).join("#") : ""); 523 - 524 - const result = spawnSync(relayBin, ["connect", url], { 533 + const result = spawnSync(relayBin, buildAttachRemoteArgs(host, session), { 525 534 stdio: "inherit", 526 535 }); 527 536
+32 -1
tests/filter.test.ts
··· 1 1 import { describe, it, expect } from "vitest"; 2 - import { buildFilteredGroups, buildSpawnRemoteArgs, type ListItem, type RelayHost } from "../src/tui/interactive.ts"; 2 + import { buildAttachRemoteArgs, buildFilteredGroups, buildSpawnRemoteArgs, type ListItem, type RelayHost, type RemoteSession } from "../src/tui/interactive.ts"; 3 3 import type { SessionInfo } from "../src/sessions.ts"; 4 4 5 5 function makeSession(name: string, status: "running" | "exited" = "running", opts?: { command?: string; cwd?: string; tags?: Record<string, string> }): SessionInfo { ··· 255 255 ]); 256 256 }); 257 257 }); 258 + 259 + describe("buildAttachRemoteArgs", () => { 260 + const sess = (name: string): RemoteSession => ({ name, status: "running" }); 261 + const host = (label: string, url: string): RelayHost => ({ 262 + label, url, sessions: [], spawn_enabled: true, error: null, 263 + }); 264 + 265 + it("appends session name to token URL path (no fragment)", () => { 266 + expect(buildAttachRemoteArgs(host("h", "https://x.example"), sess("s1"))).toEqual([ 267 + "connect", "https://x.example/s1", 268 + ]); 269 + }); 270 + 271 + it("appends session name before the URL fragment (with token hash)", () => { 272 + expect(buildAttachRemoteArgs(host("h", "https://x.example#tok"), sess("s2"))).toEqual([ 273 + "connect", "https://x.example/s2#tok", 274 + ]); 275 + }); 276 + 277 + it("uses label + --session for ssh:// peers, ignoring URL-path convention", () => { 278 + expect(buildAttachRemoteArgs(host("home", "ssh://me@home"), sess("chat"))).toEqual([ 279 + "connect", "home", "--session", "chat", 280 + ]); 281 + }); 282 + 283 + it("ssh peers with port keep label routing", () => { 284 + expect(buildAttachRemoteArgs(host("box", "ssh://user@box:2222"), sess("s"))).toEqual([ 285 + "connect", "box", "--session", "s", 286 + ]); 287 + }); 288 + });