fix(coding-agent): auto-name approved plan sessions

This commit is contained in:
can1357
2026-05-16 20:53:11 +02:00
parent c3f5a60c22
commit 4f6e70f779
3 changed files with 49 additions and 2 deletions
@@ -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",
});
@@ -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;
@@ -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("");
});
});