fix(coding-agent): hydrate resume search text lazily

This commit is contained in:
Vegard Stikbakke
2026-06-16 20:14:40 +02:00
Unverified
parent ce6055a297
commit 75fcecee40
5 changed files with 73 additions and 13 deletions
@@ -8,11 +8,13 @@ import type { SessionInfo, SessionListProgress } from "../core/session-manager.t
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts";
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
type SessionHydrator = (path: string) => Promise<SessionInfo | null>;
/** Show TUI session selector and return selected session path or null if cancelled */
export async function selectSession(
currentSessionsLoader: SessionsLoader,
allSessionsLoader: SessionsLoader,
hydrateSession?: SessionHydrator,
): Promise<string | null> {
return new Promise((resolve) => {
const ui = new TUI(new ProcessTerminal());
@@ -42,7 +44,7 @@ export async function selectSession(
process.exit(0);
},
() => ui.requestRender(),
{ showRenameHint: false, keybindings },
{ showRenameHint: false, keybindings, hydrateSession },
);
ui.addChild(selector);
@@ -1609,6 +1609,13 @@ export class SessionManager {
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
}
/**
* Hydrate a thin session listing with full searchable text.
*/
static async hydrateSessionForSearch(path: string): Promise<SessionInfo | null> {
return buildSessionInfo(path);
}
/**
* List sessions using stat mtime and only enough file content for initial display.
* Full searchable text is intentionally omitted.
+3 -2
View File
@@ -311,8 +311,9 @@ async function createSessionManager(
initTheme(settingsManager.getTheme(), true);
try {
const selectedPath = await selectSession(
(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),
(onProgress) => SessionManager.listAll(sessionDir, onProgress),
(onProgress) => SessionManager.listThin(cwd, sessionDir, onProgress),
(onProgress) => SessionManager.listAllThin(sessionDir, onProgress),
(path) => SessionManager.hydrateSessionForSearch(path),
);
if (!selectedPath) {
console.log(chalk.dim("No session selected"));
@@ -24,7 +24,7 @@ function normalizeWhitespaceLower(text: string): string {
}
function getSessionSearchText(session: SessionInfo): string {
return `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`;
return `${session.id} ${session.name ?? ""} ${session.firstMessage} ${session.allMessagesText} ${session.cwd} ${session.path}`;
}
export function hasSessionName(session: SessionInfo): boolean {
@@ -624,6 +624,7 @@ class SessionList implements Component, Focusable {
}
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
type SessionHydrator = (path: string) => Promise<SessionInfo | null>;
/**
* Delete a session file, trying the `trash` CLI first, then falling back to unlink
@@ -694,10 +695,12 @@ export class SessionSelectorComponent extends Container implements Focusable {
private allSessions: SessionInfo[] | null = null;
private currentSessionsLoader: SessionsLoader;
private allSessionsLoader: SessionsLoader;
private hydrateSession?: SessionHydrator;
private requestRender: () => void;
private renameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;
private currentLoading = false;
private allLoading = false;
private currentLoadSeq = 0;
private allLoadSeq = 0;
private mode: "list" | "rename" = "list";
@@ -743,6 +746,7 @@ export class SessionSelectorComponent extends Container implements Focusable {
renameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;
showRenameHint?: boolean;
keybindings?: KeybindingsManager;
hydrateSession?: SessionHydrator;
},
currentSessionFilePath?: string,
) {
@@ -750,6 +754,7 @@ export class SessionSelectorComponent extends Container implements Focusable {
this.keybindings = options?.keybindings ?? KeybindingsManager.create();
this.currentSessionsLoader = currentSessionsLoader;
this.allSessionsLoader = allSessionsLoader;
this.hydrateSession = options?.hydrateSession;
this.requestRender = requestRender;
this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender);
const renameSession = options?.renameSession;
@@ -907,6 +912,7 @@ export class SessionSelectorComponent extends Container implements Focusable {
private async loadScope(scope: SessionScope, reason: "initial" | "refresh" | "toggle"): Promise<void> {
const showCwd = scope === "all";
const seq = scope === "current" ? ++this.currentLoadSeq : ++this.allLoadSeq;
// Mark loading
if (scope === "current") {
@@ -915,14 +921,15 @@ export class SessionSelectorComponent extends Container implements Focusable {
this.allLoading = true;
}
const seq = scope === "all" ? ++this.allLoadSeq : undefined;
this.header.setScope(scope);
this.header.setLoading(true);
this.requestRender();
const isCurrentLoad = (): boolean =>
scope === "current" ? seq === this.currentLoadSeq : seq === this.allLoadSeq;
const onProgress = (loaded: number, total: number) => {
if (scope !== this.scope) return;
if (seq !== undefined && seq !== this.allLoadSeq) return;
if (scope !== this.scope || !isCurrentLoad()) return;
this.header.setProgress(loaded, total);
this.requestRender();
};
@@ -932,6 +939,8 @@ export class SessionSelectorComponent extends Container implements Focusable {
? this.currentSessionsLoader(onProgress)
: this.allSessionsLoader(onProgress));
if (!isCurrentLoad()) return;
if (scope === "current") {
this.currentSessions = sessions;
this.currentLoading = false;
@@ -940,13 +949,16 @@ export class SessionSelectorComponent extends Container implements Focusable {
this.allLoading = false;
}
if (scope !== this.scope) return;
if (seq !== undefined && seq !== this.allLoadSeq) return;
if (scope === this.scope) {
this.header.setLoading(false);
this.sessionList.setSessions(sessions, showCwd);
this.requestRender();
}
this.header.setLoading(false);
this.sessionList.setSessions(sessions, showCwd);
this.requestRender();
void this.hydrateScopeForSearch(scope, seq, sessions);
} catch (err) {
if (!isCurrentLoad()) return;
if (scope === "current") {
this.currentLoading = false;
} else {
@@ -954,7 +966,6 @@ export class SessionSelectorComponent extends Container implements Focusable {
}
if (scope !== this.scope) return;
if (seq !== undefined && seq !== this.allLoadSeq) return;
const message = err instanceof Error ? err.message : String(err);
this.header.setLoading(false);
@@ -967,6 +978,45 @@ export class SessionSelectorComponent extends Container implements Focusable {
}
}
private async hydrateScopeForSearch(scope: SessionScope, seq: number, sessions: SessionInfo[]): Promise<void> {
const hydrateSession = this.hydrateSession;
if (!hydrateSession) return;
const isCurrentLoad = (): boolean =>
scope === "current" ? seq === this.currentLoadSeq : seq === this.allLoadSeq;
let lastRender = 0;
for (const session of sessions) {
if (!isCurrentLoad()) return;
const hydrated = await hydrateSession(session.path);
if (!hydrated || !isCurrentLoad()) continue;
const targetSessions = scope === "current" ? this.currentSessions : this.allSessions;
if (!targetSessions) continue;
const index = targetSessions.findIndex((candidate) => candidate.path === hydrated.path);
if (index < 0) continue;
const existing = targetSessions[index]!;
targetSessions[index] = { ...hydrated, modified: existing.modified };
if (scope === this.scope) {
const now = Date.now();
if (now - lastRender > 100) {
this.sessionList.setSessions(targetSessions, scope === "all");
this.requestRender();
lastRender = now;
}
}
}
if (scope === this.scope && isCurrentLoad()) {
const targetSessions = scope === "current" ? this.currentSessions : this.allSessions;
this.sessionList.setSessions(targetSessions ?? [], scope === "all");
this.requestRender();
}
}
private toggleSortMode(): void {
// Cycle: threaded -> recent -> relevance -> threaded
this.sortMode = this.sortMode === "threaded" ? "recent" : this.sortMode === "recent" ? "relevance" : "threaded";