feat(coding-agent): add session naming via /name command and extension API

- Add SessionInfoEntry type for session metadata
- Add /name <name> command to set session display name
- Add pi.setSessionName() and pi.getSessionName() extension API
- Session selector shows name (in warning color) instead of first message when set
- Session name included in fuzzy search
- /session command displays name when set

closes #650
This commit is contained in:
Mario Zechner 2026-01-12 16:56:39 +01:00
parent 7a41975e9e
commit 8f95a13e07
14 changed files with 173 additions and 5 deletions

View file

@ -106,6 +106,12 @@ export interface LabelEntry extends SessionEntryBase {
label: string | undefined;
}
/** Session metadata entry (e.g., user-defined display name). */
export interface SessionInfoEntry extends SessionEntryBase {
type: "session_info";
name?: string;
}
/**
* Custom message entry for extensions to inject messages into LLM context.
* Use customType to identify your extension's entries.
@ -135,7 +141,8 @@ export type SessionEntry =
| BranchSummaryEntry
| CustomEntry
| CustomMessageEntry
| LabelEntry;
| LabelEntry
| SessionInfoEntry;
/** Raw file entry (includes header) */
export type FileEntry = SessionHeader | SessionEntry;
@ -159,6 +166,8 @@ export interface SessionInfo {
id: string;
/** Working directory where the session was started. Empty string for old sessions. */
cwd: string;
/** User-defined display name from session_info entries. */
name?: string;
created: Date;
modified: Date;
messageCount: number;
@ -180,6 +189,7 @@ export type ReadonlySessionManager = Pick<
| "getHeader"
| "getEntries"
| "getTree"
| "getSessionName"
>;
/** Generate a unique short ID (8 hex chars, collision-checked) */
@ -511,8 +521,17 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
let messageCount = 0;
let firstMessage = "";
const allMessages: string[] = [];
let name: string | undefined;
for (const entry of entries) {
// Extract session name (use latest)
if (entry.type === "session_info") {
const infoEntry = entry as SessionInfoEntry;
if (infoEntry.name) {
name = infoEntry.name.trim();
}
}
if (entry.type !== "message") continue;
messageCount++;
@ -535,6 +554,7 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
path: filePath,
id: (header as SessionHeader).id,
cwd,
name,
created: new Date((header as SessionHeader).timestamp),
modified: stats.mtime,
messageCount,
@ -815,6 +835,32 @@ export class SessionManager {
return entry.id;
}
/** Append a session info entry (e.g., display name). Returns entry id. */
appendSessionInfo(name: string): string {
const entry: SessionInfoEntry = {
type: "session_info",
id: generateId(this.byId),
parentId: this.leafId,
timestamp: new Date().toISOString(),
name: name.trim(),
};
this._appendEntry(entry);
return entry.id;
}
/** Get the current session name from the latest session_info entry, if any. */
getSessionName(): string | undefined {
// Walk entries in reverse to find the latest session_info with a name
const entries = this.getEntries();
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type === "session_info" && entry.name) {
return entry.name;
}
}
return undefined;
}
/**
* Append a custom message entry (for extensions) that participates in LLM context.
* @param customType Extension identifier for filtering on reload