diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 31c451fc..fb0ff0c9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -10,9 +10,13 @@ - AgentSession: `branch()` → `fork()`, `getUserMessagesForBranching()` → `getUserMessagesForForking()` - Extension events: `session_before_branch` → `session_before_fork`, `session_branch` → `session_fork` - Settings: `doubleEscapeAction: "branch" | "tree"` → `"fork" | "tree"` +- `SessionManager.list()` and `SessionManager.listAll()` are now async, returning `Promise`. Callers must await them. ([#620](https://github.com/badlogic/pi-mono/pull/620) by [@tmustier](https://github.com/tmustier)) ### Added - +- `/resume` selector now toggles between current-folder and all sessions with Tab, showing the session cwd in the All view and loading progress. ([#620](https://github.com/badlogic/pi-mono/pull/620) by [@tmustier](https://github.com/tmustier)) +- `SessionManager.list()` and `SessionManager.listAll()` accept optional `onProgress` callback for progress updates +- `SessionInfo.cwd` field containing the session's working directory (empty string for old sessions) +- `SessionListProgress` type export for progress callbacks - `/models` command to enable/disable models for Ctrl+P cycling. Changes persist to `enabledModels` in settings.json and take effect immediately. ([#626](https://github.com/badlogic/pi-mono/pull/626) by [@CarlosGtrz](https://github.com/CarlosGtrz)) - `model_select` extension hook fires when model changes via `/model`, model cycling, or session restore with `source` field and `previousModel` ([#628](https://github.com/badlogic/pi-mono/pull/628) by [@marckrenn](https://github.com/marckrenn)) - `ctx.ui.setWorkingMessage()` extension API to customize the "Working..." message during streaming ([#625](https://github.com/badlogic/pi-mono/pull/625) by [@nicobailon](https://github.com/nicobailon)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 2ad84712..f94ae4a9 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -459,7 +459,7 @@ Sessions auto-save to `~/.pi/agent/sessions/` organized by working directory. pi --continue # Continue most recent session pi -c # Short form -pi --resume # Browse and select from past sessions +pi --resume # Browse and select from past sessions (Tab to toggle Current Folder / All) pi -r # Short form pi --no-session # Ephemeral mode (don't save) diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index ae974dad..2cfb00b2 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -636,12 +636,17 @@ const { session } = await createAgentSession({ sessionManager: SessionManager.open("/path/to/session.jsonl"), }); -// List available sessions -const sessions = SessionManager.list(process.cwd()); +// List available sessions (async with optional progress callback) +const sessions = await SessionManager.list(process.cwd()); for (const info of sessions) { - console.log(`${info.id}: ${info.firstMessage} (${info.messageCount} messages)`); + console.log(`${info.id}: ${info.firstMessage} (${info.messageCount} messages, cwd: ${info.cwd})`); } +// List all sessions across all projects +const allSessions = await SessionManager.listAll((loaded, total) => { + console.log(`Loading ${loaded}/${total}...`); +}); + // Custom session directory (no cwd encoding) const customDir = "/path/to/my-sessions"; const { session } = await createAgentSession({ diff --git a/packages/coding-agent/examples/sdk/11-sessions.ts b/packages/coding-agent/examples/sdk/11-sessions.ts index f1bbd047..9c32016d 100644 --- a/packages/coding-agent/examples/sdk/11-sessions.ts +++ b/packages/coding-agent/examples/sdk/11-sessions.ts @@ -26,7 +26,7 @@ if (modelFallbackMessage) console.log("Note:", modelFallbackMessage); console.log("Continued session:", continued.sessionFile); // List and open specific session -const sessions = SessionManager.list(process.cwd()); +const sessions = await SessionManager.list(process.cwd()); console.log(`\nFound ${sessions.length} sessions:`); for (const info of sessions.slice(0, 3)) { console.log(` ${info.id.slice(0, 8)}... - "${info.firstMessage.slice(0, 30)}..."`); diff --git a/packages/coding-agent/src/cli/session-picker.ts b/packages/coding-agent/src/cli/session-picker.ts index f1ff837b..3ca22355 100644 --- a/packages/coding-agent/src/cli/session-picker.ts +++ b/packages/coding-agent/src/cli/session-picker.ts @@ -3,17 +3,23 @@ */ import { ProcessTerminal, TUI } from "@mariozechner/pi-tui"; -import type { SessionInfo } from "../core/session-manager.js"; +import type { SessionInfo, SessionListProgress } from "../core/session-manager.js"; import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js"; +type SessionsLoader = (onProgress?: SessionListProgress) => Promise; + /** Show TUI session selector and return selected session path or null if cancelled */ -export async function selectSession(sessions: SessionInfo[]): Promise { +export async function selectSession( + currentSessionsLoader: SessionsLoader, + allSessionsLoader: SessionsLoader, +): Promise { return new Promise((resolve) => { const ui = new TUI(new ProcessTerminal()); let resolved = false; const selector = new SessionSelectorComponent( - sessions, + currentSessionsLoader, + allSessionsLoader, (path: string) => { if (!resolved) { resolved = true; @@ -32,6 +38,7 @@ export async function selectSession(sessions: SessionInfo[]): Promise ui.requestRender(), ); ui.addChild(selector); diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index c3f7e0b0..a35ff8c9 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -13,8 +13,9 @@ import { statSync, writeFileSync, } from "fs"; +import { readdir, readFile, stat } from "fs/promises"; import { join, resolve } from "path"; -import { getAgentDir as getDefaultAgentDir } from "../config.js"; +import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js"; import { type BashExecutionMessage, type CustomMessage, @@ -156,6 +157,8 @@ export interface SessionContext { export interface SessionInfo { path: string; id: string; + /** Working directory where the session was started. Empty string for old sessions. */ + cwd: string; created: Date; modified: Date; messageCount: number; @@ -470,6 +473,118 @@ export function findMostRecentSession(sessionDir: string): string | null { } } +function isMessageWithContent(message: AgentMessage): message is Message { + return typeof (message as Message).role === "string" && "content" in message; +} + +function extractTextContent(message: Message): string { + const content = message.content; + if (typeof content === "string") { + return content; + } + return content + .filter((block): block is TextContent => block.type === "text") + .map((block) => block.text) + .join(" "); +} + +async function buildSessionInfo(filePath: string): Promise { + try { + const content = await readFile(filePath, "utf8"); + const entries: FileEntry[] = []; + const lines = content.trim().split("\n"); + + for (const line of lines) { + if (!line.trim()) continue; + try { + entries.push(JSON.parse(line) as FileEntry); + } catch { + // Skip malformed lines + } + } + + if (entries.length === 0) return null; + const header = entries[0]; + if (header.type !== "session") return null; + + const stats = await stat(filePath); + let messageCount = 0; + let firstMessage = ""; + const allMessages: string[] = []; + + for (const entry of entries) { + if (entry.type !== "message") continue; + messageCount++; + + const message = (entry as SessionMessageEntry).message; + if (!isMessageWithContent(message)) continue; + if (message.role !== "user" && message.role !== "assistant") continue; + + const textContent = extractTextContent(message); + if (!textContent) continue; + + allMessages.push(textContent); + if (!firstMessage && message.role === "user") { + firstMessage = textContent; + } + } + + const cwd = typeof (header as SessionHeader).cwd === "string" ? (header as SessionHeader).cwd : ""; + + return { + path: filePath, + id: (header as SessionHeader).id, + cwd, + created: new Date((header as SessionHeader).timestamp), + modified: stats.mtime, + messageCount, + firstMessage: firstMessage || "(no messages)", + allMessagesText: allMessages.join(" "), + }; + } catch { + return null; + } +} + +export type SessionListProgress = (loaded: number, total: number) => void; + +async function listSessionsFromDir( + dir: string, + onProgress?: SessionListProgress, + progressOffset = 0, + progressTotal?: number, +): Promise { + const sessions: SessionInfo[] = []; + if (!existsSync(dir)) { + return sessions; + } + + try { + const dirEntries = await readdir(dir); + const files = dirEntries.filter((f) => f.endsWith(".jsonl")).map((f) => join(dir, f)); + const total = progressTotal ?? files.length; + + let loaded = 0; + const results = await Promise.all( + files.map(async (file) => { + const info = await buildSessionInfo(file); + loaded++; + onProgress?.(progressOffset + loaded, total); + return info; + }), + ); + for (const info of results) { + if (info) { + sessions.push(info); + } + } + } catch { + // Return empty list on error + } + + return sessions; +} + /** * Manages conversation sessions as append-only trees stored in JSONL files. * @@ -1057,88 +1172,69 @@ export class SessionManager { } /** - * List all sessions. + * List all sessions for a directory. * @param cwd Working directory (used to compute default session directory) * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions//). + * @param onProgress Optional callback for progress updates (loaded, total) */ - static list(cwd: string, sessionDir?: string): SessionInfo[] { + static async list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise { const dir = sessionDir ?? getDefaultSessionDir(cwd); - const sessions: SessionInfo[] = []; + const sessions = await listSessionsFromDir(dir, onProgress); + sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); + return sessions; + } + + /** + * List all sessions across all project directories. + * @param onProgress Optional callback for progress updates (loaded, total) + */ + static async listAll(onProgress?: SessionListProgress): Promise { + const sessionsDir = getSessionsDir(); try { - const files = readdirSync(dir) - .filter((f) => f.endsWith(".jsonl")) - .map((f) => join(dir, f)); + if (!existsSync(sessionsDir)) { + return []; + } + const entries = await readdir(sessionsDir, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()).map((e) => join(sessionsDir, e.name)); - for (const file of files) { + // Count total files first for accurate progress + let totalFiles = 0; + const dirFiles: string[][] = []; + for (const dir of dirs) { try { - const content = readFileSync(file, "utf8"); - const lines = content.trim().split("\n"); - if (lines.length === 0) continue; - - // Check first line for valid session header - let header: { type: string; id: string; timestamp: string } | null = null; - try { - const first = JSON.parse(lines[0]); - if (first.type === "session" && first.id) { - header = first; - } - } catch { - // Not valid JSON - } - if (!header) continue; - - const stats = statSync(file); - let messageCount = 0; - let firstMessage = ""; - const allMessages: string[] = []; - - for (let i = 1; i < lines.length; i++) { - try { - const entry = JSON.parse(lines[i]); - - if (entry.type === "message") { - messageCount++; - - if (entry.message.role === "user" || entry.message.role === "assistant") { - const textContent = entry.message.content - .filter((c: any) => c.type === "text") - .map((c: any) => c.text) - .join(" "); - - if (textContent) { - allMessages.push(textContent); - - if (!firstMessage && entry.message.role === "user") { - firstMessage = textContent; - } - } - } - } - } catch { - // Skip malformed lines - } - } - - sessions.push({ - path: file, - id: header.id, - created: new Date(header.timestamp), - modified: stats.mtime, - messageCount, - firstMessage: firstMessage || "(no messages)", - allMessagesText: allMessages.join(" "), - }); + const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")); + dirFiles.push(files.map((f) => join(dir, f))); + totalFiles += files.length; } catch { - // Skip files that can't be read + dirFiles.push([]); + } + } + + // Process all files with progress tracking + let loaded = 0; + const sessions: SessionInfo[] = []; + const allFiles = dirFiles.flat(); + + const results = await Promise.all( + allFiles.map(async (file) => { + const info = await buildSessionInfo(file); + loaded++; + onProgress?.(loaded, totalFiles); + return info; + }), + ); + + for (const info of results) { + if (info) { + sessions.push(info); } } sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); + return sessions; } catch { - // Return empty list on error + return []; } - - return sessions; } } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 5dee215b..b50a7e2b 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -61,14 +61,14 @@ async function prepareInitialMessage( * Resolve a session argument to a file path. * If it looks like a path, use as-is. Otherwise try to match as session ID prefix. */ -function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): string { +async function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise { // If it looks like a file path, use as-is if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) { return sessionArg; } // Try to match as session ID (full or partial UUID) - const sessions = SessionManager.list(cwd, sessionDir); + const sessions = await SessionManager.list(cwd, sessionDir); const matches = sessions.filter((s) => s.id.startsWith(sessionArg)); if (matches.length >= 1) { @@ -79,12 +79,12 @@ function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string return sessionArg; } -function createSessionManager(parsed: Args, cwd: string): SessionManager | undefined { +async function createSessionManager(parsed: Args, cwd: string): Promise { if (parsed.noSession) { return SessionManager.inMemory(); } if (parsed.session) { - const resolvedPath = resolveSessionPath(parsed.session, cwd, parsed.sessionDir); + const resolvedPath = await resolveSessionPath(parsed.session, cwd, parsed.sessionDir); return SessionManager.open(resolvedPath, parsed.sessionDir); } if (parsed.continue) { @@ -314,7 +314,7 @@ export async function main(args: string[]) { } // Create session manager based on CLI flags - let sessionManager = createSessionManager(parsed, cwd); + let sessionManager = await createSessionManager(parsed, cwd); time("createSessionManager"); // Handle --resume: show session picker @@ -322,13 +322,10 @@ export async function main(args: string[]) { // Initialize keybindings so session picker respects user config KeybindingsManager.create(); - const sessions = SessionManager.list(cwd, parsed.sessionDir); - time("SessionManager.list"); - if (sessions.length === 0) { - console.log(chalk.dim("No sessions found")); - return; - } - const selectedPath = await selectSession(sessions); + const selectedPath = await selectSession( + (onProgress) => SessionManager.list(cwd, parsed.sessionDir, onProgress), + SessionManager.listAll, + ); time("selectSession"); if (!selectedPath) { console.log(chalk.dim("No session selected")); diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index 343c7379..747c0b4a 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -1,3 +1,4 @@ +import * as os from "node:os"; import { type Component, Container, @@ -5,13 +6,88 @@ import { getEditorKeybindings, Input, Spacer, - Text, truncateToWidth, + visibleWidth, } from "@mariozechner/pi-tui"; -import type { SessionInfo } from "../../../core/session-manager.js"; +import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; +type SessionScope = "current" | "all"; + +function shortenPath(path: string): string { + const home = os.homedir(); + if (!path) return path; + if (path.startsWith(home)) { + return `~${path.slice(home.length)}`; + } + return path; +} + +function formatSessionDate(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(); +} + +class SessionSelectorHeader implements Component { + private scope: SessionScope; + private loading = false; + private loadProgress: { loaded: number; total: number } | null = null; + + constructor(scope: SessionScope) { + this.scope = scope; + } + + setScope(scope: SessionScope): void { + this.scope = scope; + } + + setLoading(loading: boolean): void { + this.loading = loading; + if (!loading) { + this.loadProgress = null; + } + } + + setProgress(loaded: number, total: number): void { + this.loadProgress = { loaded, total }; + } + + invalidate(): void {} + + render(width: number): string[] { + const title = this.scope === "current" ? "Resume Session (Current Folder)" : "Resume Session (All)"; + const leftText = theme.bold(title); + let scopeText: string; + if (this.loading) { + const progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : "..."; + scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", `Loading ${progressText}`)}`; + } else { + scopeText = + this.scope === "current" + ? `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}` + : `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`; + } + const rightText = truncateToWidth(scopeText, width, ""); + const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1); + const left = truncateToWidth(leftText, availableLeft, ""); + const spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText)); + const hint = theme.fg("muted", "Tab to toggle scope"); + return [`${left}${" ".repeat(spacing)}${rightText}`, hint]; + } +} + /** * Custom session list component with multi-line items and search */ @@ -20,15 +96,18 @@ class SessionList implements Component { private filteredSessions: SessionInfo[] = []; private selectedIndex: number = 0; private searchInput: Input; + private showCwd = false; public onSelect?: (sessionPath: string) => void; public onCancel?: () => void; public onExit: () => void = () => {}; + public onToggleScope?: () => void; private maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank) - constructor(sessions: SessionInfo[]) { + constructor(sessions: SessionInfo[], showCwd: boolean) { this.allSessions = sessions; this.filteredSessions = sessions; this.searchInput = new Input(); + this.showCwd = showCwd; // Handle Enter in search input - select current item this.searchInput.onSubmit = () => { @@ -41,18 +120,22 @@ class SessionList implements Component { }; } + setSessions(sessions: SessionInfo[], showCwd: boolean): void { + this.allSessions = sessions; + this.showCwd = showCwd; + this.filterSessions(this.searchInput.getValue()); + } + private filterSessions(query: string): void { this.filteredSessions = fuzzyFilter( this.allSessions, query, - (session) => `${session.id} ${session.allMessagesText}`, + (session) => `${session.id} ${session.allMessagesText} ${session.cwd}`, ); this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1)); } - invalidate(): void { - // No cached state to invalidate currently - } + invalidate(): void {} render(width: number): string[] { const lines: string[] = []; @@ -66,23 +149,6 @@ class SessionList implements Component { 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(); - }; - // Calculate visible range with scrolling const startIndex = Math.max( 0, @@ -105,9 +171,13 @@ class SessionList implements Component { const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg); // Second line: metadata (dimmed) - also truncate for safety - const modified = formatDate(session.modified); + const modified = formatSessionDate(session.modified); const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`; - const metadata = ` ${modified} · ${msgCount}`; + const metadataParts = [modified, msgCount]; + if (this.showCwd && session.cwd) { + metadataParts.push(shortenPath(session.cwd)); + } + const metadata = ` ${metadataParts.join(" · ")}`; const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, "")); lines.push(messageLine); @@ -127,6 +197,12 @@ class SessionList implements Component { handleInput(keyData: string): void { const kb = getEditorKeybindings(); + if (kb.matches(keyData, "tab")) { + if (this.onToggleScope) { + this.onToggleScope(); + } + return; + } // Up arrow if (kb.matches(keyData, "selectUp")) { this.selectedIndex = Math.max(0, this.selectedIndex - 1); @@ -156,32 +232,50 @@ class SessionList implements Component { } } +type SessionsLoader = (onProgress?: SessionListProgress) => Promise; + /** * Component that renders a session selector */ export class SessionSelectorComponent extends Container { private sessionList: SessionList; + private header: SessionSelectorHeader; + private scope: SessionScope = "current"; + private currentSessions: SessionInfo[] | null = null; + private allSessions: SessionInfo[] | null = null; + private currentSessionsLoader: SessionsLoader; + private allSessionsLoader: SessionsLoader; + private onCancel: () => void; + private requestRender: () => void; constructor( - sessions: SessionInfo[], + currentSessionsLoader: SessionsLoader, + allSessionsLoader: SessionsLoader, onSelect: (sessionPath: string) => void, onCancel: () => void, onExit: () => void, + requestRender: () => void, ) { super(); + this.currentSessionsLoader = currentSessionsLoader; + this.allSessionsLoader = allSessionsLoader; + this.onCancel = onCancel; + this.requestRender = requestRender; + this.header = new SessionSelectorHeader(this.scope); // Add header this.addChild(new Spacer(1)); - this.addChild(new Text(theme.bold("Resume Session"), 1, 0)); + this.addChild(this.header); this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); - // Create session list - this.sessionList = new SessionList(sessions); + // Create session list (starts empty, will be populated after load) + this.sessionList = new SessionList([], false); this.sessionList.onSelect = onSelect; this.sessionList.onCancel = onCancel; this.sessionList.onExit = onExit; + this.sessionList.onToggleScope = () => this.toggleScope(); this.addChild(this.sessionList); @@ -189,9 +283,61 @@ export class SessionSelectorComponent extends Container { this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); - // Auto-cancel if no sessions - if (sessions.length === 0) { - setTimeout(() => onCancel(), 100); + // Start loading current sessions immediately + this.loadCurrentSessions(); + } + + private loadCurrentSessions(): void { + this.header.setLoading(true); + this.requestRender(); + this.currentSessionsLoader((loaded, total) => { + this.header.setProgress(loaded, total); + this.requestRender(); + }).then((sessions) => { + this.currentSessions = sessions; + this.header.setLoading(false); + this.sessionList.setSessions(sessions, false); + this.requestRender(); + // If no sessions found, cancel + if (sessions.length === 0) { + this.onCancel(); + } + }); + } + + private toggleScope(): void { + if (this.scope === "current") { + // Switching to "all" - load if not already loaded + if (this.allSessions === null) { + this.header.setLoading(true); + this.header.setScope("all"); + this.sessionList.setSessions([], true); // Clear list while loading + this.requestRender(); + // Load asynchronously with progress updates + this.allSessionsLoader((loaded, total) => { + this.header.setProgress(loaded, total); + this.requestRender(); + }).then((sessions) => { + this.allSessions = sessions; + this.header.setLoading(false); + this.scope = "all"; + this.sessionList.setSessions(this.allSessions, true); + this.requestRender(); + // If no sessions in All scope either, cancel + if (this.allSessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) { + this.onCancel(); + } + }); + } else { + this.scope = "all"; + this.sessionList.setSessions(this.allSessions, true); + this.header.setScope(this.scope); + } + } else { + // Switching back to "current" + this.scope = "current"; + this.sessionList.setSessions(this.currentSessions ?? [], false); + this.header.setScope(this.scope); } } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index fcfba4b7..e3f77bbc 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2876,9 +2876,10 @@ export class InteractiveMode { private showSessionSelector(): void { this.showSelector((done) => { - const sessions = SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir()); const selector = new SessionSelectorComponent( - sessions, + (onProgress) => + SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), + SessionManager.listAll, async (sessionPath) => { done(); await this.handleResumeSession(sessionPath); @@ -2890,6 +2891,7 @@ export class InteractiveMode { () => { void this.shutdown(); }, + () => this.ui.requestRender(), ); return { component: selector, focus: selector.getSessionList() }; });