Skip to content

feat(remote): attach a terminal to a live remote session - #227

Merged
devsuitup merged 1 commit into
mainfrom
feat/remote-terminal-attach
Sep 8, 2026
Merged

devsuitup merged 1 commit into
mainfrom
feat/remote-terminal-attach

Conversation

@devsuitup

Copy link
Copy Markdown
Owner

Closes #221.

Clicking a remote session opened a read-only transcript, because nothing could
drive a terminal on another machine. This adds that path, and it installs
nothing on the host.

The multiplexer is named by the CLI, in the descriptor
~/.claude/sessions/<pid>.json that the inventory connection already fetches
(#211). Switchboard does not probe a host to guess what runs there: it reads a
field, and an adapter is keyed on it. A host running no multiplexer has no such
field and is refused the tier explicitly. remote-attach.js is the only module
that knows the word "tmux"; elsewhere it appears solely in doc pointers.

Sizing, which is the whole difficulty

Attaching a client resizes the remote window, and the wrong size persists after
the client leaves
— that is what makes it harmful rather than cosmetic.

Measured on a fresh, never-pinned window (tmux 3.6, window-size latest),
attaching from Windows through node-pty:

client window during after detach
200x51 (height + status) 200x50 200x50
200x50 (naive) 200x49 200x49

So the window size and the status-line count are probed first, and the PTY is
opened at cols = window_width, rows = window_height + status_lines. Nothing
is written on the host: no window-size manual, no resize-window, no setting
touched on a session we do not own.

Two rejected approaches, both measured: attach -f ignore-size is genuinely
applied and the window resizes anyway; resize-window silently sets
window-size to manual at the window level, which would leave a session
pinned if we died mid-attach.

Wiring

The attached session lands in activeSessions with host set to the alias and a
handle that writes into the ssh PTY, so the existing injection path drives it
without knowing it is remote — that is what the seam merged in #225 was for.
trigger-context.js is untouched: the merged code already does exactly what was
needed.

Detach goes through the existing stop path: \x02d, a short wait, then the local
ssh client is killed. The remote session is not affected.

Verification

Everything is injected — no network, no real ssh. Three mutations, each with the
changed line printed before the run and the file restored from an md5-verified
copy:

  1. rows: height + statusLinesrows: height → 3 of 11 adapter tests red.
  2. The descriptor guard disabled → the "refuses a descriptor with no tmux field"
    test red.
  3. session.host == null forced true in the merged trigger-context.js → the
    pre-existing refactor(sessions): give a session entry a host and a transport-supplied handle #225 test red, confirming the wiring without adding a redundant
    one.

One existing assertion changed: the sandbox-badge test counts the success returns
of open-terminal, now three instead of two. The property it guards — every
success return reports the sandbox state — is unchanged and still asserted on
each.

task check: 1139 tests, 1131 passed, 0 failed, 8 pre-existing skips. Lint 0
errors.

Not in this change

Launching a remote session (#222), injection over the messaging socket (#219),
capability tiers and their UI (#218), liveness in the sidebar (#212).

The CLI's own descriptor already carries a tmux target (issue #211); this
adds the adapter that turns it into a real PTY instead of refusing every
remote resume outright. Sized at window height + status lines (measured:
the bare height leaves the tmux window one row short after the client
detaches) and detached with Ctrl-B d before the local ssh client ends, so
the remote session is never left resized or killed. Populates the
session-handle seam from #220: host/kind/handle are now set for real,
picked up unmodified by the existing getPtyForSession branch.

Refs #221
@devsuitup
devsuitup enabled auto-merge (squash) September 8, 2026 13:25
@devsuitup
devsuitup merged commit 8a4e32f into main Sep 8, 2026
9 checks passed
@devsuitup
devsuitup deleted the feat/remote-terminal-attach branch September 8, 2026 13:31
devsuitup added a commit that referenced this pull request Sep 8, 2026
…228)

remote-attach.js has carried a working tmux-attach adapter since #227,
but the sidebar sent every remote-session click to the read-only
transcript unconditionally, so the path was unreachable from the UI.

Join the sidebar's remote sessions to the indexer's live descriptors in
main.js's get-projects handler (new annotateRemoteAttachable(), next to
where remoteIndexer and remoteAttachAdapter already live) and expose the
result to the renderer as a plain boolean, session.remoteAttachable --
named for what the renderer is allowed to know, not for how attach is
implemented. The renderer only branches on that boolean; it never
inspects or names the underlying mechanism. A non-attachable remote
session keeps falling back to the transcript and now says why via a
hover title, matching the disabled-control idiom already used for a
missing project path.

Refs #221
@devsuitup devsuitup mentioned this pull request Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

design(remote): attach to a remote session through the descriptor

1 participant