fix: remove the em-dashes this release added (release review)

105 new em-dashes sat in comments, test names and one test fixture from the
foreign tmux adoption work. Replaced by hand with commas, colons or
parentheses, one reading at a time. The fixture now quotes the real refusal
text, which says "It may still be running, try again."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 08:20:39 +01:00
1 parent 9d2f9663cb
commit 9442ea8cf4
27 files changed
+99 -99

No files matched your search

+5 -5
View File
@@ -9,7 +9,7 @@
*
* The home screen shows LOCAL results automatically, so a local scan runs on a
* TTL cache: N browser tabs polling every few seconds still trigger at most one
* scan per `FOREIGN_CACHE_TTL_MS`. Docker and remote are ON DEMAND only —
* scan per `FOREIGN_CACHE_TTL_MS`. Docker and remote are ON DEMAND only:
* opening the home page must never fan out one ssh connection per saved host.
*
* ## ⚠️ The probe must not cross an extra shell
@@ -18,7 +18,7 @@
* expand. Local and docker therefore spawn with an argv array, where no outer
* shell exists to eat them first. Remote is the exception: the ssh
* command line is a string like every other ssh call in this codebase, so the
* script is wrapped in SINGLE quotes there — which is exactly why
* script is wrapped in SINGLE quotes there, which is exactly why
* `buildForeignProbeScript()` may not contain one.
*
* This was measured, not reasoned: running the probe through `execSync` (which
@@ -316,7 +316,7 @@ export function foreignDockerProbeArgv(target: DockerTargetInfo): string[] {
* Scan the containers behind the caller's docker cases. On demand only.
*
* Read-only by construction: one `docker exec` running the probe. Never inspects
* lifecycle, never starts anything — a stopped or missing container simply
* lifecycle, never starts anything: a stopped or missing container simply
* contributes a note, mirroring `probeAdoptableContainer`'s "engine unreachable
* degrades to an empty list" rule.
*/
@@ -372,7 +372,7 @@ export function foreignRemoteProbeCommand(host: RemoteHost): string {
}
/**
* Scan saved ssh hosts. On demand only — this is the expensive one.
* Scan saved ssh hosts. On demand only: this is the expensive one.
*
* ⚠️ The ssh command is a STRING (as every ssh call in this codebase is), so the
* probe crosses one extra shell and must be single-quoted. `shellescape` does
@@ -416,7 +416,7 @@ export async function discoverRemoteForeign(hosts: RemoteHost[]): Promise<Foreig
export interface ForeignDiscoveryRequest {
/** Always scanned (cheap, cached). */
local?: boolean;
/** Docker cases the CALLER may see — ownership filtering happens in the route. */
/** Docker cases the CALLER may see; ownership filtering happens in the route. */
dockerCases?: DockerCase[];
/** Remote hosts the CALLER may see. */
remoteHosts?: RemoteHost[];
+27 -27
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Pure core for FOREIGN tmux sessions — the ones a human started
* @fileoverview Pure core for FOREIGN tmux sessions, the ones a human started
* by hand, which Codeman neither created nor owns.
*
* Everything here is a string in / structure out, so all three locations (local,
@@ -32,13 +32,13 @@
* ⚠️ The script MUST NOT contain a single quote. It is wrapped in single quotes
* to cross `ssh <host> '<script>'` and `docker exec <c> sh -lc '<script>'`. That
* is the single-quote dual of the "no double quotes in the docker launch chain"
* rule in `tmux-manager.ts` — same class of bug, opposite quote.
* rule in `tmux-manager.ts`: same class of bug, opposite quote.
*
* ## Why the mode comes from the process tree
*
* `#{pane_current_command}` is `node` for BOTH claude and codex, so it cannot
* distinguish them. The classifier walks the pane's descendants over the `ps`
* snapshot with `collectDescendants` (bounded: one snapshot, no per-node spawn —
* snapshot with `collectDescendants` (bounded: one snapshot, no per-node spawn,
* see proc-tree.ts for the incident that rule exists for) and matches argv. What
* it cannot identify is `shell`, which is also the honest answer for the case
* this feature was built for: a hand-opened shell.
@@ -53,7 +53,7 @@
*
* ⚠️ The group session must be created ATTACHED (no `-d`). tmux's
* `server_check_unattached()` runs every server loop, so a detached session with
* `destroy-unattached on` is torn down almost immediately — the option has to
* `destroy-unattached on` is torn down almost immediately: the option has to
* land on a session that already has our client on it.
*
* ⚠️ **Grouping does NOT give us an independent window SIZE, and believing it did
@@ -66,12 +66,12 @@
*
* A window is one object with one size; a group shares the object. So attaching
* from Codeman resizes a session someone else is actively viewing, exactly like
* any second tmux client does — that is tmux's normal behavior, and it reverses
* any second tmux client does: that is tmux's normal behavior, and it reverses
* when we detach.
*
* `window-size largest` is deliberately NOT set here. It is a WINDOW option on a
* SHARED window, so it survives our detach (measured: still `largest` after our
* view is gone) — it would permanently rewrite the owner's configuration to buy
* view is gone): it would permanently rewrite the owner's configuration to buy
* a cropped view for us. Restoring it would need the previous value captured with
* `$(...)`, and this string carries no command substitution. It reaches `bash -c`
* verbatim now (`bashCArg()`), but it crossed `bash -c "..."` for a long time, where
@@ -102,7 +102,7 @@ import type { SessionAdopt, SessionMode } from './types/session.js';
/**
* Field separator. tmux's `-F` does NOT expand a backslash-t escape: it emits the
* LITERAL two characters (verified on next-3.7 — the same finding that shaped
* LITERAL two characters (verified on next-3.7, the same finding that shaped
* `parseRemoteSessionList` in remote-hosts.ts). The parser accepts a real TAB too,
* in case some build does expand it.
*/
@@ -145,12 +145,12 @@ export function buildForeignProbeScript(): string {
// `ps -eo` covers procps and macOS; `ps ax -o` is the BSD-ish fallback. If both
// fail we still emit the pane rows, and classification degrades to the pane
// command — which still names a plain shell correctly.
// command, which still names a plain shell correctly.
// ⚠️ No `set -f` here: the socket sweep IS a glob, and disabling pathname
// expansion turns the loop into a single literal non-match. ⚠️ The marker line
// must NOT try to carry the field separator at all. Measured: `sh`'s builtin
// `echo` EXPANDS a backslash-t to a real TAB, while tmux's own `-F` emits the
// two literal characters — so the same escape means two different things two
// two literal characters, so the same escape means two different things two
// lines apart. The socket line is space-separated instead (marker first, rest
// of line is the path), which no shell rewrites.
//
@@ -234,7 +234,7 @@ export function parseForeignProbeOutput(stdout: string): ForeignProbeOutput {
if (inProcs) {
if (procCount >= FOREIGN_MAX_PROCS) continue;
// `<pid> <ppid> <argv...>` — leading spaces are how `ps -o pid=` pads.
// `<pid> <ppid> <argv...>`: leading spaces are how `ps -o pid=` pads.
// ⚠️ `s`, so `.*$` cannot fail: without it a bare CR in an argv stopped the `.`, and
// `\s+` handed its spaces back one at a time, quadratic in a row this long.
const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/s.exec(line);
@@ -305,7 +305,7 @@ export function parseForeignProbeOutput(stdout: string): ForeignProbeOutput {
*
* Matched against the WHOLE argv of a pane descendant, so `node .../bin/claude`
* hits the claude rule via the path. The binary names come from each entry's
* `discovery.binaries` — the one place that already knows a mode name is NOT
* `discovery.binaries`, the one place that already knows a mode name is NOT
* always a binary name (`antigravity` runs `agy`). The hand-written table this
* replaces keyed on CLI ids, which CLAUDE.md forbids outside `stock.ts`, and it
* had already fallen behind: `omp` was missing, so a hand-started omp pane read
@@ -339,7 +339,7 @@ export function foreignCliSignatures(): ReadonlyArray<{ mode: SessionMode; test:
// on purpose: `SessionMode` is the CLOSED union of the stock ids, so a custom
// entry has no SessionMode to be. Letting one through would put a value the
// union does not contain on `SessionState.mode`. A custom CLI's pane falls to
// the `shell` answer below, which is the honest one — Codeman has no name for
// the `shell` answer below, which is the honest one: Codeman has no name for
// it, and an adopted wrapper runs `attach` either way.
if (!entry.stock) continue;
for (const binary of entry.discovery.binaries) {
@@ -413,7 +413,7 @@ export function classifyForeignPaneMode(
if (direct) return { mode: direct, command: truncate(own) };
// ⚠️ Report truncation. A walk that stopped at a cap returns "nothing matched",
// which is byte-identical to the answer for a real shell — without this line a
// which is byte-identical to the answer for a real shell: without this line a
// deep or wide tree silently downgrades an agent pane to `shell` and there is
// nothing anywhere to tell the two apart.
const descendants = collectDescendants(pane.panePid, probe.byParent, {
@@ -503,7 +503,7 @@ function truncate(s: string): string {
}
// ===========================================================================
// Exclusion — never adopt what Codeman already owns
// Exclusion: never adopt what Codeman already owns
// ===========================================================================
/** tmux servers Codeman runs itself. Sessions on these are never "foreign". */
@@ -518,7 +518,7 @@ const CODEMAN_SESSION_PREFIXES = ['codeman-', 'claudeman-'];
* Excluding them is not cosmetic. Locally it stops Codeman from adopting its own
* sessions (which would attach a second PTY to a live pane). On a remote host or
* inside a container it stops the SAME session from being reachable through two
* different code paths — `remote-hosts.listRemoteCodemanSessions` already owns
* different code paths: `remote-hosts.listRemoteCodemanSessions` already owns
* the `tmux -L codeman` case there, and two paths to one session is how the two
* end up disagreeing about ownership.
*
@@ -539,7 +539,7 @@ export function isCodemanOwnedPane(
}
// ===========================================================================
// Adoptability — what may cross the launch chain at all
// Adoptability: what may cross the launch chain at all
// ===========================================================================
/**
@@ -568,8 +568,8 @@ export function isCodemanOwnedPane(
* dropped during discovery, so it has no id, and the adopt endpoint re-resolves
* through that same discovery and can only 404.
*
* Generous on purpose about what real names contain — Unicode letters, digits,
* marks, spaces, and the punctuation people actually use — because a refusal
* Generous on purpose about what real names contain (Unicode letters, digits,
* marks, spaces, and the punctuation people actually use), because a refusal
* here is a session the user cannot open at all.
*/
const ADOPTABLE_TEXT = /^[\p{L}\p{N}\p{M} _.+=@#%,~^!/-]+$/u;
@@ -732,13 +732,13 @@ export interface ForeignAttachTarget {
* target gone tmux takes it as part of the name and creates a group literally
* called `=work`. It has to be a separate `has-session`.
* - `new-session -t` creates a session in the target's GROUP: same windows, our
* own session options and current window (NOT our own size — see @fileoverview
* own session options and current window (NOT our own size, see @fileoverview
* for the measurement that disproved that).
* - `status off` is why grouping earns its keep: it lands on OUR session, so the
* owner's status bar is untouched.
* - No `-d`: the session must be attached before `destroy-unattached` lands on it.
* - The escaped semicolons reach tmux as plain command separators after one shell
* parse — the same convention the docker launch chain uses.
* parse, the same convention the docker launch chain uses.
* - ⚠️ There is NO degraded fallback, on purpose. A read-only `attach-session -r`
* used to hang off `||` "for a tmux too old to group", but `||` fires on ANY
* `new-session` failure. The one that actually happens is a leftover view with
@@ -796,7 +796,7 @@ export function buildForeignAttachCommand(target: ForeignAttachTarget): string {
}
export interface ForeignDockerAttachOptions extends ForeignAttachTarget {
/** `buildDockerBaseArgs(...)` joined — e.g. `docker` or `docker --context foo`. */
/** `buildDockerBaseArgs(...)` joined, e.g. `docker` or `docker --context foo`. */
dockerBase: string;
containerName: string;
}
@@ -805,7 +805,7 @@ export interface ForeignDockerAttachOptions extends ForeignAttachTarget {
* Docker adoption: `docker exec -it` into the container and attach there.
*
* Mirrors the ADOPTED-container branch of `buildDockerLaunchCommand`: look, then
* exec. Never `create`, never `start` — the container belongs to the user, and a
* exec. Never `create`, never `start`: the container belongs to the user, and a
* missing or stopped one fails closed with a message instead of being mutated.
*/
export function buildForeignDockerAttachCommand(opts: ForeignDockerAttachOptions): string {
@@ -833,7 +833,7 @@ export function buildForeignDockerAttachCommand(opts: ForeignDockerAttachOptions
* ⚠️ Needed for docker and NOT for the other two, which is a docker fact rather
* than a design choice. Killing the wrapper pane kills a LOCAL tmux client
* directly, and over ssh it sends SIGHUP to the remote client (the same
* propagation the non-owned remote attach has relied on since COD-105) — either
* propagation the non-owned remote attach has relied on since COD-105), either
* way the client detaches and `destroy-unattached on` collects the view.
* A `docker exec` process does NOT die when its client goes away: measured, the
* in-container tmux client stayed attached after the wrapper was killed, so the
@@ -841,7 +841,7 @@ export function buildForeignDockerAttachCommand(opts: ForeignDockerAttachOptions
* exec process.
*
* Killing a session by name affects ONLY that session, never the group's other
* members — so this can only ever remove the view Codeman created, never the
* members, so this can only ever remove the view Codeman created, never the
* human's session it is grouped with. Sibling of `buildDockerKillCommand`, and
* fired best-effort on the same path.
*/
@@ -873,9 +873,9 @@ export function foreignRemoteShellCommand(script: string): string {
}
export interface ForeignRemoteAttachOptions extends ForeignAttachTarget {
/** `buildSshConnectionArgs(host)` — `['ssh', '-o BatchMode=yes', ...]`. */
/** `buildSshConnectionArgs(host)`: `['ssh', '-o BatchMode=yes', ...]`. */
sshArgs: string[];
/** `remoteSshTarget(host)` — `user@host`. */
/** `remoteSshTarget(host)`: `user@host`. */
sshTarget: string;
}
@@ -883,7 +883,7 @@ export interface ForeignRemoteAttachOptions extends ForeignAttachTarget {
* Remote adoption: ssh in and attach there.
*
* `-t` goes right after `ssh -o BatchMode=yes`, exactly where
* `buildRemoteAttachCommand` puts it — interactive tmux needs a PTY. Connection
* `buildRemoteAttachCommand` puts it: interactive tmux needs a PTY. Connection
* options come from the shared `buildSshConnectionArgs`, never hand-built here,
* so adoption reaches a proxied/custom-port host on the same terms discovery did.
*/
+1 -1
View File
@@ -229,7 +229,7 @@ export function planRebootRestore(
// ⚠️ Same family as the rule above, different reason. Codeman never started the
// process this session wrapped: all it ever had was an attach command. Replaying
// that builds a connection to a foreign session that a reboot has almost certainly
// taken with it, and — worse — the rebuilt tab would present whatever the attach
// taken with it, and, worse, the rebuilt tab would present whatever the attach
// lands on as a session we restored, passing someone else's work off as ours.
skipped.push({ sessionId, reason: 'adopted' });
continue;
+1 -1
View File
@@ -524,7 +524,7 @@ export function isRemoteClaudeTranscriptPath(path: string): boolean {
/**
* Shell lines that set `cfg` to claude's config dir on the remote host: an absolute
* `CLAUDE_CONFIG_DIR` from the ssh command's environment, else `$HOME/.claude` — claude's own
* `CLAUDE_CONFIG_DIR` from the ssh command's environment, else `$HOME/.claude`, claude's own
* rule. ⚠️ The remote claude runs under `$SHELL -i -l -c` (`remoteLoginShellCommand`), this
* script under a plain non-interactive `sh` from sshd's `$SHELL -c`: a `CLAUDE_CONFIG_DIR` set
* only in a login or interactive profile (`.profile`, `.bash_profile`, `.zshrc`) is not seen
+2 -2
View File
@@ -1370,7 +1370,7 @@ export class Session extends EventEmitter {
*
* ⚠️ Deliberately NOT folded into `isExternalCliMode()`. That predicate answers
* "does this CLI render its own TUI", which is about the PROTOCOL. This one
* answers "did we launch the process", which is about AUTHORITY — an adopted
* answers "did we launch the process", which is about AUTHORITY: an adopted
* session can be `mode: 'claude'` and still have no hooks installed, no
* `envOverrides`, no effort and no `--session-id` we chose. Conflating the two
* would silently grant claude-shaped automation over a process we do not own.
@@ -1502,7 +1502,7 @@ export class Session extends EventEmitter {
*
* ⚠️ An ADOPTED session is the same shape for a different reason: its pane runs
* `tmux attach` to a session a human started, never the agent. That attach exiting
* with 0 says the connection ended or its owner closed the foreign session — neither
* with 0 says the connection ended or its owner closed the foreign session; neither
* means this session finished, and neither is Codeman's to clear the tab for. Without
* this, the clean-exit sweep (`pane-exit-sweep.ts`) would close someone else's live
* session's tab the moment they detached from it themselves.
+3 -3
View File
@@ -1931,7 +1931,7 @@ function buildRemoteSessionCommand(options: {
}
/**
* ADOPTION — the pane command for a wrapper session around a tmux session a
* ADOPTION: the pane command for a wrapper session around a tmux session a
* human started. Sibling of `buildRemoteSessionCommand` and
* `buildDockerLaunchCommand`, and the same shape of decision: the wrapper is an
* ordinary `codeman-<8hex>` session on OUR socket, and only what runs INSIDE its
@@ -3227,7 +3227,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// for a non-owned session. The only `kill-session` we run is on OUR LOCAL
// socket (`this.tmux()` = `tmux -L codeman` on THIS host), which kills the
// local pane — it does NOT reach the REMOTE socket.
// ADOPTION — DETACH-NOT-KILL, by construction rather than by discipline.
// ADOPTION: DETACH-NOT-KILL, by construction rather than by discipline.
//
// An adopted session's wrapper is an ordinary session on OUR socket whose
// pane runs `tmux attach` (or `docker exec …`/`ssh …` then attach) against a
@@ -3235,7 +3235,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// DETACH on the foreign side; the human's session keeps running, and our
// grouped view session self-destroys via `destroy-unattached on`.
//
// The guard below is a formality more than a fence — `this.tmux()` only ever
// The guard below is a formality more than a fence: `this.tmux()` only ever
// addresses our own socket, so no code path here CAN reach the foreign
// server. It is written out anyway so the intent survives future edits, the
// same way the non-owned-remote early return does.
+4 -4
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Discovery contract for FOREIGN tmux sessions — the ones a human
* @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.
*
@@ -9,7 +9,7 @@
* tmux inside a container, the tmux on an ssh host.
*
* Key exports:
* - ForeignTmuxLocation, WHERE a foreign tmux server lives — the same three
* - 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.
@@ -50,7 +50,7 @@ export interface ForeignPaneProbe {
windowIndex: number;
paneId: string;
panePid: number;
/** tmux's own idea of the running command — `node` for both claude and codex. */
/** tmux's own idea of the running command: `node` for both claude and codex. */
paneCurrentCommand: string;
paneCurrentPath: string;
sessionAttached: boolean;
@@ -93,7 +93,7 @@ export interface ForeignTmuxSession {
adoptedBy?: string;
}
/** One discovery scan. Never throws — unreachable locations become notes. */
/** 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). */
+3 -3
View File
@@ -390,7 +390,7 @@ export interface AdoptDockerConnection {
}
/**
* ADOPTION overlay — a tmux session a HUMAN started, that Codeman attached to.
* 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
@@ -400,7 +400,7 @@ export interface AdoptDockerConnection {
* 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
* 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
@@ -424,7 +424,7 @@ export interface SessionAdopt {
* 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).
* 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;
+1 -1
View File
@@ -10962,7 +10962,7 @@ class CodemanApp {
// only thing Codeman owns; the server's killSession refuses to reach the
// foreign server at all (see the adopted early return in tmux-manager). So
// the red "Terminate the session completely" option is not merely redundant
// here, it is a false promise about someone else's live work — and the kind
// here, it is a false promise about someone else's live work, and the kind
// of false promise that stops a user closing the tab at all. Hide it, and say
// what actually happens instead.
const killBtn = document.getElementById('closeConfirmKillBtn');
+13 -13
View File
@@ -17,13 +17,13 @@
*
* The list is a poll, not an SSE stream: the server caches a local scan on a TTL,
* so N tabs polling costs one scan per TTL no matter how many are open. But an
* open tab that has LEFT the home screen must stop — otherwise every background
* open tab that has LEFT the home screen must stop, otherwise every background
* tab keeps sweeping tmux sockets forever. `startForeignPolling` /
* `stopForeignPolling` are called from `showWelcome`/`hideWelcome`.
*
* ⚠️ Docker and remote locations are NOT in that poll. Each costs a `docker exec`
* or a full ssh handshake per target, each with a ~12s timeout, so riding them
* along on an 8s tick stacks connections on a slow host — the one cost the
* along on an 8s tick stacks connections on a slow host, the one cost the
* server-side design explicitly refuses. The wide scan is therefore ONE-SHOT:
* turning the toggle on fetches once, and after that it is the user's Rescan
* button. The periodic poll stays local-only forever.
@@ -35,7 +35,7 @@
*
* ⚠️ Adoption creates a REAL session, so the button locks while in flight and the
* result goes through the app's normal idempotent create path
* (`_onSessionCreated` then `selectSession`) — never by inserting a tab here.
* (`_onSessionCreated` then `selectSession`), never by inserting a tab here.
*
* @mixin Extends CodemanApp.prototype via Object.assign
* @dependency app.js (this.sessions, _onSessionCreated, selectSession)
@@ -80,12 +80,12 @@ Object.assign(CodemanApp.prototype, {
if (this._foreignPollTimer || this._foreignRefused) return;
void this.loadForeignSessions();
this._foreignPollTimer = setInterval(() => {
// Local only — see @fileoverview. The wide scan never rides the timer.
// Local only: see @fileoverview. The wide scan never rides the timer.
void this.loadForeignSessions();
}, this._foreignPollMs || FOREIGN_POLL_FALLBACK_MS);
},
/** Stop polling. MUST run on leaving the home screen — see @fileoverview. */
/** Stop polling. MUST run on leaving the home screen, see @fileoverview. */
stopForeignPolling() {
if (!this._foreignPollTimer) return;
clearInterval(this._foreignPollTimer);
@@ -125,7 +125,7 @@ Object.assign(CodemanApp.prototype, {
/**
* Fetch the list. `wide` asks the server for containers and ssh hosts too, and
* ONLY an explicit user action ever passes it — never the interval.
* ONLY an explicit user action ever passes it, never the interval.
*/
async loadForeignSessions({ wide = false } = {}) {
if (this._foreignRefused) return;
@@ -207,8 +207,8 @@ Object.assign(CodemanApp.prototype, {
}
// ⚠️ `_api`, not `_apiJson`: the latter folds a network failure, a non-2xx
// and `success:false` all into null, and the server deliberately tells
// two different stories here — "that tmux session is no longer there" vs
// "could not reach it just now (…) — try again". Collapsing them showed a
// two different stories here: "that tmux session is no longer there" vs
// "could not reach it just now (…), try again". Collapsing them showed a
// live remote session as deleted every time the link flickered.
const res = await this._api('/api/sessions/adopt', { method: 'POST', body });
const payload = await this._readJsonBody(res);
@@ -225,7 +225,7 @@ Object.assign(CodemanApp.prototype, {
this._onSessionCreated?.(data.session);
await this.selectSession(data.session.id);
// Still one explicit user action, so a wide row's "Open" refreshes the
// wide list too — otherwise the row just adopted keeps reading as free.
// wide list too, otherwise the row just adopted keeps reading as free.
await this.loadForeignSessions({ wide: this.foreignScanRemote() });
} finally {
this._foreignAdoptInFlight = false;
@@ -259,7 +259,7 @@ Object.assign(CodemanApp.prototype, {
},
/**
* Render into one container. The ONLY row builder — see @fileoverview.
* Render into one container. The ONLY row builder, see @fileoverview.
*
* Everything is built with DOM APIs and `textContent`: a session name and a
* command line come from another user's process, so they are never allowed
@@ -277,14 +277,14 @@ Object.assign(CodemanApp.prototype, {
// Hidden rather than shown-empty: on a first run there is usually nothing,
// and an empty block on the welcome screen reads as a broken feature.
// A note is a REASON the list is empty, so a block carrying one must stay
// visible — hiding it is exactly how "where did my session go" becomes
// visible: hiding it is exactly how "where did my session go" becomes
// unanswerable.
const hasNotes =
(Array.isArray(this._foreignNotes) && this._foreignNotes.length > 0) ||
(Array.isArray(this._foreignWideNotes) && this._foreignWideNotes.length > 0);
// ⚠️ `canScanWide` must keep the block visible even with nothing to show.
// The scan toggle lives in the header, so hiding an empty block also hides
// the only control that could fill it — on a host with containers but no
// the only control that could fill it: on a host with containers but no
// local tmux sessions that made the whole feature unreachable.
container.hidden = !this.foreignScanRemote() && !this._foreignError && !hasNotes && !this._foreignCanScanWide;
if (!container.hidden) {
@@ -435,7 +435,7 @@ Object.assign(CodemanApp.prototype, {
/**
* One delegated listener per container, wired once. Delegation matters here
* because the list is fully rebuilt on every poll — per-button listeners would
* because the list is fully rebuilt on every poll: per-button listeners would
* leak one set per tick.
*/
wireForeignSessions() {
+1 -1
View File
@@ -509,7 +509,7 @@ Object.assign(CodemanApp.prototype, {
);
// Sessions a human opened outside Codeman. Its own container, rebuilt by the
// ONE renderer in foreign-sessions.js — the phone must not grow a second row
// ONE renderer in foreign-sessions.js: the phone must not grow a second row
// builder that could describe the same session differently from the desktop.
const foreign = document.createElement('div');
foreign.className = 'foreign-sessions mobile-foreign-sessions';
+1 -1
View File
@@ -4219,7 +4219,7 @@ html[data-session-list="sidebar"] .session-sidebar .session-tab .tab-close {
/* Foreign sessions block inside the phone overview (foreign-sessions.js).
The desktop block sits inside the welcome column; here it is a full-width
section between CURRENT and PAST, so it only needs the surrounding spacing —
section between CURRENT and PAST, so it only needs the surrounding spacing:
every row style is shared with styles.css on purpose. */
.mobile-foreign-sessions {
margin: 0.75rem 0.75rem 0;
+1 -1
View File
@@ -22134,7 +22134,7 @@ html[data-tile-anim="soft"] .tile-grid-ghosts.tile-grid-ghosts--release:not(.til
}
/* ═══════════════════════════════════════════════════════════════
Foreign sessions — tmux sessions a human started outside Codeman
Foreign sessions: tmux sessions a human started outside Codeman
(foreign-sessions.js). Rendered on the welcome screen and, with the
same row builder, inside the phone overview.
+1 -1
View File
@@ -2961,7 +2961,7 @@ Object.assign(CodemanApp.prototype, {
// the column without overlapping the content leaves it hidden.
this.showHomeSessions?.();
// Sessions a human opened outside Codeman. Polls only while this screen is
// up (stopped in hideWelcome) — see foreign-sessions.js.
// up (stopped in hideWelcome), see foreign-sessions.js.
this.wireForeignSessions?.();
this.startForeignPolling?.();
}
+3 -3
View File
@@ -4,8 +4,8 @@
* discovery of FOREIGN tmux sessions (ones a human started outside Codeman).
*
* Discovery lives here rather than beside the adopt endpoint on purpose: like
* every other route in this file it exposes cross-user process state — other
* people's session names, commands and working directories — so it inherits the
* every other route in this file it exposes cross-user process state (other
* people's session names, commands and working directories), so it inherits the
* admin gate this file already applies. Adoption is a session CREATE and stays in
* `session-routes.ts`, where the owner, capacity and case-space gates live.
*/
@@ -66,7 +66,7 @@ export function registerMuxRoutes(app: FastifyInstance, ctx: InfraPort & Session
const wantRemote = q.remote === '1' || q.remote === 'true';
// Read the registries either way: `canScanWide` tells the browser whether the
// expensive scan has anywhere to go. Without it the UI hides an empty block —
// expensive scan has anywhere to go. Without it the UI hides an empty block,
// and with it the toggle that is the ONLY way to populate that block, which on
// a host with containers but no local tmux sessions made the feature invisible.
const dockerCases = await readAllDockerCases();
+1 -1
View File
@@ -72,7 +72,7 @@ export function registerRalphRoutes(
// ⚠️ Adoption gate, kept SEPARATE from the external-CLI gate above: an adopted
// session can be `mode: 'claude'` and still be a process we never launched.
// Everything below drives the pane on the assumption Codeman owns what runs
// in it — sending `/clear`, killing and relaunching the agent — which against
// in it (sending `/clear`, killing and relaunching the agent), which against
// someone else's live session is destructive, not merely unsupported.
if (session.isAdopted) {
return createErrorResponse(
+3 -3
View File
@@ -101,7 +101,7 @@ export function registerRespawnRoutes(
// ⚠️ Adoption gate, kept SEPARATE from the external-CLI gate above: an adopted
// session can be `mode: 'claude'` and still be a process we never launched.
// Everything below drives the pane on the assumption Codeman owns what runs
// in it — sending `/clear`, killing and relaunching the agent — which against
// in it (sending `/clear`, killing and relaunching the agent), which against
// someone else's live session is destructive, not merely unsupported.
if (session.isAdopted) {
return createErrorResponse(
@@ -255,7 +255,7 @@ export function registerRespawnRoutes(
// ⚠️ Adoption gate, kept SEPARATE from the external-CLI gate above: an adopted
// session can be `mode: 'claude'` and still be a process we never launched.
// Everything below drives the pane on the assumption Codeman owns what runs
// in it — sending `/clear`, killing and relaunching the agent — which against
// in it (sending `/clear`, killing and relaunching the agent), which against
// someone else's live session is destructive, not merely unsupported.
if (session.isAdopted) {
return createErrorResponse(
@@ -335,7 +335,7 @@ export function registerRespawnRoutes(
// ⚠️ Adoption gate, kept SEPARATE from the external-CLI gate above: an adopted
// session can be `mode: 'claude'` and still be a process we never launched.
// Everything below drives the pane on the assumption Codeman owns what runs
// in it — sending `/clear`, killing and relaunching the agent — which against
// in it (sending `/clear`, killing and relaunching the agent), which against
// someone else's live session is destructive, not merely unsupported.
if (session.isAdopted) {
return createErrorResponse(
+2 -2
View File
@@ -1657,8 +1657,8 @@ export function registerSessionRoutes(
// pane's cwd. That cwd is an observation about another machine: it can carry
// characters outside SAFE_PATH_PATTERN (`~/c++`, `Program Files (x86)`), which
// makes `createSession` throw so the tab never starts at all, and when it is a
// container or ssh path every local consumer of `workingDir` — the Files panel,
// the boot hook sweep, the fs watchers — reads it as a path on THIS host. The
// container or ssh path every local consumer of `workingDir` (the Files panel,
// the boot hook sweep, the fs watchers) reads it as a path on THIS host. The
// foreign cwd stays on `adopt.paneCurrentPath`, which is for display only.
// `/tmp` is the same neutral cwd `resolveMuxAttachCwd` already gives the pane.
const session = new Session({
+2 -2
View File
@@ -4164,7 +4164,7 @@ export class WebServer extends EventEmitter {
// and one more: it is the ONLY thing that marks this session as
// wrapping a process Codeman never launched. Dropping it on recovery
// silently re-enabled respawn, Ralph and hook-backed waits against
// someone else's live tmux session after every server restart —
// someone else's live tmux session after every server restart:
// measured, not hypothetical. The mux record is preferred because it
// is what `killSession`'s detach-not-kill guard already reads.
adopt: muxSession.adopt ?? savedState?.adopt,
@@ -4455,7 +4455,7 @@ export class WebServer extends EventEmitter {
// connection facts live on `adopt.remote` / `adopt.docker`, so a session
// wrapping someone's in-container or over-ssh claude looks local here.
// Its workspace was never Codeman's to begin with, so installing hooks
// there writes `.claude/settings.local.json` into the human's own repo —
// there writes `.claude/settings.local.json` into the human's own repo,
// on EVERY restart, since this sweep runs from boot recovery.
if (session.isAdopted) continue;
if (session.docker && !session.docker.hooksEnabled) continue;
+1 -1
View File
@@ -208,7 +208,7 @@ export interface HookCapabilityOptions {
* be `mode: 'claude'` and still have no hooks, because hooks are installed into
* a WORKSPACE at session-create time (`applyWorkspaceHooks`) and we never
* created this one. Answering from the mode there would promise `stop` and
* `blocked` for a process that can never post either — the exact
* `blocked` for a process that can never post either, the exact
* infinite-wait-dressed-as-a-timeout this predicate exists to prevent.
*/
adopted?: boolean;