Fix automatic session title generation

This commit is contained in:
Federico Jaramillo Martinez
2026-05-08 15:06:04 +02:00
parent 99f434a259
commit b328c816f9
3 changed files with 40 additions and 7 deletions
+8 -4
View File
@@ -15,7 +15,7 @@ import { BUILTIN_COMMANDS } from "./builtinCommands.js";
import { SessionCommandService } from "./sessionCommandService.js"; import { SessionCommandService } from "./sessionCommandService.js";
import { SessionArchiveStore } from "./sessionArchiveStore.js"; import { SessionArchiveStore } from "./sessionArchiveStore.js";
import type { ActiveSession } from "./sessionRuntimeStore.js"; import type { ActiveSession } from "./sessionRuntimeStore.js";
import { generateShortSessionName } from "./sessionNameGenerator.js"; import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
function noop(): void { function noop(): void {
// Intentionally empty default unsubscribe callback. // Intentionally empty default unsubscribe callback.
@@ -263,12 +263,16 @@ export class PiSessionService {
if (model === undefined) return; if (model === undefined) return;
void generateShortSessionName(this.modelRegistry, model, firstMessage).then((name) => { void generateShortSessionName(this.modelRegistry, model, firstMessage).then((name) => {
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
}).catch(() => {
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
});
}
private applyGeneratedSessionName(session: AgentSession, name: string | undefined): void {
if (name === undefined || session.sessionName !== undefined) return; if (name === undefined || session.sessionName !== undefined) return;
session.setSessionName(name); session.setSessionName(name);
this.publishSessionName(session); this.publishSessionName(session);
}).catch(() => {
// Session naming is best-effort and must not affect prompt handling.
});
} }
private publishSessionName(session: AgentSession): void { private publishSessionName(session: AgentSession): void {
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { cleanSessionName, fallbackSessionName } from "./sessionNameGenerator.js";
describe("sessionNameGenerator", () => {
it("cleans model-generated titles", () => {
expect(cleanSessionName('Title: "Fix Session Naming."\nextra')).toBe("Fix Session Naming");
});
it("builds a concise fallback from the first request", () => {
expect(fallbackSessionName("Seems like auto name for sessions is not working, I still get the first message as a name."))
.toBe("Seems like auto name for sessions");
});
it("ignores skill blocks in fallback names", () => {
expect(fallbackSessionName('<skill name="x" location="/x">\nDo x\n</skill>\n\nCheck the UI now'))
.toBe("Check the UI now");
});
});
+13 -2
View File
@@ -4,6 +4,7 @@ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
const SESSION_NAME_TIMEOUT_MS = 10_000; const SESSION_NAME_TIMEOUT_MS = 10_000;
const SESSION_NAME_MAX_INPUT_CHARS = 4_000; const SESSION_NAME_MAX_INPUT_CHARS = 4_000;
const SESSION_NAME_MAX_LENGTH = 60; const SESSION_NAME_MAX_LENGTH = 60;
const FALLBACK_SESSION_NAME_MAX_WORDS = 6;
export async function generateShortSessionName<TApi extends Api>(modelRegistry: ModelRegistry, model: Model<TApi>, firstMessage: string): Promise<string | undefined> { export async function generateShortSessionName<TApi extends Api>(modelRegistry: ModelRegistry, model: Model<TApi>, firstMessage: string): Promise<string | undefined> {
const provider = getApiProvider(model.api); const provider = getApiProvider(model.api);
@@ -23,7 +24,6 @@ export async function generateShortSessionName<TApi extends Api>(modelRegistry:
}], }],
}, },
{ {
temperature: 0.2,
maxTokens: 24, maxTokens: 24,
reasoning: "minimal", reasoning: "minimal",
signal: AbortSignal.timeout(SESSION_NAME_TIMEOUT_MS), signal: AbortSignal.timeout(SESSION_NAME_TIMEOUT_MS),
@@ -43,10 +43,21 @@ export async function generateShortSessionName<TApi extends Api>(modelRegistry:
return cleanSessionName(finalMessage === undefined ? streamedText : textFromAssistant(finalMessage)); return cleanSessionName(finalMessage === undefined ? streamedText : textFromAssistant(finalMessage));
} }
export function fallbackSessionName(firstMessage: string): string | undefined {
return cleanSessionName(firstMessage
.replace(/<skill name="[^"]+" location="[^"]+">[\s\S]*?<\/skill>/g, "")
.replace(/```[\s\S]*?```/g, " ")
.replace(/[`*_#[\](){}<>]/g, " ")
.split(/\s+/)
.filter(Boolean)
.slice(0, FALLBACK_SESSION_NAME_MAX_WORDS)
.join(" "));
}
export function cleanSessionName(value: string): string | undefined { export function cleanSessionName(value: string): string | undefined {
const title = (value.split("\n", 1)[0] ?? "") const title = (value.split("\n", 1)[0] ?? "")
.replace(/^\s*(title|session title)\s*:\s*/i, "")
.replace(/^\s*["'`]+|["'`.]+\s*$/g, "") .replace(/^\s*["'`]+|["'`.]+\s*$/g, "")
.replace(/^(title|session title)\s*:\s*/i, "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim() .trim()
.slice(0, SESSION_NAME_MAX_LENGTH) .slice(0, SESSION_NAME_MAX_LENGTH)