mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
Merge #510 feat(adopt): adopt tmux sessions a human started, in all three locations
The attach line now takes the rule #770 gave remote and docker launches: bashCArg() single-quotes it at both respawn-pane sites, so it reaches bash verbatim. quoteAdoptLaunch is retired, since its double quotes kept the same $ and backtick safety but lost a real newline. #510's no-cd condition stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
c65706a3e4
64 files changed
+5321
-103
No files matched your search
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @fileoverview Discovery contract for FOREIGN tmux sessions — the ones a human
|
||||
* started by hand (`tmux new -s work`, then `claude` or `codex` inside it), which
|
||||
* Codeman did not create and does not own.
|
||||
*
|
||||
* Codeman's own sessions live on a dedicated, instance-scoped socket
|
||||
* (`tmux -L codeman`) and are gated by `SAFE_MUX_NAME_PATTERN`. Everything here
|
||||
* is about the OTHER servers: the default socket, a colleague's `-L` socket, the
|
||||
* tmux inside a container, the tmux on an ssh host.
|
||||
*
|
||||
* Key exports:
|
||||
* - ForeignTmuxLocation, WHERE a foreign tmux server lives — the same three
|
||||
* locations Codeman already runs sessions in (local / docker / remote).
|
||||
* - ForeignPaneProbe, one parsed row of the probe script's output.
|
||||
* - ForeignTmuxSession, one adoptable candidate as the UI sees it.
|
||||
* - ForeignDiscoveryResult, one scan of one or more locations.
|
||||
*
|
||||
* ⚠️ `location` here describes where a candidate was FOUND. Once adopted, the
|
||||
* location fact is carried by `SessionAdopt` (types/session.ts) instead. The two
|
||||
* must never stand in for each other, or an un-adopted candidate starts looking
|
||||
* like a live session.
|
||||
*
|
||||
* No I/O here. The probe/parse/classify core is `src/foreign-tmux.ts`; the three
|
||||
* transports are `src/foreign-tmux-discovery.ts`.
|
||||
*/
|
||||
|
||||
import type { SessionMode } from './session.js';
|
||||
|
||||
/** Where a foreign tmux server lives. Mirrors Codeman's own three locations. */
|
||||
export type ForeignTmuxLocationKind = 'local' | 'docker' | 'remote';
|
||||
|
||||
/**
|
||||
* A location to scan. `hostId` refers to the saved docker-host / remote-host
|
||||
* registry entry, so the transport can be rebuilt without trusting the browser.
|
||||
*/
|
||||
export interface ForeignTmuxLocation {
|
||||
kind: ForeignTmuxLocationKind;
|
||||
/** Registry id (`docker-hosts.json` / `remote-hosts.json`). Absent for local. */
|
||||
hostId?: string;
|
||||
/** Display label for the UI. Absent for local. */
|
||||
label?: string;
|
||||
/** Container to `docker exec` into (`kind === 'docker'`). */
|
||||
containerName?: string;
|
||||
}
|
||||
|
||||
/** One pane row from the probe script, after parsing. */
|
||||
export interface ForeignPaneProbe {
|
||||
socketPath: string;
|
||||
sessionName: string;
|
||||
windowIndex: number;
|
||||
paneId: string;
|
||||
panePid: number;
|
||||
/** tmux's own idea of the running command — `node` for both claude and codex. */
|
||||
paneCurrentCommand: string;
|
||||
paneCurrentPath: string;
|
||||
sessionAttached: boolean;
|
||||
/** tmux `session_created`, epoch SECONDS. */
|
||||
sessionCreated: number;
|
||||
windows: number;
|
||||
}
|
||||
|
||||
/** One adoptable foreign session. */
|
||||
export interface ForeignTmuxSession {
|
||||
/**
|
||||
* Stable, opaque id for the UI and the adopt request. Derived from
|
||||
* location + socket + session name, so it survives a re-scan and can be
|
||||
* matched against an already-adopted session without echoing raw paths back
|
||||
* through the browser as command inputs.
|
||||
*/
|
||||
id: string;
|
||||
location: ForeignTmuxLocationKind;
|
||||
hostId?: string;
|
||||
hostLabel?: string;
|
||||
containerName?: string;
|
||||
/** Absolute socket path AS SEEN FROM THE LOCATION (not from Codeman's host). */
|
||||
socketPath: string;
|
||||
sessionName: string;
|
||||
windows: number;
|
||||
attached: boolean;
|
||||
/** Epoch MILLISECONDS (tmux reports seconds; normalized on parse). */
|
||||
createdAt: number;
|
||||
/**
|
||||
* What the session's FIRST LISTED pane is running, from the bounded process-tree
|
||||
* walk. Not the active window: claude in window 1 of a session whose window 0 holds
|
||||
* a shell reads as `shell`. The tab lands on that same first window, so the two agree.
|
||||
*/
|
||||
mode: SessionMode;
|
||||
/** Truncated command line behind the classification, for the UI subtitle. */
|
||||
command: string;
|
||||
/** That same pane's cwd. Informational: an adopted pane is never `cd`'d. */
|
||||
workingDir: string;
|
||||
/** Codeman session id already wrapping this target, when one exists. */
|
||||
adoptedBy?: string;
|
||||
}
|
||||
|
||||
/** One discovery scan. Never throws — unreachable locations become notes. */
|
||||
export interface ForeignDiscoveryResult {
|
||||
sessions: ForeignTmuxSession[];
|
||||
/** Epoch ms of the scan the result came from (may be a cache hit). */
|
||||
scannedAt: number;
|
||||
/**
|
||||
* Non-fatal reasons a location produced nothing (host unreachable, no tmux,
|
||||
* engine down). Surfaced so an empty list is never silently ambiguous.
|
||||
*/
|
||||
notes: string[];
|
||||
}
|
||||
@@ -75,4 +75,5 @@ export * from './search.js';
|
||||
export * from './user.js';
|
||||
export * from './webview.js';
|
||||
export * from './intent.js';
|
||||
export * from './foreign-tmux.js';
|
||||
export * from './mcp-sync.js';
|
||||
@@ -366,6 +366,74 @@ export interface SessionDocker {
|
||||
owned?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection facts needed to reach an adopted foreign tmux server. Deliberately
|
||||
* the exact `Pick`s that `buildSshConnectionArgs` / `buildDockerBaseArgs` consume,
|
||||
* so an adopted session can rebuild its command after a server restart without
|
||||
* re-reading a registry that may have been edited in the meantime.
|
||||
*/
|
||||
export interface AdoptRemoteConnection extends RemoteSshOptions {
|
||||
hostId: string;
|
||||
label: string;
|
||||
host: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface AdoptDockerConnection {
|
||||
hostId: string;
|
||||
label: string;
|
||||
engine: DockerEngine;
|
||||
containerName: string;
|
||||
daemonHost?: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ADOPTION overlay — a tmux session a HUMAN started, that Codeman attached to.
|
||||
*
|
||||
* This is the FOURTH location overlay, alongside remote-SSH and Docker, and it
|
||||
* obeys the same rule they do: it is **not** a `SessionMode`. The mode is still
|
||||
* claude / codex / shell / …, decided by the process-tree probe rather than by
|
||||
* the user.
|
||||
*
|
||||
* Shape of an adopted session: Codeman creates a NORMAL wrapper session on its
|
||||
* OWN socket (`codeman-<8hex>`, so `isValidMuxName` and every capture/input/
|
||||
* recovery path is untouched) whose single pane runs a command that attaches to
|
||||
* the foreign target. Killing the tab kills only the wrapper — the foreign
|
||||
* session is structurally out of reach, exactly like a non-owned remote session.
|
||||
*
|
||||
* ⚠️ There is no `owned` field here, unlike `SessionRemote`/`SessionDocker`: an
|
||||
* adopted session is *by definition* someone else's. A field that could be true
|
||||
* would invite a code path that kills it.
|
||||
*/
|
||||
export interface SessionAdopt {
|
||||
/** Where the foreign tmux server lives. */
|
||||
location: 'local' | 'docker' | 'remote';
|
||||
/** Absolute socket path as seen FROM that location (passed to `tmux -S`). */
|
||||
socketPath: string;
|
||||
/** The foreign session we attach to. Never Codeman-named, never name-validated. */
|
||||
targetSession: string;
|
||||
/**
|
||||
* Our own grouped VIEW session on the foreign server. Grouping (rather than a
|
||||
* bare `attach`) buys `status off` and an independent CURRENT WINDOW, both of
|
||||
* which land on our session instead of mutating the owner's.
|
||||
*
|
||||
* ⚠️ It does NOT buy an independent size, and the comment here used to claim it
|
||||
* did. A window is one object with one size and a group shares that object, so
|
||||
* our attach resizes the owner's view exactly as any second tmux client does
|
||||
* (measured on 3.3a: bare attach and grouped both shrink a 200x49 target to
|
||||
* 80x23; only `window-size largest` holds it, and that is a shared window
|
||||
* option which survives our detach — see foreign-tmux.ts for why it is not set).
|
||||
* Created with `destroy-unattached on` so it evaporates with our pane.
|
||||
*/
|
||||
viewSession: string;
|
||||
/** The pane cwd at adoption time. Informational: an adopted pane is never `cd`'d. */
|
||||
paneCurrentPath?: string;
|
||||
remote?: AdoptRemoteConnection;
|
||||
docker?: AdoptDockerConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid Claude CLI effort levels (claude >= 2.1.154).
|
||||
* `ultracode` = xhigh effort + standing dynamic-workflow orchestration; it is a
|
||||
@@ -884,6 +952,12 @@ export interface SessionState {
|
||||
remote?: SessionRemote;
|
||||
/** Docker execution metadata, present when this session runs inside a container via local tmux + docker exec */
|
||||
docker?: SessionDocker;
|
||||
/**
|
||||
* Adoption metadata, present when this session is a wrapper around a tmux
|
||||
* session a human started outside Codeman. Its presence is what disables
|
||||
* respawn/Ralph/orchestrator and hook-backed waits for the session.
|
||||
*/
|
||||
adopt?: SessionAdopt;
|
||||
/** Owning username in multi-user mode; undefined in single-user (ignored when the flag is off) */
|
||||
owner?: string;
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user