Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cli-session-name-matches-tab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'aicodeman': minor
---

New App Settings option, "Pin tab names on the CLI" (off by default): launch every Claude session with `--name <tab name>`, not only the ones you renamed, so Claude Code's prompt-box label, `/resume` entry and remote session list read the same as the tab instead of Claude's own generated title. With it off nothing changes. The cost: Claude stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn (an auto-name reaches the CLI at its next spawn). The setting is read at each session start and respawn, so a toggle applies without a server restart.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph

**MCP server sync** (opt-in, `mcpSyncEnabled`, SYNCED, default OFF; `src/mcp-sync.ts`, `GET`/`POST /api/mcp-sync`): copies each installed, enabled CLI's user-level MCP servers into the others. It is the ONE subsystem that writes another CLI's REAL user config (`~/.claude.json`, `~/.codex/config.toml`, `~/.gemini/*`, opencode's), which is why it is opt-in and admin-only in multi-user mode (both verbs 403 for a non-admin, and the Settings group is hidden for them). Where each CLI keeps the file is registry data, `capabilities.mcpConfig` (`{ path, format, relocation? }`), never a branch on the id. ⚠️ Tools that sync but are not run modes (GitHub Copilot CLI, the first) are plain data in `MCP_SYNC_ONLY_TOOLS` (`src/mcp-sync-targets.ts`), take part only when installed or already configured, and their companion `settings.json` (`disabledMcpServers`) is read, never written; a test must clear their relocation vars too. ⚠️ ADDITIVE only: a name already defined, in any shape, is never edited or removed (a different same-name definition is a reported conflict), and a server switched off in its own CLI is never copied. ⚠️ Never write a file that did not parse; re-parse the NEW text and require every added server to read back before the tmp+rename (written through a symlink, previous file kept as `<file>.codeman-bak`, one apply at a time, else 409). ⚠️ A file that receives copied `env`/`headers` (secrets) is left `0600`, and so is the backup. ⚠️ Responses carry server NAMES only, never env values, headers or file text: a parse failure is reported by line and column (`describeMcpSyncError`), never the parser's own message (smol-toml and V8 both quote source). ⚠️ `mcpConfig.relocation` names the env var the CLI reads to move its file (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `XDG_CONFIG_HOME`, `GEMINI_CLI_HOME`, and `COPILOT_HOME` for Copilot), resolved from the SERVER env at call time; a relative value reports the target `skipped`, never a guessed write, and a per-session `envOverrides` relocation is not followed. Tests must pass `home` (which drops the `process.env` default) or clear those vars first. → `docs/cli-registry.md` (MCP server sync), `docs/api-reference.md`, `docs/wiki/Settings-Reference.md`

**Auto-named sessions** (`autoNameSessions`, SYNCED, default OFF): a placeholder tab (`w3-myapp`) takes its first real prompt as a title in the `<prefix>: <title>` form, so the case identity and `w<n>` counter survive. Ownership is `SessionState.nameSource` (`placeholder` | `auto` | `manual`; the `name` setter / `PUT /api/sessions/:id/name` makes it `manual`, never touched again). ⚠️ `applyAutoName()` flips to `auto` even if the string is unchanged, so only the FIRST titled prompt names the tab. ⚠️ Only user input counts: `SessionWriteOptions.fromUser` is set by the browser WS path and `POST /api/sessions/:id/input` ONLY; any new user-input path must set it (and the send-key Shift+Enter path must call `trackUserInput()`). ⚠️ The pure tracker (`session-auto-name.ts`) sits on the raw keystroke stream with an explicit rule per key; add a rule for any new key class. ⚠️ `nameSource` also decides `--name`: only a `manual` name is pinned on the claude CLI (`Session.cliPinnedName`), since `--name` is also the `/resume` title; a rename appends a `custom-title` row to a LOCAL, non-docker transcript, and a same-name PUT is a no-op (never flips to `manual`). Tests: `test/session-auto-name.test.ts`. → [architecture-invariants#auto-named-sessions-first-prompt--tab-title](docs/architecture-invariants.md#auto-named-sessions-first-prompt--tab-title)
**Auto-named sessions** (`autoNameSessions`, SYNCED, default OFF): a placeholder tab (`w3-myapp`) takes its first real prompt as a title in the `<prefix>: <title>` form, so the case identity and `w<n>` counter survive. Ownership is `SessionState.nameSource` (`placeholder` | `auto` | `manual`; the `name` setter / `PUT /api/sessions/:id/name` makes it `manual`, never touched again). ⚠️ `applyAutoName()` flips to `auto` even if the string is unchanged, so only the FIRST titled prompt names the tab. ⚠️ Only user input counts: `SessionWriteOptions.fromUser` is set by the browser WS path and `POST /api/sessions/:id/input` ONLY; any new user-input path must set it (and the send-key Shift+Enter path must call `trackUserInput()`). ⚠️ The pure tracker (`session-auto-name.ts`) sits on the raw keystroke stream with an explicit rule per key; add a rule for any new key class. ⚠️ `nameSource` also decides `--name`: by default only a `manual` name is pinned on the claude CLI (`Session.cliPinnedName`), since `--name` is also the `/resume` title and a pinned one stops Claude generating its own; the opt-in synced `pinCliSessionName` (default OFF, re-read at every spawn and respawn) pins every tab name, after which a Codeman rename holds only until the next turn; a rename appends a `custom-title` row to a LOCAL, non-docker transcript, and a same-name PUT is a no-op (never flips to `manual`). Tests: `test/session-auto-name.test.ts`. → [architecture-invariants#auto-named-sessions-first-prompt--tab-title](docs/architecture-invariants.md#auto-named-sessions-first-prompt--tab-title)

**Maintainer bot (external)**: the Telegram bot that reviews open PRs and triages discussion threads in Codeman sessions used to live at `scripts/pr-bot/`. It moved OUT of this repository on 2026-09-14, to `~/codeman-cases/prbot/` (its own private git repo, systemd unit `codeman-pr-bot`, guide + agent rules in its own `README.md` and `CLAUDE.md`). It is a CLIENT of Codeman's HTTP API like any other, so nothing here depends on it and it is not part of the server, the CLI or the npm package. ⚠️ It spawns real sessions named `prbot-<n>` / `dscbot-<n>` on the local Codeman and holds clones under `~/.codeman/pr-bot/`, so those session names and that data dir are taken; it also fetches PR heads into `refs/pr-bot/*` of this checkout and must never check out, reset or clean it. The CHANGELOG entries for 1.25.0 and earlier still describe it, which is history rather than drift.

Expand Down
7 changes: 5 additions & 2 deletions docs/agent-control-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -761,9 +761,12 @@ workers. Tests: `test/name-flag-injection.test.ts`.
Later narrowing: `--name` is not only the peer name but also the `/resume` picker
entry and the terminal title, and a pinned title stops Claude generating its own, so
pinning the `w1-myapp` placeholder listed every conversation of a case under the same
name in `/resume`. Only a manual name is pinned now (`Session.cliPinnedName`,
name in `/resume`. Only a manual name is pinned by default (`Session.cliPinnedName`,
`nameSource === 'manual'`, carried to the builders as `cliName`); placeholder and auto
names leave Claude to title the conversation. A rename in Codeman appends a
names leave Claude to title the conversation. The opt-in synced `pinCliSessionName`
(default OFF, re-read at every spawn and respawn) pins every tab name instead, at the
cost of Claude's generated titles; a rename then holds only until the next turn, because
a pane spawned with `--name` re-appends its in-memory title. A rename in Codeman appends a
`custom-title` row to the conversation's transcript (`claude-session-title.ts`), the
row `/rename` writes. Tests: `test/claude-resume-title.test.ts`,
`test/routes/session-name-routes.test.ts`.
2 changes: 1 addition & 1 deletion docs/architecture-invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ History: before the tree, every pair got a U-bridge bezier hanging below the str

**Opt-in, and the listener orders its checks for cost.** The prompt lands in the tab name, `mux-sessions.json`, every `session:updated` broadcast, the TUI, both home screens and `/api/search` (which matches on `sessionName`), while Read My Mind deliberately keeps prompts 0600 and out of search because prompts can carry secrets; so `autoNameSessions` is synced and default OFF, like `agentSkillEnabled`, `approvalsInboxEnabled` and `readMyMindEnabled`. The listener checks `nameSource` and derives the title BEFORE reading `settings.json`, so an already-named session costs nothing per prompt.

**`nameSource` also decides what claude is told to call the conversation.** `--name` is the prompt-box label, the `/resume` picker entry and the terminal title at once, and a pinned title stops Claude generating its own, so only a `manual` name is passed (`Session.cliPinnedName`); pinning the `w1-myapp` placeholder gave every conversation of a case the same `/resume` entry. A rename through `PUT /api/sessions/:id/name` is carried into `/resume` by appending the `custom-title` row `/rename` writes (`claude-session-title.ts`), for local transcripts only: a remote pane's lives on the remote host and a docker pane's inside the container. The route returns early on an unchanged name, because the Session Options field saves on blur and recomposes the same placeholder, which would otherwise flip it to `manual` and re-pin it.
**`nameSource` also decides what claude is told to call the conversation.** `--name` is the prompt-box label, the `/resume` picker entry and the terminal title at once, and a pinned title stops Claude generating its own, so by default only a `manual` name is passed (`Session.cliPinnedName`); pinning the `w1-myapp` placeholder gave every conversation of a case the same `/resume` entry, because `_nextCaseSessionStartNumber()` numbers from the highest OPEN tab and restarts once a case's tabs are closed. The opt-in synced `pinCliSessionName` (default OFF) pins every non-blank tab name for users who want the CLI labels to match their tabs; it is read fresh at every claude create and respawn (`readPinCliSessionNameEnabled()`, hooks-config.ts, like `readPlanUsageTelemetryEnabled()`), so a toggle applies from the next spawn with no restart. ⚠️ Its cost: Claude stops generating its own titles for those sessions, and a rename holds only until the next turn, because a pane spawned with `--name` re-appends its in-memory title after every turn (`claude-session-title.ts`); a server restart re-attaches the live pane without respawning it. A rename through `PUT /api/sessions/:id/name` is carried into `/resume` by appending the `custom-title` row `/rename` writes (`claude-session-title.ts`), for local transcripts only: a remote pane's lives on the remote host and a docker pane's inside the container. The route returns early on an unchanged name, because the Session Options field saves on blur and recomposes the same placeholder, which would otherwise flip it to `manual` and re-pin it.

Further detail: the `<prefix>: <title>` form (`w3-myapp: fix the login redirect`) is the one `parseSessionPrefix()` (app.js, #232) already renders as the title alone with the prefix in the tooltip, and that `_nextCaseSessionStartNumber()` still counts. The `name` setter is reached via `PUT /api/sessions/:id/name`. The listener lives in `session-listener-wiring.ts`. Tests: `test/session-auto-name.test.ts`, `test/session-listener-wiring.test.ts`, `test/routes/session-name-routes.test.ts`.

Expand Down
6 changes: 3 additions & 3 deletions plugins/codeman/skills/codeman/reference/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ CLI is 2.1.224+ (`buildNameCliArgs`, `session-cli-builder.ts:97-101`, wired in a
the worker's messages arrive tagged `from-name="<that name>"`; a derived-name worker's
messages carry no `from-name`). Name your workers: a quick-start WITHOUT
`sessionName` leaves the Codeman name empty, so there is nothing to pass and the
peer name stays derived. ⚠️ Give them a DESCRIPTIVE name: only a name the user chose
is pinned (`Session.cliPinnedName`), because `--name` is also the conversation's
peer name stays derived. ⚠️ Give them a DESCRIPTIVE name: by default only a name the user chose
is pinned (`Session.cliPinnedName`; the opt-in `pinCliSessionName` setting pins every tab name, and a rename then holds only until the next turn), because `--name` is also the conversation's
`/resume` title and terminal title and suppresses Claude's own generated title. A
placeholder-shaped name (`w9-msgtest`, anything matching `isGeneratedSessionName`)
and an auto name are NOT passed, so such a worker's peer name is derived; use
and an auto name are NOT passed (with `pinCliSessionName` off, the default), so such a worker's peer name is derived; use
`msgtest-worker` rather than `w9-msgtest`. The flag is fail-closed (older/unknown CLI omits it, because an
unknown flag aborts startup and would kill every spawn) and allowlist-sanitized (a name of
only unsafe characters is dropped), and the docker/remote builders never see it at all
Expand Down
2 changes: 1 addition & 1 deletion plugins/codeman/skills/codeman/reference/verbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ The shape, each step verified live (probes, failure modes and safety detail in
column; the row's `name [ref]` is the address. On Codeman 1.16+ with claude
2.1.224+ a worker's peer name is its Codeman session name, so pass a DESCRIPTIVE
`sessionName` in quick-start to pick it (a `w<N>-` placeholder-shaped name is not
pinned, so it lists derived); older setups list a name derived from the case folder.
pinned by default, so it lists derived); older setups list a name derived from the case folder.
No row = messaging is off for that worker (it is feature-flagged even on matching
CLI versions, observed live): fall back to the HTTP recipes without complaint.
3. `SendMessage` the task; first contact must use the `name [ref]` form copied from
Expand Down
6 changes: 3 additions & 3 deletions skills/codeman/reference/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ CLI is 2.1.224+ (`buildNameCliArgs`, `session-cli-builder.ts:97-101`, wired in a
the worker's messages arrive tagged `from-name="<that name>"`; a derived-name worker's
messages carry no `from-name`). Name your workers: a quick-start WITHOUT
`sessionName` leaves the Codeman name empty, so there is nothing to pass and the
peer name stays derived. ⚠️ Give them a DESCRIPTIVE name: only a name the user chose
is pinned (`Session.cliPinnedName`), because `--name` is also the conversation's
peer name stays derived. ⚠️ Give them a DESCRIPTIVE name: by default only a name the user chose
is pinned (`Session.cliPinnedName`; the opt-in `pinCliSessionName` setting pins every tab name, and a rename then holds only until the next turn), because `--name` is also the conversation's
`/resume` title and terminal title and suppresses Claude's own generated title. A
placeholder-shaped name (`w9-msgtest`, anything matching `isGeneratedSessionName`)
and an auto name are NOT passed, so such a worker's peer name is derived; use
and an auto name are NOT passed (with `pinCliSessionName` off, the default), so such a worker's peer name is derived; use
`msgtest-worker` rather than `w9-msgtest`. The flag is fail-closed (older/unknown CLI omits it, because an
unknown flag aborts startup and would kill every spawn) and allowlist-sanitized (a name of
only unsafe characters is dropped), and the docker/remote builders never see it at all
Expand Down
2 changes: 1 addition & 1 deletion skills/codeman/reference/verbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ The shape, each step verified live (probes, failure modes and safety detail in
column; the row's `name [ref]` is the address. On Codeman 1.16+ with claude
2.1.224+ a worker's peer name is its Codeman session name, so pass a DESCRIPTIVE
`sessionName` in quick-start to pick it (a `w<N>-` placeholder-shaped name is not
pinned, so it lists derived); older setups list a name derived from the case folder.
pinned by default, so it lists derived); older setups list a name derived from the case folder.
No row = messaging is off for that worker (it is feature-flagged even on matching
CLI versions, observed live): fall back to the HTTP recipes without complaint.
3. `SendMessage` the task; first contact must use the `name [ref]` form copied from
Expand Down
10 changes: 6 additions & 4 deletions src/claude-session-title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
* used to change only the tab, so `/resume` kept listing the old name.
*
* Appending the row is enough for a pane that was spawned WITHOUT `--name`
* (every placeholder- or auto-named tab, see `Session.cliPinnedName`): that
* process holds no title of its own and never writes one back. A process that
* WAS spawned with `--name` re-appends its in-memory title after each turn, so
* there the new title holds from the next spawn, which pins the new name.
* (every placeholder- or auto-named tab, unless the opt-in `pinCliSessionName`
* setting is on, see `Session.cliPinnedName`): that process holds no title of
* its own and never writes one back. A process that WAS spawned with `--name`
* (every tab with `pinCliSessionName` on) re-appends its in-memory title after
* each turn, so there the new title holds only until the next turn, then from
* the next spawn, which pins the new name.
*
* @module claude-session-title
*/
Expand Down
13 changes: 13 additions & 0 deletions src/hooks-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,19 @@ export async function readPlanUsageTelemetryEnabled(): Promise<boolean> {
return settings.showPlanUsageLimits !== false;
}

/**
* Whether every Claude session is launched with `--name` set to its tab name
* (the `pinCliSessionName` setting). Read FRESH from settings.json at every
* claude create and respawn, like {@link readPlanUsageTelemetryEnabled}, so a
* toggle applies from the next spawn with no server restart. An ABSENT key
* means OFF: only a name the user chose is pinned, and Claude titles the
* placeholder- and auto-named conversations itself (see `Session.cliPinnedName`).
*/
export async function readPinCliSessionNameEnabled(): Promise<boolean> {
const settings = await readJsonConfig<Record<string, unknown>>(SETTINGS_PATH, 'settings.json', {});
return settings.pinCliSessionName === true;
}

/**
* Resolve the statusLine command to pass as an EPHEMERAL `claude --settings`
* CLI flag for this one process (see buildSpawnCommandFromRegistry in
Expand Down
6 changes: 4 additions & 2 deletions src/mux-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ export interface CreateSessionOptions {
* Name pinned on a claude spawn as `--name` (version-gated, sanitized, local only).
* Deliberately NOT `name`: `--name` owns the prompt-box label, the `/resume` picker
* entry and the terminal title, and a pinned title stops Claude generating its own,
* so only a user-chosen name belongs here (see `Session.cliPinnedName`).
* so by default only a user-chosen name belongs here; the opt-in `pinCliSessionName`
* setting pins every tab name (see `Session.cliPinnedName`).
*/
cliName?: string;
niceConfig?: NiceConfig;
Expand Down Expand Up @@ -141,7 +142,8 @@ export interface RespawnPaneOptions {
* Name pinned on a respawned claude as `--name` (version-gated, sanitized, local only).
* Deliberately NOT `name`: `--name` owns the prompt-box label, the `/resume` picker
* entry and the terminal title, and a pinned title stops Claude generating its own,
* so only a user-chosen name belongs here (see `Session.cliPinnedName`).
* so by default only a user-chosen name belongs here; the opt-in `pinCliSessionName`
* setting pins every tab name (see `Session.cliPinnedName`).
*/
cliName?: string;
niceConfig?: NiceConfig;
Expand Down
Loading
Loading