mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 07:04:25 +00:00
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:
parent
7a41975e9e
commit
8f95a13e07
14 changed files with 173 additions and 5 deletions
|
|
@ -86,6 +86,8 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
|||
sendMessage: notInitialized,
|
||||
sendUserMessage: notInitialized,
|
||||
appendEntry: notInitialized,
|
||||
setSessionName: notInitialized,
|
||||
getSessionName: notInitialized,
|
||||
getActiveTools: notInitialized,
|
||||
getAllTools: notInitialized,
|
||||
setActiveTools: notInitialized,
|
||||
|
|
@ -169,6 +171,14 @@ function createExtensionAPI(
|
|||
runtime.appendEntry(customType, data);
|
||||
},
|
||||
|
||||
setSessionName(name: string): void {
|
||||
runtime.setSessionName(name);
|
||||
},
|
||||
|
||||
getSessionName(): string | undefined {
|
||||
return runtime.getSessionName();
|
||||
},
|
||||
|
||||
exec(command: string, args: string[], options?: ExecOptions) {
|
||||
return execCommand(command, args, options?.cwd ?? cwd, options);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ export class ExtensionRunner {
|
|||
this.runtime.sendMessage = actions.sendMessage;
|
||||
this.runtime.sendUserMessage = actions.sendUserMessage;
|
||||
this.runtime.appendEntry = actions.appendEntry;
|
||||
this.runtime.setSessionName = actions.setSessionName;
|
||||
this.runtime.getSessionName = actions.getSessionName;
|
||||
this.runtime.getActiveTools = actions.getActiveTools;
|
||||
this.runtime.getAllTools = actions.getAllTools;
|
||||
this.runtime.setActiveTools = actions.setActiveTools;
|
||||
|
|
|
|||
|
|
@ -727,6 +727,16 @@ export interface ExtensionAPI {
|
|||
/** Append a custom entry to the session for state persistence (not sent to LLM). */
|
||||
appendEntry<T = unknown>(customType: string, data?: T): void;
|
||||
|
||||
// =========================================================================
|
||||
// Session Metadata
|
||||
// =========================================================================
|
||||
|
||||
/** Set the session display name (shown in session selector). */
|
||||
setSessionName(name: string): void;
|
||||
|
||||
/** Get the current session name, if set. */
|
||||
getSessionName(): string | undefined;
|
||||
|
||||
/** Execute a shell command. */
|
||||
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
||||
|
||||
|
|
@ -797,6 +807,10 @@ export type SendUserMessageHandler = (
|
|||
|
||||
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
|
||||
|
||||
export type SetSessionNameHandler = (name: string) => void;
|
||||
|
||||
export type GetSessionNameHandler = () => string | undefined;
|
||||
|
||||
export type GetActiveToolsHandler = () => string[];
|
||||
|
||||
export type GetAllToolsHandler = () => string[];
|
||||
|
|
@ -825,6 +839,8 @@ export interface ExtensionActions {
|
|||
sendMessage: SendMessageHandler;
|
||||
sendUserMessage: SendUserMessageHandler;
|
||||
appendEntry: AppendEntryHandler;
|
||||
setSessionName: SetSessionNameHandler;
|
||||
getSessionName: GetSessionNameHandler;
|
||||
getActiveTools: GetActiveToolsHandler;
|
||||
getAllTools: GetAllToolsHandler;
|
||||
setActiveTools: SetActiveToolsHandler;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ export {
|
|||
type SessionEntryBase,
|
||||
type SessionHeader,
|
||||
type SessionInfo,
|
||||
type SessionInfoEntry,
|
||||
SessionManager,
|
||||
type SessionMessageEntry,
|
||||
type ThinkingLevelChangeEntry,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|||
appendEntry: (customType, data) => {
|
||||
session.sessionManager.appendCustomEntry(customType, data);
|
||||
},
|
||||
setSessionName: (name) => {
|
||||
session.sessionManager.appendSessionInfo(name);
|
||||
},
|
||||
getSessionName: () => {
|
||||
return session.sessionManager.getSessionName();
|
||||
},
|
||||
getActiveTools: () => session.getActiveToolNames(),
|
||||
getAllTools: () => session.getAllToolNames(),
|
||||
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
||||
|
|
|
|||
|
|
@ -267,6 +267,12 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
appendEntry: (customType, data) => {
|
||||
session.sessionManager.appendCustomEntry(customType, data);
|
||||
},
|
||||
setSessionName: (name) => {
|
||||
session.sessionManager.appendSessionInfo(name);
|
||||
},
|
||||
getSessionName: () => {
|
||||
return session.sessionManager.getSessionName();
|
||||
},
|
||||
getActiveTools: () => session.getActiveToolNames(),
|
||||
getAllTools: () => session.getAllToolNames(),
|
||||
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue