diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 6570813fa..cca26e6f6 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -41,7 +41,12 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas import type { Goal, GoalModeState } from "../goals/state"; import { resolveLocalUrlToPath } from "../internal-urls"; import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events"; -import { normalizePlanTitle, type PlanApprovalDetails, renameApprovedPlanFile } from "../plan-mode/approved-plan"; +import { + humanizePlanTitle, + normalizePlanTitle, + type PlanApprovalDetails, + renameApprovedPlanFile, +} from "../plan-mode/approved-plan"; import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" }; import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with { type: "text", @@ -1440,6 +1445,7 @@ export class InteractiveMode implements InteractiveModeContext { options: { planFilePath: string; finalPlanFilePath: string; + title: string; preserveContext?: boolean; compactBeforeExecute?: boolean; }, @@ -1523,6 +1529,20 @@ export class InteractiveMode implements InteractiveModeContext { return; } + // Approved plans land in a fresh (or compacted) session whose first user-visible + // turn is the synthetic plan-approved prompt — that path bypasses the + // input-controller's title generation. Seed an auto-name from the plan title + // so the session is not left unnamed. `setSessionName("auto")` is a no-op + // when the user has already chosen a name (preserveContext paths). + const seededName = humanizePlanTitle(options.title); + if (seededName && !this.sessionManager.getSessionName()) { + const applied = await this.sessionManager.setSessionName(seededName, "auto"); + if (applied) { + setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd()); + this.updateEditorBorderColor(); + } + } + // markPlanReferenceSent fires only on the dispatch path so the synthetic // plan-approved prompt is the source of the reference injection. this.session.markPlanReferenceSent(); @@ -1828,6 +1848,7 @@ export class InteractiveMode implements InteractiveModeContext { await this.#approvePlan(latestPlanContent, { planFilePath, finalPlanFilePath, + title: details.title, preserveContext: choice !== "Approve and execute", compactBeforeExecute: choice === "Approve and compact context", }); diff --git a/packages/coding-agent/src/plan-mode/approved-plan.ts b/packages/coding-agent/src/plan-mode/approved-plan.ts index 27f1a3a8f..1e0d3388d 100644 --- a/packages/coding-agent/src/plan-mode/approved-plan.ts +++ b/packages/coding-agent/src/plan-mode/approved-plan.ts @@ -37,6 +37,15 @@ export function normalizePlanTitle(title: string): { title: string; fileName: st return { title: normalizedTitle, fileName: withExtension }; } +/** Humanize a normalized plan title for use as a session display name. + * Replaces `-`/`_` separators with spaces and capitalizes the first letter. + * Returns an empty string when the input collapses to whitespace. */ +export function humanizePlanTitle(title: string): string { + const spaced = title.replace(/[-_]+/g, " ").trim(); + if (!spaced) return ""; + return spaced.charAt(0).toUpperCase() + spaced.slice(1); +} + interface RenameApprovedPlanFileOptions { planFilePath: string; finalPlanFilePath: string; diff --git a/packages/coding-agent/test/plan-mode/approved-plan.test.ts b/packages/coding-agent/test/plan-mode/approved-plan.test.ts index 16772d7a7..327c02e22 100644 --- a/packages/coding-agent/test/plan-mode/approved-plan.test.ts +++ b/packages/coding-agent/test/plan-mode/approved-plan.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; -import { renameApprovedPlanFile } from "@oh-my-pi/pi-coding-agent/plan-mode/approved-plan"; +import { humanizePlanTitle, renameApprovedPlanFile } from "@oh-my-pi/pi-coding-agent/plan-mode/approved-plan"; describe("renameApprovedPlanFile", () => { let tmpDir: string; @@ -45,3 +45,20 @@ describe("renameApprovedPlanFile", () => { await expect(fs.stat(path.join(artifactsDir, "local", "PLAN.md"))).rejects.toThrow(); }); }); + +describe("humanizePlanTitle", () => { + it("replaces separators with spaces and capitalizes", () => { + expect(humanizePlanTitle("migrate-mcp-loader")).toBe("Migrate mcp loader"); + expect(humanizePlanTitle("fix_session_naming")).toBe("Fix session naming"); + expect(humanizePlanTitle("RefactorRouter")).toBe("RefactorRouter"); + }); + + it("collapses runs of separators", () => { + expect(humanizePlanTitle("foo--bar__baz")).toBe("Foo bar baz"); + }); + + it("returns empty string for blank-ish input", () => { + expect(humanizePlanTitle("")).toBe(""); + expect(humanizePlanTitle("---")).toBe(""); + }); +});