From 968e1d5a15af4012da7260d61a6c69a8acb9ec11 Mon Sep 17 00:00:00 2001 From: Hikari Date: Thu, 12 Mar 2026 23:47:14 -0700 Subject: [PATCH] feat: update session resume UI to show most recent prompt first Session list now sorts by last_activity_at descending so the most recently used session appears at the top. Preview text now shows the most recent user message rather than the first few messages, giving a much more relevant glimpse of where each session left off. --- src/lib/stores/sessions.test.ts | 164 +++++++++++++++++++++++++++++++- src/lib/stores/sessions.ts | 28 +++--- 2 files changed, 179 insertions(+), 13 deletions(-) diff --git a/src/lib/stores/sessions.test.ts b/src/lib/stores/sessions.test.ts index f686f06..56aa9ef 100644 --- a/src/lib/stores/sessions.test.ts +++ b/src/lib/stores/sessions.test.ts @@ -59,12 +59,52 @@ const makeConversation = () => ({ describe("sessionsStore - loadSessions", () => { it("loads sessions from backend and updates the store", async () => { - const sessionList = [{ id: "session-1", name: "Test", message_count: 1, preview: "..." }]; + const sessionList = [ + { + id: "session-1", + name: "Test", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-03T11:00:00.000Z", + }, + ]; setMockInvokeResult("list_sessions", sessionList); await sessionsStore.loadSessions(); expect(get(sessionsStore.sessions)).toEqual(sessionList); }); + it("sorts sessions by last_activity_at descending", async () => { + const sessionList = [ + { + id: "older", + name: "Older", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-01T10:00:00.000Z", + }, + { + id: "newest", + name: "Newest", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-03T12:00:00.000Z", + }, + { + id: "middle", + name: "Middle", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-02T10:00:00.000Z", + }, + ]; + setMockInvokeResult("list_sessions", sessionList); + await sessionsStore.loadSessions(); + const sorted = get(sessionsStore.sessions); + expect(sorted[0].id).toBe("newest"); + expect(sorted[1].id).toBe("middle"); + expect(sorted[2].id).toBe("older"); + }); + it("handles errors gracefully", async () => { const spy = vi.spyOn(console, "error").mockImplementation(() => {}); setMockInvokeResult("list_sessions", new Error("Backend error")); @@ -128,12 +168,44 @@ describe("sessionsStore - searchSessions", () => { }); it("searches with the given query", async () => { - const results = [{ id: "session-1", name: "Test", message_count: 1, preview: "..." }]; + const results = [ + { + id: "session-1", + name: "Test", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-03T11:00:00.000Z", + }, + ]; setMockInvokeResult("search_sessions", results); await sessionsStore.searchSessions("test"); expect(get(sessionsStore.sessions)).toEqual(results); }); + it("sorts search results by last_activity_at descending", async () => { + const results = [ + { + id: "older", + name: "Older", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-01T10:00:00.000Z", + }, + { + id: "newest", + name: "Newest", + message_count: 1, + preview: "...", + last_activity_at: "2026-03-03T12:00:00.000Z", + }, + ]; + setMockInvokeResult("search_sessions", results); + await sessionsStore.searchSessions("query"); + const sorted = get(sessionsStore.sessions); + expect(sorted[0].id).toBe("newest"); + expect(sorted[1].id).toBe("older"); + }); + it("updates searchQuery store", async () => { setMockInvokeResult("search_sessions", []); await sessionsStore.searchSessions("hello"); @@ -187,6 +259,94 @@ describe("sessionsStore - saveConversation", () => { const conv = { ...makeConversation(), terminalLines: [] }; await sessionsStore.saveConversation(conv as never); }); + + it("uses the most recent user message as the preview", async () => { + const { invoke } = await import("@tauri-apps/api/core"); + setMockInvokeResult("save_session", undefined); + setMockInvokeResult("list_sessions", []); + + const conv = { + ...makeConversation(), + terminalLines: [ + { + id: "1", + type: "user", + content: "First message", + timestamp: new Date(), + toolName: undefined, + }, + { + id: "2", + type: "assistant", + content: "Reply one", + timestamp: new Date(), + toolName: undefined, + }, + { + id: "3", + type: "user", + content: "Most recent prompt", + timestamp: new Date(), + toolName: undefined, + }, + { + id: "4", + type: "assistant", + content: "Reply two", + timestamp: new Date(), + toolName: undefined, + }, + ], + }; + await sessionsStore.saveConversation(conv as never); + + const saveCall = vi.mocked(invoke).mock.calls.find((call) => call[0] === "save_session"); + const capturedSession = (saveCall![1] as { session: SavedSession }).session; + expect(capturedSession.preview).toBe("Most recent prompt"); + }); + + it("truncates long preview text at 150 characters", async () => { + const { invoke } = await import("@tauri-apps/api/core"); + setMockInvokeResult("save_session", undefined); + setMockInvokeResult("list_sessions", []); + + const longContent = "A".repeat(200); + const conv = { + ...makeConversation(), + terminalLines: [ + { id: "1", type: "user", content: longContent, timestamp: new Date(), toolName: undefined }, + ], + }; + await sessionsStore.saveConversation(conv as never); + + const saveCall = vi.mocked(invoke).mock.calls.find((call) => call[0] === "save_session"); + const capturedSession = (saveCall![1] as { session: SavedSession }).session; + expect(capturedSession.preview).toBe("A".repeat(150) + "..."); + }); + + it("uses 'Empty conversation' as preview when there are no user messages", async () => { + const { invoke } = await import("@tauri-apps/api/core"); + setMockInvokeResult("save_session", undefined); + setMockInvokeResult("list_sessions", []); + + const conv = { + ...makeConversation(), + terminalLines: [ + { + id: "1", + type: "assistant", + content: "Only assistant message", + timestamp: new Date(), + toolName: undefined, + }, + ], + }; + await sessionsStore.saveConversation(conv as never); + + const saveCall = vi.mocked(invoke).mock.calls.find((call) => call[0] === "save_session"); + const capturedSession = (saveCall![1] as { session: SavedSession }).session; + expect(capturedSession.preview).toBe("Empty conversation"); + }); }); describe("sessionsStore - scheduleAutoSave and cancelAutoSave", () => { diff --git a/src/lib/stores/sessions.ts b/src/lib/stores/sessions.ts index 02c5c19..777fbb8 100644 --- a/src/lib/stores/sessions.ts +++ b/src/lib/stores/sessions.ts @@ -378,7 +378,11 @@ function createSessionsStore() { isLoading.set(true); try { const result = await invoke("list_sessions"); - sessions.set(result); + sessions.set( + result.sort( + (a, b) => new Date(b.last_activity_at).getTime() - new Date(a.last_activity_at).getTime() + ) + ); } catch (error) { console.error("Failed to load sessions:", error); } finally { @@ -395,15 +399,13 @@ function createSessionsStore() { tool_name: line.toolName, })); - const userAndAssistantMessages = conversation.terminalLines.filter( - (line) => line.type === "user" || line.type === "assistant" - ); - const previewContent = - userAndAssistantMessages - .slice(0, 3) - .map((m) => m.content) - .join(" ") - .slice(0, 150) + (userAndAssistantMessages.length > 3 ? "..." : ""); + const userMessages = conversation.terminalLines.filter((line) => line.type === "user"); + const mostRecentUserMessage = userMessages.at(-1); + const previewContent = mostRecentUserMessage + ? mostRecentUserMessage.content.length > 150 + ? mostRecentUserMessage.content.slice(0, 150) + "..." + : mostRecentUserMessage.content + : "Empty conversation"; const session: SavedSession = { id: conversation.id, @@ -458,7 +460,11 @@ function createSessionsStore() { const result = await invoke("search_sessions", { query, }); - sessions.set(result); + sessions.set( + result.sort( + (a, b) => new Date(b.last_activity_at).getTime() - new Date(a.last_activity_at).getTime() + ) + ); } catch (error) { console.error("Failed to search sessions:", error); } finally {