diff options
| author | Adam Malczewski <[email protected]> | 2026-05-29 13:59:17 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-29 13:59:17 +0900 |
| commit | 520c9e30cc58b40d3b1ee9e7895f003c4f873206 (patch) | |
| tree | 7a10da94141d754dca25e728f4a388057a5d5981 /packages/core/src | |
| parent | 0954c6520878e073c7822fc4a8f4a752474f5d7a (diff) | |
| download | dispatch-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.ts | 77 | ||||
| -rw-r--r-- | packages/core/src/index.ts | 1 |
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"; |
