mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
fix(sessions): stop pinning the w1-myapp placeholder as Claude's /resume title (#457)
* fix(sessions): stop pinning the w1-myapp placeholder as Claude's /resume title Local claude spawns passed the tab name as `--name`. That flag is not only the cross-session peer name: it is also the prompt-box label, the `/resume` picker entry and the terminal title, and a pinned title stops Claude generating its own (`customTitle ?? aiTitle`). So every conversation of a case was listed in `/resume` as the same `w1-myapp`, and none of them got a generated title. On one workspace, 34 of 34 conversations spawned with `--name` had no ai-title, while every conversation spawned without it had one. Only a name the user chose is pinned now: `Session.cliPinnedName` is the name when `nameSource === 'manual'`, carried to the builders as a separate `cliName` so the tab/mux name is untouched. Placeholder and auto names let Claude title the conversation again. A rename in Codeman also reaches `/resume`: the new name is appended to the conversation's transcript as the `custom-title` row `/rename` writes (never creating the file, never writing an empty title). For a pane spawned without `--name` this holds immediately; a pane spawned with one re-appends its own title each turn, so there the new name holds from the next spawn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sessions): skip no-op renames and docker sessions when syncing the /resume title A same-name PUT (the Session Options field saves on blur and recomposes the unchanged placeholder) no longer flips nameSource to manual or appends a custom-title row, and docker sessions skip the host transcript scan since their transcript lives in the container. The skill pages no longer use a w<N>- name as the peer-name example, and the changeset notes the re-append caveat. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: record that nameSource decides --name and renames reach /resume Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: codeman-local <codeman@local> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
codeman-local
parent
7485afecaf
commit
69a71287e6
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @fileoverview Carry a Codeman rename into Claude Code's own session title.
|
||||
*
|
||||
* Claude Code keeps a conversation's title in its transcript as a
|
||||
* `{"type":"custom-title"}` row (what `/rename` writes), last row wins, and the
|
||||
* `/resume` picker shows `customTitle ?? aiTitle`. Renaming a tab in Codeman
|
||||
* 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.
|
||||
*
|
||||
* @module claude-session-title
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
/**
|
||||
* Append a `custom-title` row for `conversationId` to an existing transcript.
|
||||
* Never creates the file: a missing transcript means the conversation has not
|
||||
* been written yet, and a file of only a title row would show up in `/resume`
|
||||
* as an empty conversation. Returns whether a row was written.
|
||||
*/
|
||||
export async function appendClaudeCustomTitle(
|
||||
transcriptPath: string,
|
||||
conversationId: string,
|
||||
title: string
|
||||
): Promise<boolean> {
|
||||
const customTitle = title.trim();
|
||||
// Claude reads the row through `customTitle ?? aiTitle`, so an empty string
|
||||
// would blank the picker entry rather than fall back to the generated title.
|
||||
if (!customTitle) return false;
|
||||
try {
|
||||
if (!(await fs.stat(transcriptPath)).isFile()) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// One O_APPEND write of one line, the same way Claude appends its own rows,
|
||||
// so it cannot interleave with a row the live process is writing.
|
||||
const row = JSON.stringify({ type: 'custom-title', customTitle, sessionId: conversationId });
|
||||
await fs.appendFile(transcriptPath, `${row}\n`);
|
||||
return true;
|
||||
}
|
||||
+15
-1
@@ -90,6 +90,13 @@ export interface CreateSessionOptions {
|
||||
workingDir: string;
|
||||
mode: SessionMode;
|
||||
name?: string;
|
||||
/**
|
||||
* 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`).
|
||||
*/
|
||||
cliName?: string;
|
||||
niceConfig?: NiceConfig;
|
||||
model?: string;
|
||||
claudeMode?: ClaudeMode;
|
||||
@@ -123,8 +130,15 @@ export interface RespawnPaneOptions {
|
||||
sessionId: string;
|
||||
workingDir: string;
|
||||
mode: SessionMode;
|
||||
/** Session display name; a respawned claude keeps its `--name` peer name (version-gated, local only). */
|
||||
/** Session display name (tab name). */
|
||||
name?: string;
|
||||
/**
|
||||
* 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`).
|
||||
*/
|
||||
cliName?: string;
|
||||
niceConfig?: NiceConfig;
|
||||
model?: string;
|
||||
claudeMode?: ClaudeMode;
|
||||
|
||||
+16
-1
@@ -1560,6 +1560,19 @@ export class Session extends EventEmitter {
|
||||
return this._nameSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get cliPinnedName(): string | undefined {
|
||||
return this._nameSource === 'manual' ? this._name : undefined;
|
||||
}
|
||||
|
||||
setAutoClear(enabled: boolean, threshold?: number): void {
|
||||
this._autoOps.setAutoClear(enabled, threshold);
|
||||
}
|
||||
@@ -2093,6 +2106,7 @@ export class Session extends EventEmitter {
|
||||
workingDir: this.workingDir,
|
||||
mode: this.mode,
|
||||
name: this._name,
|
||||
cliName: this.cliPinnedName,
|
||||
niceConfig: this._niceConfig,
|
||||
model: this._model,
|
||||
claudeMode: this._claudeMode,
|
||||
@@ -2561,6 +2575,7 @@ export class Session extends EventEmitter {
|
||||
workingDir: this.workingDir,
|
||||
mode: this.mode,
|
||||
name: this._name,
|
||||
cliName: this.cliPinnedName,
|
||||
niceConfig: this._niceConfig,
|
||||
model: this._model,
|
||||
claudeMode: this._claudeMode,
|
||||
@@ -2699,7 +2714,7 @@ export class Session extends EventEmitter {
|
||||
this._model,
|
||||
this._allowedTools,
|
||||
this._effort,
|
||||
this._name,
|
||||
this.cliPinnedName,
|
||||
getClaudeCliVersion()
|
||||
);
|
||||
this.ptyProcess = spawnPtyWithHelperRepair(() =>
|
||||
|
||||
+5
-4
@@ -871,7 +871,7 @@ export function buildSpawnCommand(options: {
|
||||
effort?: EffortLevel;
|
||||
/** Resolved by resolveStatusLineCliCommand (hooks-config.ts) — undefined skips the exporter. Claude only. */
|
||||
statusLineCommand?: string;
|
||||
/** Codeman session name, passed to claude as `--name` (version-gated, sanitized; local spawns only). */
|
||||
/** Name pinned on claude as `--name` (version-gated, sanitized; local spawns only). Only a user-chosen name: see `Session.cliPinnedName`. */
|
||||
sessionName?: string;
|
||||
/**
|
||||
* Claude CLI version for the `--name` gate. Omitted = probe the local CLI
|
||||
@@ -2054,6 +2054,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
workingDir,
|
||||
mode,
|
||||
name,
|
||||
cliName,
|
||||
niceConfig,
|
||||
model,
|
||||
claudeMode,
|
||||
@@ -2157,7 +2158,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
resumeSessionId,
|
||||
effort,
|
||||
statusLineCommand,
|
||||
sessionName: name,
|
||||
sessionName: cliName,
|
||||
});
|
||||
|
||||
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
||||
@@ -2385,7 +2386,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
effort,
|
||||
remote,
|
||||
docker,
|
||||
name,
|
||||
cliName,
|
||||
} = options;
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return null;
|
||||
@@ -2422,7 +2423,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
resumeSessionId,
|
||||
effort,
|
||||
statusLineCommand,
|
||||
sessionName: name,
|
||||
sessionName: cliName,
|
||||
});
|
||||
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
||||
const cmd = wrapWithNice(baseCmd, config);
|
||||
|
||||
@@ -176,6 +176,7 @@ import {
|
||||
selectLastAnsweredTurn,
|
||||
} from '../response-viewer-transcript.js';
|
||||
import { readDeepSeekLastResponse } from '../../deepseek-transcript.js';
|
||||
import { appendClaudeCustomTitle } from '../../claude-session-title.js';
|
||||
|
||||
// Path to linked-cases registry (same file used by case-routes resolveCasePath)
|
||||
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
||||
@@ -1162,10 +1163,14 @@ export function registerSessionRoutes(
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
const name = String(body.name || '').slice(0, MAX_SESSION_NAME_LENGTH);
|
||||
// A no-op rename (the Session Options name field saves on blur and recomposes the same
|
||||
// string) must not flip nameSource to 'manual' or append a custom-title row to the transcript.
|
||||
if (name === session.name) return { name: session.name };
|
||||
session.name = name;
|
||||
// Also update the mux session name if applicable
|
||||
ctx.mux.updateSessionName(id, session.name);
|
||||
persistAndBroadcastSession(ctx, session);
|
||||
await syncClaudeTitle(session);
|
||||
return { name: session.name };
|
||||
});
|
||||
|
||||
@@ -2392,6 +2397,26 @@ export function registerSessionRoutes(
|
||||
return full ? { text: lastText, timestamp: lastTimestamp, messages } : { text: lastText, timestamp: lastTimestamp };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror a rename into the conversation's `/resume` title (claude-session-title.ts).
|
||||
* Local Claude-format transcripts only: a remote pane's transcript lives on the remote host and a
|
||||
* docker pane's inside the container (HOME=/home/agent), never under the host's projects dir. Best
|
||||
* effort: the tab rename has already happened and must not fail on this.
|
||||
*/
|
||||
async function syncClaudeTitle(session: Session): Promise<void> {
|
||||
if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' || session.remote || session.docker) return;
|
||||
try {
|
||||
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
||||
const hookPath = ctx.getTranscriptPath(session.id);
|
||||
const transcript = hookPath
|
||||
? { sessionId: basename(hookPath, '.jsonl'), path: hookPath }
|
||||
: await findClaudeTranscript(projectsDir, session.claudeSessionId || session.id, session.id);
|
||||
if (transcript) await appendClaudeCustomTitle(transcript.path, transcript.sessionId, session.name);
|
||||
} catch (err) {
|
||||
console.warn(`[Session] Could not carry rename into the Claude transcript for ${session.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/** Locate a top-level Claude transcript, including recovered tmux sessions. */
|
||||
async function findClaudeTranscript(
|
||||
projectsDir: string,
|
||||
|
||||
Reference in New Issue
Block a user