fix(FN-1817): enrich session list with lastMessagePreview and restore active session on remount

- Add lastMessagePreview field to ChatSession to show message previews in session list
- Restore active session state on component remount via stored lastSessionId
- Add chat routes for session management (create, list, read, update/delete)
- Add tests for useChat hook, chat store, and chat routes
- Remove unused serveCommand assignment in serve.ts command
This commit is contained in:
Fusion
2026-04-16 05:47:34 -07:00
committed by gsxdsm
parent 6e784c3700
commit 7e05e8962f
8 changed files with 381 additions and 2 deletions

View File

@@ -449,6 +449,68 @@ describe("ChatStore", () => {
expect(result).toBeUndefined();
});
});
describe("getLastMessageForSessions", () => {
it("returns the most recent message for each session", async () => {
const session1 = createTestSession(store);
const session2 = createTestSession(store);
// Add messages to session1
store.addMessage(session1.id, { role: "user", content: "Hello" });
await new Promise((r) => setTimeout(r, 5));
const latestMsg1 = store.addMessage(session1.id, {
role: "assistant",
content: "Latest for session 1",
});
// Add only one message to session2
const latestMsg2 = store.addMessage(session2.id, {
role: "assistant",
content: "Latest for session 2",
});
const result = store.getLastMessageForSessions([session1.id, session2.id]);
expect(result.size).toBe(2);
expect(result.get(session1.id)).toBeDefined();
expect(result.get(session1.id)!.content).toBe("Latest for session 1");
expect(result.get(session2.id)).toBeDefined();
expect(result.get(session2.id)!.content).toBe("Latest for session 2");
});
it("handles empty session list", () => {
const result = store.getLastMessageForSessions([]);
expect(result.size).toBe(0);
});
it("handles sessions with no messages", () => {
const session1 = createTestSession(store);
const session2 = createTestSession(store);
// Only add message to session1
store.addMessage(session1.id, { role: "user", content: "Hello" });
const result = store.getLastMessageForSessions([session1.id, session2.id]);
expect(result.size).toBe(1);
expect(result.has(session1.id)).toBe(true);
expect(result.has(session2.id)).toBe(false);
});
it("handles non-existent session IDs", () => {
const session = createTestSession(store);
store.addMessage(session.id, { role: "user", content: "Hello" });
const result = store.getLastMessageForSessions([
session.id,
"non-existent-1",
"non-existent-2",
]);
expect(result.size).toBe(1);
expect(result.has(session.id)).toBe(true);
});
});
});
// ── Event Emission Tests ─────────────────────────────────────────

View File

@@ -339,4 +339,39 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
if (!row) return undefined;
return this.rowToMessage(row);
}
/**
* Get the latest message for each session in the provided list.
* Uses a single SQL query with GROUP BY and MAX to efficiently fetch last messages.
*
* @param sessionIds - Array of session IDs to fetch last messages for
* @returns Map of sessionId -> latest ChatMessage for that session
*/
getLastMessageForSessions(sessionIds: string[]): Map<string, ChatMessage> {
if (!sessionIds || sessionIds.length === 0) {
return new Map();
}
// Create placeholders for the IN clause
const placeholders = sessionIds.map(() => "?").join(", ");
// Use a subquery to get the latest message per session using MAX(createdAt)
// Then join back to get the full message row
const rows = this.db.prepare(`
SELECT cm.* FROM chat_messages cm
INNER JOIN (
SELECT sessionId, MAX(createdAt) as maxCreatedAt
FROM chat_messages
WHERE sessionId IN (${placeholders})
GROUP BY sessionId
) latest ON cm.sessionId = latest.sessionId AND cm.createdAt = latest.maxCreatedAt
`).all(...sessionIds);
const result = new Map<string, ChatMessage>();
for (const row of rows as any[]) {
const message = this.rowToMessage(row);
result.set(message.sessionId, message);
}
return result;
}
}