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

+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;