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:
committed by
Peter Steinberger
parent
4fda10c508
commit
83d5e30027
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user