feat: add heuristic session title derivation for session picker

Enable meaningful session titles via priority-based derivation:
1. displayName (user-set)
2. subject (group name)
3. First user message (truncated to 60 chars)
4. sessionId prefix + date fallback

Opt-in via includeDerivedTitles param to avoid perf impact on
regular listing. Reads only first 10 lines of transcript files.

Closes #1161
This commit is contained in:
CJ Winslow
2026-01-18 02:18:50 -08:00
committed by Peter Steinberger
parent 4fda10c508
commit 83d5e30027
6 changed files with 358 additions and 3 deletions

View File

@@ -22,6 +22,7 @@ import {
parseAgentSessionKey,
} from "../routing/session-key.js";
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
import { readFirstUserMessageFromTranscript } from "./session-utils.fs.js";
import type {
GatewayAgentRow,
GatewaySessionRow,
@@ -32,6 +33,7 @@ import type {
export {
archiveFileOnDisk,
capArrayByJsonBytes,
readFirstUserMessageFromTranscript,
readSessionMessages,
resolveSessionTranscriptCandidates,
} from "./session-utils.fs.js";
@@ -43,6 +45,51 @@ export type {
SessionsPatchResult,
} from "./session-utils.types.js";
const DERIVED_TITLE_MAX_LEN = 60;
function formatSessionIdPrefix(sessionId: string, updatedAt?: number | null): string {
const prefix = sessionId.slice(0, 8);
if (updatedAt && updatedAt > 0) {
const d = new Date(updatedAt);
const date = d.toISOString().slice(0, 10);
return `${prefix} (${date})`;
}
return prefix;
}
function truncateTitle(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
const cut = text.slice(0, maxLen - 1);
const lastSpace = cut.lastIndexOf(" ");
if (lastSpace > maxLen * 0.6) return cut.slice(0, lastSpace) + "…";
return cut + "…";
}
export function deriveSessionTitle(
entry: SessionEntry | undefined,
firstUserMessage?: string | null,
): string | undefined {
if (!entry) return undefined;
if (entry.displayName?.trim()) {
return entry.displayName.trim();
}
if (entry.subject?.trim()) {
return entry.subject.trim();
}
if (firstUserMessage?.trim()) {
return truncateTitle(firstUserMessage.trim(), DERIVED_TITLE_MAX_LEN);
}
if (entry.sessionId) {
return formatSessionIdPrefix(entry.sessionId, entry.updatedAt);
}
return undefined;
}
export function loadSessionEntry(sessionKey: string) {
const cfg = loadConfig();
const sessionCfg = cfg.session;
@@ -341,6 +388,7 @@ export function listSessionsFromStore(params: {
const includeGlobal = opts.includeGlobal === true;
const includeUnknown = opts.includeUnknown === true;
const includeDerivedTitles = opts.includeDerivedTitles === true;
const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : "";
const label = typeof opts.label === "string" ? opts.label.trim() : "";
const agentId = typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : "";
@@ -400,6 +448,7 @@ export function listSessionsFromStore(params: {
const deliveryFields = normalizeSessionDeliveryFields(entry);
return {
key,
entry,
kind: classifySessionKey(key, entry),
label: entry?.label,
displayName,
@@ -429,7 +478,7 @@ export function listSessionsFromStore(params: {
lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel,
lastTo: deliveryFields.lastTo ?? entry?.lastTo,
lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId,
} satisfies GatewaySessionRow;
};
})
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
@@ -443,11 +492,25 @@ export function listSessionsFromStore(params: {
sessions = sessions.slice(0, limit);
}
const finalSessions: GatewaySessionRow[] = sessions.map((s) => {
const { entry, ...rest } = s;
let derivedTitle: string | undefined;
if (includeDerivedTitles && entry?.sessionId) {
const firstUserMsg = readFirstUserMessageFromTranscript(
entry.sessionId,
storePath,
entry.sessionFile,
);
derivedTitle = deriveSessionTitle(entry, firstUserMsg);
}
return { ...rest, derivedTitle } satisfies GatewaySessionRow;
});
return {
ts: now,
path: storePath,
count: sessions.length,
count: finalSessions.length,
defaults: getSessionDefaults(cfg),
sessions,
sessions: finalSessions,
};
}