mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix: keep foreign discovery from throwing, and correct the adoption docs (release review)
candidatesFrom had moved outside the try when the transports moved to runForeignProbe, so a classifier failure would have reached the route as a 500. The module promises it never throws, so the guard is back for local and docker. Docs: - The api reference said `blocked` waits are always refused, and listed Ralph as refused while two of its file routes were not (they are now). - The invariants doc and CLAUDE.md gain the rules this review turned up: target syntax in names, the ping before list-panes, nothing typed into an adopted pane, quoteAdoptLaunch. - CLAUDE.md no longer says every claude session gets hooks or a stop wait. - ForeignTmuxSession's mode and workingDir describe the first listed pane, not the active one. - The wiki had no page on adoption: The Dashboard gains an Opened outside Codeman section, and the remote and docker pages point at it. docs/docker-cases.md now separates an adopted container from an adopted tmux session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25214fcc5f
commit
2d7cd49449
11 files changed
+104
-30
No files matched your search
@@ -233,11 +233,18 @@ export async function discoverLocalForeign(force = false): Promise<ForeignDiscov
|
||||
|
||||
localInFlight = (async (): Promise<ForeignDiscoveryResult> => {
|
||||
const notes: string[] = [];
|
||||
// argv array: no outer shell, so the script's own `$TMUX_TMPDIR`/`$(id -u)`
|
||||
// reach the inner sh intact (see @fileoverview).
|
||||
const run = await runForeignProbe(['sh', '-c', buildForeignProbeScript()]);
|
||||
const sessions = candidatesFrom(run.stdout, 'local', 'local', {}, 'local', notes);
|
||||
if (!run.ok) notes.push(probeFailureNote('local', run, 'tmux probe produced no result'));
|
||||
let sessions: ForeignTmuxSession[] = [];
|
||||
try {
|
||||
// argv array: no outer shell, so the script's own `$TMUX_TMPDIR`/`$(id -u)`
|
||||
// reach the inner sh intact (see @fileoverview).
|
||||
const run = await runForeignProbe(['sh', '-c', buildForeignProbeScript()]);
|
||||
sessions = candidatesFrom(run.stdout, 'local', 'local', {}, 'local', notes);
|
||||
if (!run.ok) notes.push(probeFailureNote('local', run, 'tmux probe produced no result'));
|
||||
} catch {
|
||||
// The probe answers rather than throws, so this is the classifier. "Nothing to
|
||||
// adopt here" is the only honest answer and it is never an error to the caller.
|
||||
notes.push('local: tmux probe produced no result');
|
||||
}
|
||||
const result: ForeignDiscoveryResult = { sessions, scannedAt: Date.now(), notes };
|
||||
localCache = { at: Date.now(), result };
|
||||
return result;
|
||||
@@ -322,18 +329,22 @@ export async function discoverDockerForeign(cases: DockerCase[]): Promise<Foreig
|
||||
await Promise.all(
|
||||
targets.map(async (t) => {
|
||||
const where = `docker ${t.containerName}`;
|
||||
const run = await runForeignProbe(foreignDockerProbeArgv(t));
|
||||
sessions.push(
|
||||
...candidatesFrom(
|
||||
run.stdout,
|
||||
'docker',
|
||||
`${t.hostId} ${t.containerName}`,
|
||||
{ hostId: t.hostId, hostLabel: t.label, containerName: t.containerName },
|
||||
where,
|
||||
notes
|
||||
)
|
||||
);
|
||||
if (!run.ok) notes.push(probeFailureNote(where, run, 'not running, no tmux, or engine unreachable'));
|
||||
try {
|
||||
const run = await runForeignProbe(foreignDockerProbeArgv(t));
|
||||
sessions.push(
|
||||
...candidatesFrom(
|
||||
run.stdout,
|
||||
'docker',
|
||||
`${t.hostId} ${t.containerName}`,
|
||||
{ hostId: t.hostId, hostLabel: t.label, containerName: t.containerName },
|
||||
where,
|
||||
notes
|
||||
)
|
||||
);
|
||||
if (!run.ok) notes.push(probeFailureNote(where, run, 'not running, no tmux, or engine unreachable'));
|
||||
} catch {
|
||||
notes.push(`${where}: not running, no tmux, or engine unreachable`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -79,11 +79,15 @@ export interface ForeignTmuxSession {
|
||||
attached: boolean;
|
||||
/** Epoch MILLISECONDS (tmux reports seconds; normalized on parse). */
|
||||
createdAt: number;
|
||||
/** What the active pane is running, from the bounded process-tree walk. */
|
||||
/**
|
||||
* 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;
|
||||
/** The active pane's cwd. Informational — an adopted pane is never `cd`'d. */
|
||||
/** 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;
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Desktop home tab rail** (`home-sessions.js`, desktop only): the welcome overlay's left gutter carries the open tabs as a rail docked flush left, full height, in overview order, each row showing `created … · <state> <duration>` from `_mobileOverviewSince()`; state classification is reused from mobile-overview.js (so it loads after it). ⚠️ The number badge is the Alt+1..9 tab-strip index, never renumber it to row position. ⚠️ The width gate lives in two places that must stay equal: `HOME_SESSIONS_MIN_WIDTH` (1180) and a `max-width: 1179px` media query. ⚠️ `.home-sessions[hidden]` must re-assert `display: none`. ⚠️ Size all children in `em` off the one `clamp()` knob, never `rem`/px. Age stamps tick in place (`_tickHomeSessionsTimes()`), never by re-render. Test: `test/home-sessions.test.ts`. → [architecture-invariants#desktop-home-tab-rail](../../docs/architecture-invariants.md#desktop-home-tab-rail)
|
||||
|
||||
**Opened outside Codeman** (`foreign-sessions.js`, both home screens): the tmux sessions a human started, listed from `GET /api/mux/foreign` (root CLAUDE.md, Adopted tmux sessions). ONE renderer, `renderForeignSessions(container)`, fills the welcome block and the phone overview's, and every row goes through `textContent`, since a session name and a command line come from someone else's process. ⚠️ It polls only while a home screen is up (`startForeignPolling`/`stopForeignPolling` from `showWelcome()`/`hideWelcome()`), and the poll is local only: the docker and ssh scan costs one connection per target, so it runs once on the toggle and again on Rescan, and its rows and notes are kept apart (`_foreignWideSessions`, `_foreignWideNotes`) or the next local poll would wipe them. ⚠️ A 403 is a non-admin in multi-user mode, an answer and not a failed poll: `_foreignRefused` stops the polling and shows no block. ⚠️ An adopted tab has no kill outcome to offer, so the close dialog hides the red button and always tears the wrapper down. ⚠️ `session.remote` and `session.docker` are undefined on an adopted session, so a check that hides a row for those two usually needs `session.adopt` as well. Tests: `test/foreign-sessions-ui.test.ts`, `test/foreign-sessions-i18n.test.ts` (harvests every string from the real module).
|
||||
**Opened outside Codeman** (`foreign-sessions.js`, both home screens): the tmux sessions a human started, listed from `GET /api/mux/foreign` (root CLAUDE.md, Adopted tmux sessions). ONE renderer, `renderForeignSessions(container)`, fills the welcome block and the phone overview's, and every row goes through `textContent`, since a session name and a command line come from someone else's process. ⚠️ It polls only while a home screen is up (`startForeignPolling`/`stopForeignPolling` from `showWelcome()`/`hideWelcome()`), and the poll is local only: the docker and ssh scan costs one connection per target, so it runs once on the toggle and again on Rescan, and its rows and notes are kept apart (`_foreignWideSessions`, `_foreignWideNotes`) or the next local poll would wipe them. ⚠️ A 403 is a non-admin in multi-user mode, an answer and not a failed poll: `_foreignRefused` stops the polling and shows no block. ⚠️ `closeSession()` re-reads the list after an adopted tab's DELETE, since the home screen's own load races it and the row would say `Go to tab` until the next poll. ⚠️ An adopted tab has no kill outcome to offer, so the close dialog hides the red button and always tears the wrapper down. ⚠️ `session.remote` and `session.docker` are undefined on an adopted session, so a check that hides a row for those two usually needs `session.adopt` as well. Tests: `test/foreign-sessions-ui.test.ts`, `test/foreign-sessions-i18n.test.ts` (harvests every string from the real module).
|
||||
|
||||
**Home-screen session order** (`CodemanSessionOrder` in constants.js, pure): BOTH home screens (phone overview, desktop rail) must order rows through this ONE comparator. Rank `needs` → `error` → `waiting` → `working` → `idle` → `done`. ⚠️ The tiebreak flips: states a session is still IN sort oldest-first, states it has STOPPED sort newest-first. ⚠️ The running group keys off `lastSubmitAt`, never `lastActivityAt` (a working pane repaints constantly). ⚠️ A 0 stamp means unknown and sorts last within its state. Final tiebreak is `orderIndex`, so the list never shuffles. The tab strip itself is NOT sorted by this. Test: `test/session-overview-order.test.ts`. → [architecture-invariants#home-screen-session-order](../../docs/architecture-invariants.md#home-screen-session-order)
|
||||
|
||||
|
||||
Reference in new issue
Block a user