From b328c816f9f56e09ef4ca61518d3fd624a00a385 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Fri, 8 May 2026 15:06:04 +0200 Subject: [PATCH] Fix automatic session title generation --- src/server/sessions/piSessionService.ts | 14 +++++++++----- .../sessions/sessionNameGenerator.test.ts | 18 ++++++++++++++++++ src/server/sessions/sessionNameGenerator.ts | 15 +++++++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 src/server/sessions/sessionNameGenerator.test.ts diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index c8a4003..ecf10cc 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -15,7 +15,7 @@ import { BUILTIN_COMMANDS } from "./builtinCommands.js"; import { SessionCommandService } from "./sessionCommandService.js"; import { SessionArchiveStore } from "./sessionArchiveStore.js"; import type { ActiveSession } from "./sessionRuntimeStore.js"; -import { generateShortSessionName } from "./sessionNameGenerator.js"; +import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js"; function noop(): void { // Intentionally empty default unsubscribe callback. @@ -263,14 +263,18 @@ export class PiSessionService { if (model === undefined) return; void generateShortSessionName(this.modelRegistry, model, firstMessage).then((name) => { - if (name === undefined || session.sessionName !== undefined) return; - session.setSessionName(name); - this.publishSessionName(session); + this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage)); }).catch(() => { - // Session naming is best-effort and must not affect prompt handling. + this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage)); }); } + private applyGeneratedSessionName(session: AgentSession, name: string | undefined): void { + if (name === undefined || session.sessionName !== undefined) return; + session.setSessionName(name); + this.publishSessionName(session); + } + private publishSessionName(session: AgentSession): void { const event = session.sessionName === undefined ? { type: "session.name", sessionId: session.sessionId } as const diff --git a/src/server/sessions/sessionNameGenerator.test.ts b/src/server/sessions/sessionNameGenerator.test.ts new file mode 100644 index 0000000..be26d68 --- /dev/null +++ b/src/server/sessions/sessionNameGenerator.test.ts @@ -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('\nDo x\n\n\nCheck the UI now')) + .toBe("Check the UI now"); + }); +}); diff --git a/src/server/sessions/sessionNameGenerator.ts b/src/server/sessions/sessionNameGenerator.ts index e81d56f..0c5ed07 100644 --- a/src/server/sessions/sessionNameGenerator.ts +++ b/src/server/sessions/sessionNameGenerator.ts @@ -4,6 +4,7 @@ import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; const SESSION_NAME_TIMEOUT_MS = 10_000; const SESSION_NAME_MAX_INPUT_CHARS = 4_000; const SESSION_NAME_MAX_LENGTH = 60; +const FALLBACK_SESSION_NAME_MAX_WORDS = 6; export async function generateShortSessionName(modelRegistry: ModelRegistry, model: Model, firstMessage: string): Promise { const provider = getApiProvider(model.api); @@ -23,7 +24,6 @@ export async function generateShortSessionName(modelRegistry: }], }, { - temperature: 0.2, maxTokens: 24, reasoning: "minimal", signal: AbortSignal.timeout(SESSION_NAME_TIMEOUT_MS), @@ -43,10 +43,21 @@ export async function generateShortSessionName(modelRegistry: return cleanSessionName(finalMessage === undefined ? streamedText : textFromAssistant(finalMessage)); } +export function fallbackSessionName(firstMessage: string): string | undefined { + return cleanSessionName(firstMessage + .replace(/[\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 { const title = (value.split("\n", 1)[0] ?? "") + .replace(/^\s*(title|session title)\s*:\s*/i, "") .replace(/^\s*["'`]+|["'`.]+\s*$/g, "") - .replace(/^(title|session title)\s*:\s*/i, "") .replace(/\s+/g, " ") .trim() .slice(0, SESSION_NAME_MAX_LENGTH)