summaryrefslogtreecommitdiffhomepage
path: root/src/app/App.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 18:26:00 +0900
committerAdam Malczewski <[email protected]>2026-06-12 18:26:00 +0900
commit4d2e6d817f65a13631f00786c45b403d7d7d58ec (patch)
treeceb08176b7e121fcbd53938096322cb2b7958d48 /src/app/App.svelte
parent46d6b48d5229bd8cd8eb61efd0a0275349de7caf (diff)
downloaddispatch-web-4d2e6d817f65a13631f00786c45b403d7d7d58ec.tar.gz
dispatch-web-4d2e6d817f65a13631f00786c45b403d7d7d58ec.zip
feat(chat): chat limit — bulk quarter-unload, 75% fresh-load window, show-earlier page-in
Long transcripts no longer grow unbounded: past the chat limit (default 256 chunks, localStorage dispatch.chatLimit) the oldest ceil(limit/4) committed chunks are unloaded in ONE bulk pass — never one-per-delta (old Dispatch's scroll-jump-per-step bug) — and only while the reader is stuck to the bottom (scrolled-up readers defer the trim; it catches up in whole quarters). A fresh page load windows to the newest floor(0.75*limit). Unloading is purely local (IndexedDB cache + server keep everything); a hiddenBeforeSeq watermark keeps history merges from resurrecting unloaded chunks, and a 'Show earlier messages' affordance pages a quarter back in from the cache with scroll-anchor preservation. Thinking-collapse render keys stay stable across trims via a hiddenThinkingCount ordinal base. - core/chunks/trim.ts: pure policy (trim/window/restore/normalize) + tests - chat store: chatLimit + canUnload deps, windowed load, showEarlier() - composition root: dispatch.chatLimit localStorage knob + unload gate wired to smart-scroll isAtBottom() - backend CR-5 OPENED (not a blocker): ?limit=/?beforeSeq= on GET /conversations/:id (courier backend-handoff-chat-limit.md) - scripts/live-probe.ts: fix pre-existing stale TurnMetricsEntry reads (m1.usage -> total.usage) that crashed the probe; 17/17 live checks pass
Diffstat (limited to 'src/app/App.svelte')
-rw-r--r--src/app/App.svelte34
1 files changed, 33 insertions, 1 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte
index 50f24e7..4c5a82b 100644
--- a/src/app/App.svelte
+++ b/src/app/App.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import type { InvokeMessage } from "@dispatch/ui-contract";
+ import { tick } from "svelte";
import Table from "../components/Table.svelte";
import {
CacheWarmingView,
@@ -76,6 +77,31 @@
let transcriptEl = $state<HTMLElement | undefined>();
let transcriptContentEl = $state<HTMLElement | undefined>();
+ // Chat-limit unload gate: old chunks may be unloaded only while the reader is
+ // stuck to the bottom. While stuck, a trim removes content far ABOVE the
+ // viewport and the controller re-pins to the bottom — no visible jump; while
+ // reading history, trimming is deferred instead of yanking the page (the old
+ // Dispatch bug). In an $effect so a swapped store prop would be re-wired.
+ $effect(() => {
+ store.attachUnloadGate(() => smartScroll.isAtBottom());
+ });
+
+ // "Show earlier messages": page older history back in, preserving the reader's
+ // viewport position — prepended content grows scrollHeight, so shift scrollTop
+ // by the growth (the manual analogue of CSS scroll anchoring, which not every
+ // engine applies here).
+ async function handleShowEarlier(): Promise<void> {
+ const el = transcriptEl;
+ const prevHeight = el?.scrollHeight ?? 0;
+ const prevTop = el?.scrollTop ?? 0;
+ await store.activeChat.showEarlier();
+ await tick();
+ if (el) {
+ const delta = el.scrollHeight - prevHeight;
+ if (delta > 0) el.scrollTop = prevTop + delta;
+ }
+ }
+
// Attach/detach the controller to the live scroll element + content (disposed on
// unmount). The content element is observed (ResizeObserver) so the view follows
// height changes that aren't a transcript append.
@@ -201,7 +227,13 @@
<div bind:this={transcriptEl} class="h-full overflow-y-auto">
<div bind:this={transcriptContentEl}>
{#key store.activeConversationId}
- <ChatView chunks={store.activeChat.chunks} turnMetrics={store.activeChat.turnMetrics} />
+ <ChatView
+ chunks={store.activeChat.chunks}
+ turnMetrics={store.activeChat.turnMetrics}
+ hasEarlier={store.activeChat.hasEarlier}
+ onShowEarlier={handleShowEarlier}
+ thinkingKeyBase={store.activeChat.thinkingKeyBase}
+ />
{/key}
</div>
</div>