mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
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:
1 parent
9d2f9663cb
commit
9442ea8cf4
27 files changed
+99
-99
No files matched your search
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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?.();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user