diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 55744b85..f5ad2be2 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -123,27 +123,13 @@ Current directory: ${process.cwd()}`; async function selectSession(sessionManager: SessionManager): Promise { return new Promise((resolve) => { const ui = new TUI(new ProcessTerminal()); - let selectedPath: string | null = null; let resolved = false; - // Handle Ctrl+C - const handleSigint = () => { - if (!resolved) { - resolved = true; - ui.stop(); - process.exit(0); - } - }; - - process.on("SIGINT", handleSigint); - const selector = new SessionSelectorComponent( sessionManager, (path: string) => { if (!resolved) { resolved = true; - selectedPath = path; - process.removeListener("SIGINT", handleSigint); ui.stop(); resolve(path); } @@ -151,7 +137,6 @@ async function selectSession(sessionManager: SessionManager): Promise { if (!resolved) { resolved = true; - process.removeListener("SIGINT", handleSigint); ui.stop(); resolve(null); } @@ -159,7 +144,7 @@ async function selectSession(sessionManager: SessionManager): Promise void; + public onCancel?: () => void; + + constructor(sessions: SessionItem[]) { + this.sessions = sessions; + } + + render(width: number): string[] { + const lines: string[] = []; + + if (this.sessions.length === 0) { + lines.push(chalk.gray(" No sessions found")); + return lines; + } + + // Format dates + const formatDate = (date: Date): string => { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return "just now"; + if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`; + if (diffDays === 1) return "1 day ago"; + if (diffDays < 7) return `${diffDays} days ago`; + + return date.toLocaleDateString(); + }; + + // Render each session (2 lines per session) + for (let i = 0; i < this.sessions.length; i++) { + const session = this.sessions[i]; + const isSelected = i === this.selectedIndex; + + // Normalize first message to single line + const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim(); + + // First line: cursor + message + const cursor = isSelected ? chalk.blue("› ") : " "; + const maxMsgWidth = width - 2; // Account for cursor + const truncatedMsg = normalizedMessage.substring(0, maxMsgWidth); + const messageLine = cursor + (isSelected ? chalk.bold(truncatedMsg) : truncatedMsg); + + // Second line: metadata (dimmed) + const modified = formatDate(session.modified); + const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`; + const metadata = ` ${modified} · ${msgCount}`; + const metadataLine = chalk.dim(metadata); + + lines.push(messageLine); + lines.push(metadataLine); + } + + return lines; + } + + handleInput(keyData: string): void { + // Up arrow + if (keyData === "\x1b[A") { + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + } + // Down arrow + else if (keyData === "\x1b[B") { + this.selectedIndex = Math.min(this.sessions.length - 1, this.selectedIndex + 1); + } + // Enter + else if (keyData === "\r") { + const selected = this.sessions[this.selectedIndex]; + if (selected && this.onSelect) { + this.onSelect(selected.path); + } + } + // Escape or Ctrl+C + else if (keyData === "\x1b" || keyData === "\x03") { + if (this.onCancel) { + this.onCancel(); + } + } + } +} + /** * Component that renders a session selector */ export class SessionSelectorComponent extends Container { - private selectList: SelectList; + private sessionList: SessionList; constructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) { super(); @@ -32,77 +122,31 @@ export class SessionSelectorComponent extends Container { // Load all sessions const sessions = sessionManager.loadAllSessions(); - if (sessions.length === 0) { - this.addChild(new DynamicBorder()); - this.addChild(new Text(chalk.gray(" No previous sessions found"), 0, 0)); - this.addChild(new DynamicBorder()); - this.selectList = new SelectList([], 0); - this.addChild(this.selectList); - - // Auto-cancel if no sessions - setTimeout(() => onCancel(), 100); - return; - } - - // Format sessions as select items - const items: SelectItem[] = sessions.map((session) => { - // Format dates - const formatDate = (date: Date): string => { - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) return "now"; - if (diffMins < 60) return `${diffMins}m`; - if (diffHours < 24) return `${diffHours}h`; - if (diffDays === 1) return "1d"; - if (diffDays < 7) return `${diffDays}d`; - - // Fallback to date string - return date.toLocaleDateString(); - }; - - // Normalize first message to single line - const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim(); - - // Build description with metadata (single line, compact format) - const modified = formatDate(session.modified); - const msgCount = `${session.messageCount}msg`; - - // Keep description compact: "modified • count" - const description = `${modified} • ${msgCount}`; - - return { - value: session.path, - label: normalizedMessage, // Let SelectList handle truncation based on actual width - description, - }; - }); - - // Add top border + // Add header + this.addChild(new Spacer(1)); + this.addChild(new Text(chalk.bold("Resume Session"), 1, 0)); + this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); - this.addChild(new Text(chalk.bold("Select a session to resume:"), 1, 0)); + this.addChild(new Spacer(1)); - // Create selector - this.selectList = new SelectList(items, Math.min(10, items.length)); + // Create session list + this.sessionList = new SessionList(sessions); + this.sessionList.onSelect = onSelect; + this.sessionList.onCancel = onCancel; - this.selectList.onSelect = (item) => { - onSelect(item.value); - }; - - this.selectList.onCancel = () => { - onCancel(); - }; - - this.addChild(this.selectList); + this.addChild(this.sessionList); // Add bottom border + this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); + + // Auto-cancel if no sessions + if (sessions.length === 0) { + setTimeout(() => onCancel(), 100); + } } - getSelectList(): SelectList { - return this.selectList; + getSessionList(): SessionList { + return this.sessionList; } } diff --git a/packages/tui/src/components/select-list.ts b/packages/tui/src/components/select-list.ts index 12950d44..43d946cd 100644 --- a/packages/tui/src/components/select-list.ts +++ b/packages/tui/src/components/select-list.ts @@ -148,8 +148,8 @@ export class SelectList implements Component { this.onSelect(selectedItem); } } - // Escape - else if (keyData === "\x1b") { + // Escape or Ctrl+C + else if (keyData === "\x1b" || keyData === "\x03") { if (this.onCancel) { this.onCancel(); }