feat(remote): attach a terminal to a live remote session - #227
Merged
Merged
Conversation
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
enabled auto-merge (squash)
September 8, 2026 13:25
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
Merged
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 #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>.jsonthat 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.jsis the only modulethat 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:
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. Nothingis written on the host: no
window-size manual, noresize-window, no settingtouched on a session we do not own.
Two rejected approaches, both measured:
attach -f ignore-sizeis genuinelyapplied and the window resizes anyway;
resize-windowsilently setswindow-sizetomanualat the window level, which would leave a sessionpinned if we died mid-attach.
Wiring
The attached session lands in
activeSessionswithhostset to the alias and ahandle 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.jsis untouched: the merged code already does exactly what wasneeded.
Detach goes through the existing stop path:
\x02d, a short wait, then the localssh 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:
rows: height + statusLines→rows: height→ 3 of 11 adapter tests red.test red.
session.host == nullforced true in the mergedtrigger-context.js→ thepre-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 — everysuccess 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 0errors.
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).