generated from nhcarrigan/template
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.
This commit is contained in:
@@ -59,12 +59,52 @@ const makeConversation = () => ({
|
|||||||
|
|
||||||
describe("sessionsStore - loadSessions", () => {
|
describe("sessionsStore - loadSessions", () => {
|
||||||
it("loads sessions from backend and updates the store", async () => {
|
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);
|
setMockInvokeResult("list_sessions", sessionList);
|
||||||
await sessionsStore.loadSessions();
|
await sessionsStore.loadSessions();
|
||||||
expect(get(sessionsStore.sessions)).toEqual(sessionList);
|
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 () => {
|
it("handles errors gracefully", async () => {
|
||||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
setMockInvokeResult("list_sessions", new Error("Backend error"));
|
setMockInvokeResult("list_sessions", new Error("Backend error"));
|
||||||
@@ -128,12 +168,44 @@ describe("sessionsStore - searchSessions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("searches with the given query", async () => {
|
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);
|
setMockInvokeResult("search_sessions", results);
|
||||||
await sessionsStore.searchSessions("test");
|
await sessionsStore.searchSessions("test");
|
||||||
expect(get(sessionsStore.sessions)).toEqual(results);
|
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 () => {
|
it("updates searchQuery store", async () => {
|
||||||
setMockInvokeResult("search_sessions", []);
|
setMockInvokeResult("search_sessions", []);
|
||||||
await sessionsStore.searchSessions("hello");
|
await sessionsStore.searchSessions("hello");
|
||||||
@@ -187,6 +259,94 @@ describe("sessionsStore - saveConversation", () => {
|
|||||||
const conv = { ...makeConversation(), terminalLines: [] };
|
const conv = { ...makeConversation(), terminalLines: [] };
|
||||||
await sessionsStore.saveConversation(conv as never);
|
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", () => {
|
describe("sessionsStore - scheduleAutoSave and cancelAutoSave", () => {
|
||||||
|
|||||||
+17
-11
@@ -378,7 +378,11 @@ function createSessionsStore() {
|
|||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
try {
|
try {
|
||||||
const result = await invoke<SessionListItem[]>("list_sessions");
|
const result = await invoke<SessionListItem[]>("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) {
|
} catch (error) {
|
||||||
console.error("Failed to load sessions:", error);
|
console.error("Failed to load sessions:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -395,15 +399,13 @@ function createSessionsStore() {
|
|||||||
tool_name: line.toolName,
|
tool_name: line.toolName,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const userAndAssistantMessages = conversation.terminalLines.filter(
|
const userMessages = conversation.terminalLines.filter((line) => line.type === "user");
|
||||||
(line) => line.type === "user" || line.type === "assistant"
|
const mostRecentUserMessage = userMessages.at(-1);
|
||||||
);
|
const previewContent = mostRecentUserMessage
|
||||||
const previewContent =
|
? mostRecentUserMessage.content.length > 150
|
||||||
userAndAssistantMessages
|
? mostRecentUserMessage.content.slice(0, 150) + "..."
|
||||||
.slice(0, 3)
|
: mostRecentUserMessage.content
|
||||||
.map((m) => m.content)
|
: "Empty conversation";
|
||||||
.join(" ")
|
|
||||||
.slice(0, 150) + (userAndAssistantMessages.length > 3 ? "..." : "");
|
|
||||||
|
|
||||||
const session: SavedSession = {
|
const session: SavedSession = {
|
||||||
id: conversation.id,
|
id: conversation.id,
|
||||||
@@ -458,7 +460,11 @@ function createSessionsStore() {
|
|||||||
const result = await invoke<SessionListItem[]>("search_sessions", {
|
const result = await invoke<SessionListItem[]>("search_sessions", {
|
||||||
query,
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to search sessions:", error);
|
console.error("Failed to search sessions:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user