Merge pull request #646: feat(sessions): launch every Claude session with --name set to its tab name

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 08:16:41 +02:00
commit d2f6021132
20 files changed
+250 -53

No files matched your search

+6 -4
View File
@@ -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
*/
+13
View File
@@ -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
+4 -2
View File
@@ -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;
@@ -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;
+28 -8
View File
@@ -499,6 +499,8 @@ export class Session extends EventEmitter {
private _name: string;
private _nameSource: SessionNameSource;
/** `pinCliSessionName` as of the last spawn (see {@link _refreshCliNamePin}). */
private _pinEveryCliName = false;
/**
* Reconstructs the composer draft from USER keystrokes so the first real
* prompt can name the tab. Fed only when a write says `fromUser`, and never
@@ -1806,16 +1808,31 @@ export class Session extends EventEmitter {
}
/**
* The name to pin on the Claude CLI as `--name`, or undefined to let Claude
* title the conversation itself. `--name` is the prompt-box label, the
* `/resume` picker entry and the terminal title all at once, and a pinned
* title stops Claude generating its own, so only a name the user chose is
* worth pinning. Pinning the `w1-myapp` placeholder gave every conversation
* in a case the same `/resume` entry; an auto name is a cut of the first
* prompt, which Claude's own generated title already beats.
* The name to pin on the Claude CLI as `--name`, or undefined to let Claude title the
* conversation itself. `--name` is the prompt-box label, the `/resume` picker entry and the
* terminal title all at once, and a pinned title stops Claude generating its own.
*
* By default only a name the user chose is pinned. Pinning the `w1-myapp` placeholder gave
* every conversation in a case the same `/resume` entry (the tab number restarts once a
* case's tabs are closed), and an auto name is a cut of the first prompt, which Claude's
* own generated title already beats.
*
* With the opt-in `pinCliSessionName` setting every non-blank tab name is pinned, so the
* CLI's labels read the same as the tabs, at the cost of those generated titles. A rename
* in Codeman then holds only until the next turn: a process started with `--name`
* re-appends its in-memory title (see claude-session-title.ts). The setting is re-read
* at each spawn and respawn by {@link _refreshCliNamePin}.
*/
get cliPinnedName(): string | undefined {
return this._nameSource === 'manual' ? this._name : undefined;
if (!this._pinEveryCliName) return this._nameSource === 'manual' ? this._name : undefined;
return this._name.trim() ? this._name : undefined;
}
/** Re-read `pinCliSessionName` so `cliPinnedName` decides per launch, with no server restart. */
private async _refreshCliNamePin(): Promise<void> {
// Dynamic: hooks-config -> route-helpers imports Session, so a static import would be a cycle.
const { readPinCliSessionNameEnabled } = await import('./hooks-config.js');
this._pinEveryCliName = await readPinCliSessionNameEnabled();
}
setAutoClear(enabled: boolean, threshold?: number): void {
@@ -2475,6 +2492,7 @@ export class Session extends EventEmitter {
* still inside it and renders no local `--session-id` to collide.
*/
private async _buildRespawnPaneOptionsWithResumePin(): Promise<import('./mux-interface.js').RespawnPaneOptions> {
await this._refreshCliNamePin();
const options = this._buildRespawnPaneOptions();
if (this._remote || this._docker) return options;
const entry = getCli(this.mode);
@@ -2739,6 +2757,8 @@ export class Session extends EventEmitter {
throw new Error('Session is being closed');
}
await this._refreshCliNamePin();
// Bounds the workspace-trust scan (see _maybeAcceptTrustDialog). Stamped here
// rather than at PTY spawn so a slow mux attach still counts as startup.
this._interactiveStartedAt = Date.now();
+1 -1
View File
@@ -1089,7 +1089,7 @@ export function buildSpawnCommand(options: {
advisorModel?: string;
/** Resolved by resolveStatusLineCliCommand (hooks-config.ts) — undefined skips the exporter. Claude only. */
statusLineCommand?: string;
/** Name pinned on claude as `--name` (version-gated, sanitized; local spawns only). Only a user-chosen name: see `Session.cliPinnedName`. */
/** Name pinned on claude as `--name` (version-gated, sanitized; local spawns only). By default only a user-chosen name; `pinCliSessionName` pins every tab name: see `Session.cliPinnedName`. */
sessionName?: string;
/**
* Claude CLI version for the `--name` gate. Omitted = probe the local CLI
+3
View File
@@ -678,6 +678,9 @@
'Claude Model': 'Claude 模型',
'1M Opus Context': 'Opus 100 万上下文',
'Remote auto-reconnect': '远程自动重连',
'Pin tab names on the CLI': '在 CLI 上固定标签名',
"Launch every Claude session with its tab name, so the CLI's prompt label and /resume list match your tabs. Claude then stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn. Applies from the next session start or respawn, no server restart needed.":
'以标签名启动每个 Claude 会话,让 CLI 的提示框标签和 /resume 列表与你的标签一致。Claude 随后不再为这些会话生成自己的标题,在 Codeman 中的重命名也只保留到下一轮。从下一次会话启动或重新生成时生效,无需重启服务器。',
'Thinking Effort': '思考强度',
Low: '低',
Medium: '中',
+7
View File
@@ -2787,6 +2787,13 @@
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsWorkspaceHooks"><span class="slider"></span></label>
</div>
<div class="set-row" data-search="pin cli session name resume title tab label">
<div class="set-row-text">
<span class="set-row-label">Pin tab names on the CLI</span>
<span class="set-row-desc">Launch every Claude session with its tab name, so the CLI's prompt label and /resume list match your tabs. Claude then stops generating its own titles for those sessions, and a rename in Codeman holds only until the next turn. Applies from the next session start or respawn, no server restart needed.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsPinCliSessionName"><span class="slider"></span></label>
</div>
<div class="set-row" data-search="remote auto reconnect ssh">
<div class="set-row-text">
<span class="set-row-label">Remote auto-reconnect</span>
+2
View File
@@ -588,6 +588,7 @@ Object.assign(CodemanApp.prototype, {
// Default ON: an absent key is a user who has never seen this setting, and OFF
// for them means no tab alerts in any workspace Codeman did not scaffold.
document.getElementById('appSettingsWorkspaceHooks').checked = settings.workspaceHooksEnabled !== false;
document.getElementById('appSettingsPinCliSessionName').checked = settings.pinCliSessionName === true;
document.getElementById('appSettingsClaudeModel').value = settings.claudeModel ?? '';
document.getElementById('appSettingsOpusContext1m').checked = settings.opusContext1mEnabled ?? false;
document.getElementById('appSettingsRemoteAutoReconnect').checked = settings.remoteAutoReconnect ?? true;
@@ -2708,6 +2709,7 @@ Object.assign(CodemanApp.prototype, {
agentTeamsEnabled: document.getElementById('appSettingsAgentTeams').checked,
agentSkillEnabled: document.getElementById('appSettingsAgentSkill').checked,
workspaceHooksEnabled: document.getElementById('appSettingsWorkspaceHooks').checked,
pinCliSessionName: document.getElementById('appSettingsPinCliSessionName').checked,
claudeVoiceEnabled: document.getElementById('appSettingsClaudeVoice').checked,
claudeModel: document.getElementById('appSettingsClaudeModel').value,
opusContext1mEnabled: document.getElementById('appSettingsOpusContext1m').checked,
+8
View File
@@ -1425,6 +1425,14 @@ export const SettingsUpdateSchema = z
* never added — for a user who wants Codeman to leave their repos alone.
*/
workspaceHooksEnabled: z.boolean().optional(),
/**
* Launch every Claude session with `--name` set to its tab name (default OFF: only a
* name the user chose is pinned). Nothing acts on it in `PUT /api/settings`: it is read
* fresh from disk by readPinCliSessionNameEnabled() (hooks-config.ts) at every claude
* create and respawn, like readPlanUsageTelemetryEnabled(), so a toggle applies from the
* next spawn with no restart.
*/
pinCliSessionName: z.boolean().optional(),
/**
* Let browser dictation transcribe through this machine's Claude Code login,
* the same speech-to-text service the CLI's own `/voice` mode uses