summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-29 13:59:17 +0900
committerAdam Malczewski <[email protected]>2026-05-29 13:59:17 +0900
commit520c9e30cc58b40d3b1ee9e7895f003c4f873206 (patch)
tree7a10da94141d754dca25e728f4a388057a5d5981 /packages/core/src
parent0954c6520878e073c7822fc4a8f4a752474f5d7a (diff)
downloaddispatch-520c9e30cc58b40d3b1ee9e7895f003c4f873206.tar.gz
dispatch-520c9e30cc58b40d3b1ee9e7895f003c4f873206.zip
feat: disappearing chat history — chunk-limited frontend window with backend pagination
Frontend keeps only a bounded window of chunks in memory (configurable via settings slider, default 100). Older messages are evicted when at the bottom and re-fetched from the backend on scroll-up. - Backend: paginated GET /tabs/:id/messages with ?limit=N&before=seq - Store: evictMessages trims oldest messages until total chunks ≤ limit - Store: loadMoreMessages fetches next page and prepends with dedup - ChatPanel: smart scroll hooks trigger eviction on return-to-bottom - ChatPanel: onNearTop loads older history with scroll-position maintenance - Settings: chunk limit slider in Memory section - Fix: oldestLoadedSeq recalculated after eviction (pagination cursor stays valid) - Fix: seq preserved on ChatMessage for cursor tracking - Fix: scrolledUpTabs cleaned up on tab switch (no memory leak) - Fix: evictMessages reads appSettings.chunkLimit directly (live updates)
Diffstat (limited to 'packages/core/src')
-rw-r--r--packages/core/src/db/messages.ts77
-rw-r--r--packages/core/src/index.ts1
2 files changed, 71 insertions, 7 deletions
diff --git a/packages/core/src/db/messages.ts b/packages/core/src/db/messages.ts
index 34b69e5..7fc6ccf 100644
--- a/packages/core/src/db/messages.ts
+++ b/packages/core/src/db/messages.ts
@@ -56,16 +56,26 @@ export function updateMessage(id: string, contentJson: string): void {
}
/**
- * Read all messages for a tab in seq order. `content_json` is parsed into
+ * Read messages for a tab in seq order (ASC). `content_json` is parsed into
* `Chunk[]` here so callers don't have to. If a row's JSON is malformed,
* the message is returned with an empty chunk list rather than throwing.
+ *
+ * When `options` is omitted, returns ALL messages (backward compatible).
+ *
+ * When `options.before` is provided, returns messages with `seq < before`,
+ * taking the most recent ones first (DESC) up to `options.limit`, then
+ * reversing back to ASC before returning.
+ *
+ * When only `options.limit` is provided, returns the most recent `limit`
+ * messages, reversed back to ASC.
*/
-export function getMessagesForTab(tabId: string): MessageRow[] {
+export function getMessagesForTab(
+ tabId: string,
+ options?: { limit?: number; before?: number },
+): MessageRow[] {
const db = getDatabase();
- const rows = db
- .query("SELECT * FROM messages WHERE tab_id = $tabId ORDER BY seq ASC")
- .all({ $tabId: tabId }) as Array<Record<string, unknown>>;
- return rows.map((row) => {
+
+ const mapRow = (row: Record<string, unknown>): MessageRow => {
const rawJson = row.content_json as string;
let chunks: Chunk[];
try {
@@ -82,7 +92,60 @@ export function getMessagesForTab(tabId: string): MessageRow[] {
chunks,
createdAt: row.created_at as number,
};
- });
+ };
+
+ // Backward-compatible path: no options → ALL messages, seq ASC.
+ if (!options) {
+ const rows = db
+ .query("SELECT * FROM messages WHERE tab_id = $tabId ORDER BY seq ASC")
+ .all({ $tabId: tabId }) as Array<Record<string, unknown>>;
+ return rows.map(mapRow);
+ }
+
+ const { limit, before } = options;
+
+ // Paginated path: fetch DESC, then reverse to ASC before returning.
+ if (before !== undefined) {
+ // `seq < before`, DESC, optionally limited.
+ if (limit !== undefined) {
+ const rows = db
+ .query(
+ "SELECT * FROM messages WHERE tab_id = $tabId AND seq < $before ORDER BY seq DESC LIMIT $limit",
+ )
+ .all({ $tabId: tabId, $before: before, $limit: limit }) as Array<Record<string, unknown>>;
+ return rows.map(mapRow).reverse();
+ }
+ const rows = db
+ .query("SELECT * FROM messages WHERE tab_id = $tabId AND seq < $before ORDER BY seq DESC")
+ .all({ $tabId: tabId, $before: before }) as Array<Record<string, unknown>>;
+ return rows.map(mapRow).reverse();
+ }
+
+ // Only `limit` provided: most recent `limit`, reversed to ASC.
+ if (limit !== undefined) {
+ const rows = db
+ .query("SELECT * FROM messages WHERE tab_id = $tabId ORDER BY seq DESC LIMIT $limit")
+ .all({ $tabId: tabId, $limit: limit }) as Array<Record<string, unknown>>;
+ return rows.map(mapRow).reverse();
+ }
+
+ // `options` was provided but empty → same as no options.
+ const rows = db
+ .query("SELECT * FROM messages WHERE tab_id = $tabId ORDER BY seq ASC")
+ .all({ $tabId: tabId }) as Array<Record<string, unknown>>;
+ return rows.map(mapRow);
+}
+
+/**
+ * Return the total number of persisted messages for a tab.
+ * Used by the API to advertise total history size alongside a paginated window.
+ */
+export function getTotalMessageCount(tabId: string): number {
+ const db = getDatabase();
+ const row = db
+ .query("SELECT COUNT(*) as count FROM messages WHERE tab_id = $tabId")
+ .get({ $tabId: tabId }) as { count: number } | null;
+ return row?.count ?? 0;
}
export function clearMessagesForTab(tabId: string): void {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 74cb159..e33ad2f 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -35,6 +35,7 @@ export {
appendMessage,
clearMessagesForTab,
getMessagesForTab,
+ getTotalMessageCount,
type MessageRow,
updateMessage,
} from "./db/messages.js";