From 36d89427446aab8df0fc0cff5743bf720c84c26b Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 6 Nov 2026 03:38:38 +0100 Subject: [PATCH] docs(adopt): add the frontend note and scan the module for CLI ids foreign-sessions.js joins the files the no-id test scans, so the catalog lookup stays one. The block's rules go where a frontend reader looks, in src/web/CLAUDE.md, and the architecture note no longer calls the file surfaces read-only, since File Viewer edit mode writes. Co-Authored-By: Claude Opus 5.5 --- docs/architecture-invariants.md | 2 +- src/web/CLAUDE.md | 2 ++ test/frontend-cli-no-id-branching.test.ts | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index a963e9d9..baf0c4da 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -105,7 +105,7 @@ Input arriving during a wake is buffered: a chunk over 4 KB is dropped whole, ne **One wrapper per target.** `foreignTargetKey()` is the ONE key, read by the adopt route and `adoptedBy`, built from the location, the host identity, the socket and the name: a socket and a name alone collide across containers and hosts. Both read live `Session`s, since the mux record only exists after the first attach. -**No workspace.** ⚠️ An adopted session keeps its connection on `adopt.remote`/`adopt.docker`, so `session.remote` and `session.docker` are undefined, and its `workingDir` is the neutral `/tmp`. Code that branches on those two reads an in-container or over-ssh adoption as a local session working in `/tmp`. So anything that treats `workingDir` as the session's workspace must also test `session.isAdopted` (`adopt` on a pure view): uploads, the upload GC, git status, git fetch and its scheduler, the image watcher, Read My Mind's git signals, the Discord folder, `last-response` and guarded input all do. The Files panel still lists `/tmp`: read-only, and adoption is admin-only in multi-user mode. A wrapper reports `statusLineExporter: 'remote'`, which is also what keeps keep-warm off it. +**No workspace.** ⚠️ An adopted session keeps its connection on `adopt.remote`/`adopt.docker`, so `session.remote` and `session.docker` are undefined, and its `workingDir` is the neutral `/tmp`. Code that branches on those two reads an in-container or over-ssh adoption as a local session working in `/tmp`. So anything that treats `workingDir` as the session's workspace must also test `session.isAdopted` (`adopt` on a pure view): uploads, the upload GC, git status, git fetch and its scheduler, the image watcher, Read My Mind's git signals, the Discord folder, `last-response` and guarded input all do. The file surfaces (Files panel, File Viewer and its edit mode) still treat `/tmp` as the workspace, which is harmless: it is world-writable anyway, and adoption is admin-only in multi-user mode. A wrapper reports `statusLineExporter: 'remote'`, which is also what keeps keep-warm off it. **Lifecycle.** `paneExitApplies` is false for a wrapper, so an attach that ended is never `paneExit` and the sweep never closes the tab. Boot recovery re-attaches a dead wrapper, which prints that the session is gone and exits. Reboot restore refuses one, since all Codeman ever had was an attach command. diff --git a/src/web/CLAUDE.md b/src/web/CLAUDE.md index 7f5ab0bf..94231924 100644 --- a/src/web/CLAUDE.md +++ b/src/web/CLAUDE.md @@ -46,6 +46,8 @@ 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 … · ` 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). + **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) **Welcome "Resume Conversation" list** (terminal-ui.js): `loadHistorySessions()` fetches once and caches the corpus on `_historyAll`/`_historyCases`; every subsequent view (filter box, sort select, expand, the periodic refresh in panels-ui.js) goes through `_renderHistoryList()`, so never append rows to `#historyList` directly or re-fetch to re-sort. ⚠️ The box height is **class-driven**: expanding the list without `.history-list.expanded` leaves the collapsed `max-height` in place and just deepens a scroll well, which is the bug #260 reported (35 sessions in a ~4-row box). ⚠️ The A–Z sort keys off `_historyRowLabel()`, the SAME string the row renders (`name || firstPrompt || path`), most rows are transcript-backed and have no session name, so sorting on `name` alone silently does nothing. ⚠️ A filter implies expansion, and `_renderSearch()` hides `#historyHeader` (title + controls) as one unit while a search is active. Tests: `test/history-list-controls.test.ts`. diff --git a/test/frontend-cli-no-id-branching.test.ts b/test/frontend-cli-no-id-branching.test.ts index e4f952a5..41a1221c 100644 --- a/test/frontend-cli-no-id-branching.test.ts +++ b/test/frontend-cli-no-id-branching.test.ts @@ -36,6 +36,8 @@ const SCANNED_FILES = [ // The command palette's new-session agent picker reads its agents from the // injected catalog; panels-ui.js carries no id branch, so keep it that way. 'panels-ui.js', + // A row's mode comes from the same catalog, never from a list of ids here. + 'foreign-sessions.js', ]; /**