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

@ -130,7 +130,7 @@ class SessionList implements Component {
this.filteredSessions = fuzzyFilter(
this.allSessions,
query,
(session) => `${session.id} ${session.allMessagesText} ${session.cwd}`,
(session) => `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`,
);
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
}
@ -167,14 +167,24 @@ class SessionList implements Component {
const session = this.filteredSessions[i];
const isSelected = i === this.selectedIndex;
// Normalize first message to single line
const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
// Use session name if set, otherwise first message
const hasName = !!session.name;
const displayText = session.name ?? session.firstMessage;
const normalizedMessage = displayText.replace(/\n/g, " ").trim();
// First line: cursor + message (truncate to visible width)
// Use warning color for custom names to distinguish from first message
const cursor = isSelected ? theme.fg("accent", " ") : " ";
const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
let styledMsg = truncatedMsg;
if (hasName) {
styledMsg = theme.fg("warning", truncatedMsg);
}
if (isSelected) {
styledMsg = theme.bold(styledMsg);
}
const messageLine = cursor + styledMsg;
// Second line: metadata (dimmed) - also truncate for safety
const modified = formatSessionDate(session.modified);

View file

@ -286,6 +286,7 @@ export class InteractiveMode {
{ name: "export", description: "Export session to HTML file" },
{ name: "share", description: "Share session as a secret GitHub gist" },
{ name: "copy", description: "Copy last agent message to clipboard" },
{ name: "name", description: "Set session display name" },
{ name: "session", description: "Show session info and stats" },
{ name: "changelog", description: "Show changelog entries" },
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
@ -677,6 +678,12 @@ export class InteractiveMode {
appendEntry: (customType, data) => {
this.sessionManager.appendCustomEntry(customType, data);
},
setSessionName: (name) => {
this.sessionManager.appendSessionInfo(name);
},
getSessionName: () => {
return this.sessionManager.getSessionName();
},
getActiveTools: () => this.session.getActiveToolNames(),
getAllTools: () => this.session.getAllToolNames(),
setActiveTools: (toolNames) => this.session.setActiveToolsByName(toolNames),
@ -1442,6 +1449,11 @@ export class InteractiveMode {
this.editor.setText("");
return;
}
if (text === "/name" || text.startsWith("/name ")) {
this.handleNameCommand(text);
this.editor.setText("");
return;
}
if (text === "/session") {
this.handleSessionCommand();
this.editor.setText("");
@ -3258,10 +3270,34 @@ export class InteractiveMode {
}
}
private handleNameCommand(text: string): void {
const name = text.replace(/^\/name\s*/, "").trim();
if (!name) {
const currentName = this.sessionManager.getSessionName();
if (currentName) {
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name: ${currentName}`), 1, 0));
} else {
this.showWarning("Usage: /name <name>");
}
this.ui.requestRender();
return;
}
this.sessionManager.appendSessionInfo(name);
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
this.ui.requestRender();
}
private handleSessionCommand(): void {
const stats = this.session.getSessionStats();
const sessionName = this.sessionManager.getSessionName();
let info = `${theme.bold("Session Info")}\n\n`;
if (sessionName) {
info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
}
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
info += `${theme.bold("Messages")}\n`;