summaryrefslogtreecommitdiffhomepage
path: root/src/app
AgeCommit message (Collapse)Author
2026-06-28fix(tasks): keep fixed 60vh height even with 0 tasksAdam Malczewski
2026-06-28Merge branch 'feature/concurrency-fixes' into predevAdam Malczewski
# Conflicts: # backend-handoff.md
2026-06-28Merge branch 'feature/workspace-active-indicator' into predevAdam Malczewski
2026-06-28Merge branch 'feature/cache-display-fix' into predevAdam Malczewski
2026-06-28feat(concurrency): configurable cooldown + adaptive-headroom auto-reduce bannerAdam Malczewski
Consumes the backend's concurrency-fixes (commit 2d27666) — additive to [email protected], NO version bump. ConcurrencyStatusEntry gains cooldownMs / autoReduced / autoReducedFrom? / notice?; new ConcurrencyCooldownResponse / SetConcurrencyCooldownRequest + GET/PUT /concurrency/cooldown/:providerId. FE: - Pure core (logic/view-model.ts): DEFAULT_COOLDOWN_MS; parseCooldownInput (non-negative int — 0 is valid, unlike the limit); normalizeCooldown; cooldownLabel ("350ms"/"1.2s"/"0ms (off)"); viewConcurrencyStatus extended (cooldown + autoReduce fields; auto-reduce → warning badge, not busy); viewAutoReduce/autoReduceNotices (banner view — prefers backend notice, synthesizes a fallback); summarizeStatus "N auto-reduced" fragment; normalizeConcurrencyStatus coerces new fields (immutable readonly build; autoReducedFrom/notice only when autoReduced===true); normalizeConcurrencyCooldown. - Types (logic/types.ts): re-export the 2 new contract types + ConcurrencyCooldownResult + Get/SaveConcurrencyCooldown ports. - UI: ConcurrencyCooldownRow.svelte (inline-edit cooldown + Save → PUT, seeded via the ChatLimitField pattern); AutoReduceBanner.svelte (dismissible banner — backend notice + "Was N, now M." + "Restore to N"); ConcurrencyView renders the cooldown per status card + the banner section. The banner persists while autoReduced===true, is dismissible, and clears automatically once a poll shows autoReduced===false after a restore PUT. - Store (store.svelte.ts): getConcurrencyCooldown + setConcurrencyCooldown (surface 400/404/503 as ok:false; normalize at the seam) + interface decls. - App.svelte: saveConcurrencyCooldown adapter → ConcurrencyView. - Tests: +47 (view-model cooldown/auto-reduce/normalizers; component banner render, cooldown PUT, restore clears banner, dismiss; store cooldown GET/PUT). Re-synced the file: dep (bun install) + re-mirrored .dispatch/transport-contract. reference.md. typecheck 0/0, 1048 tests green (run twice), biome clean, build OK. Worktree env: an untracked dispatch-backend → backend symlink was created in the worktree parent so the canonical file:../dispatch-backend/... paths resolve (NOT committed — per the §2d/§2j worktree convention).
2026-06-28feat(workspace-active-indicator): show loading dots on workspace cards with ↵Adam Malczewski
active chats
2026-06-28fix(cache-display): fix stale cache info on unloaded steps + missing cache ↵Adam Malczewski
percentages on active steps
2026-06-27style(header-padding): increase top/bottom padding of workspace header barAdam Malczewski
2026-06-27Merge branch 'feature/vision-handoff' into devAdam Malczewski
# Conflicts: # .dispatch/transport-contract.reference.md # backend-handoff.md # src/app/App.svelte # src/features/chat/ui/Composer.svelte
2026-06-27refactor(vision): fold Vision settings into the Compaction sidebar viewAdam Malczewski
Both pertain to compaction (message compaction + image compaction), so they share one sidebar panel. Removes the standalone "Vision" view kind; the VisionSettingsView now renders inside the Compaction panel, below CompactionView (with an "Image compaction" divider). CompactionView stays keyed per conversation (its percent is per-conversation); VisionSettingsView stays un-keyed (its settings are global). The vision feature library + manifest are unchanged (still a composed module). A persisted "vision" sidebar panel gracefully renders empty (the kind is no longer in viewKinds) until the user re-selects Compaction. Verification: svelte-check 0/0; vitest 959/959; biome clean; build OK. Not merged or pushed.
2026-06-27feat(vision): resolve persisted image URLs against the API baseAdam Malczewski
Images are now stored on disk under tmp (not SQLite) and served via GET /images/:conversationId/:imageId. Persisted ImageChunk.url is a compact relative HTTP path (/images/<conv>/<uuid>.png) instead of a base64 data URL. No wire/transport-contract type change (behavior only) — re-mirrored the delta notes. - New pure resolveImageUrl(url, apiBase) helper (core/chunks/image-url.ts, +8 tests): data/absolute URLs pass through; relative paths are prepended with the API base (no double slash; empty base -> root-relative). Exported from core/chunks + re-exported from features/chat. - ChatView: new apiBaseUrl prop; <img src> uses resolveImageUrl. The optimistic echo's data URL passes through; persisted relative paths resolve against the base. +3 tests. - AppStore exposes httpBase (getter); App.svelte passes apiBaseUrl into ChatView and the heartbeat RunModal (also renders image chunks). Verification: svelte-check 0/0; vitest 959/959 (run twice, +11); biome clean; vite build OK. See backend-handoff.md §2j-update-2. Not merged or pushed.
2026-06-27feat(vision): consult_vision tool + vision settings APIAdam Malczewski
Backend vision update (additive to [email protected] / [email protected], no version bump). Contracts mirrored (.dispatch/transport-contract.reference.md): - VisionSettingsResponse + SetVisionSettingsRequest (GET/PUT /settings/vision). - Delta note: read_image -> consult_vision; numbered placeholders; compaction. Tool rendering (ChatView): - read_image test -> consult_vision (rendering is generic by toolName). - +2 tests: numbered-placeholder text chunk + [Compacted image] text chunk (both regular text chunks, render as-is — no special handling). New vision feature library (src/features/vision/): - logic/view-model.ts (32 tests): VisionSettings/VisionSettingsPatch types (consumer-defines-port), LoadVisionSettings/SaveVisionSettings ports + results, normalizeVisionSettings (network-seam coercion), parseImageLimit/ imageLimitChanged, compactionModelOptions (vision-capable models via chat's public isVisionModel + Auto sentinel), round-trip helpers, imageLimitLabel. - ui/VisionSettingsView.svelte (9 tests): imageLimit input + Save, compactionModel dropdown (Auto + vision-capable models), load-on-mount, save-on-change, error/saved feedback. - index.ts. Cross-unit seam: isVisionModel added to features/chat public index.ts (additive); imported through the public surface, not internals. Store wiring (src/app/store.svelte.ts): - visionSettings state + refreshVisionSettings (GET /settings/vision, normalized at the seam) + setVisionSettings (PUT, partial, returns merged) + VisionSettingsResult; seeded on boot; exposed on AppStore. +4 store tests. Mounted in App.svelte: new "Vision" sidebar view kind + VisionSettingsView in viewContent (not conversation-scoped); load/save adapters; visionManifest. Verification: svelte-check 0/0; vitest 948/948 (run twice, +47 since the prior vision commit); biome clean; vite build OK. See backend-handoff.md §2j. Not merged or pushed.
2026-06-27feat(concurrency): loading-ring for queued chats (CR-13 consumed)Adam Malczewski
Backend shipped "queued" ConversationStatus (additive to [email protected]): when a request blocks on a concurrency slot, conversation.statusChanged broadcasts "queued" (broadcast-only, never persisted); "active" on slot grant. FE consumes it: - WS parser (adapters/ws/logic.ts): accepts "queued" in the status set. - Store handler: "queued" updates the status map + opens a tab for a new cross-device queued conversation (like "active"). - TabList: status === "queued" -> loading-ring (spinner, aria-label "Queued"); "active" -> loading-dots (unchanged). - Composer: status type widened to ComposerStatus (idle|running|queued|error), exported from features/chat. "queued" -> a loading-ring status icon + placeholder "Queued for a slot…"; behaves like "running" for the send button (steer/stop — the turn is in flight, just waiting for a slot). - App.svelte: composerStatus derived (error > queued > running > idle) — conversationStatus === "queued" wins over generating so the corner shows a ring during the wait (turn-start fires before the slot is granted, so generating is already true while status === "queued"). - Re-mirrored .dispatch/wire.reference.md (ConversationStatus widened + header). Tests: WS parser accepts queued; store handler sets status + opens a cross-device tab + transitions queued->active->idle; TabList renders a ring for queued + dots for active. typecheck 0/0, 925 tests green (x2), biome clean, build OK. backend-handoff.md CR-13 marked RESOLVED.
2026-06-27Merge branch 'dev' into feature/provider-concurrencyAdam Malczewski
2026-06-27feat(concurrency): provider dropdown instead of free-text inputAdam Malczewski
Replace the Add-form provider text input with a <select> dropdown populated from the available models' provider prefixes (<provider>/<model>), unioned with providers already carrying a configured limit (so a limit set out-of-band but whose model list is empty still appears). The selection auto-defaults to the first option and falls back when an option disappears. - logic/view-model.ts: pure providerFromModel + providerOptions(models, limits) (distinct provider ids, first-seen order) + 7 tests. - ConcurrencyView.svelte: models prop + <select> bound to newProviderId; disabled + 'No providers available' when there are no models. - App.svelte: pass models={store.models}; component test updated to the dropdown (selectOptions) + a no-models case (6 component tests). Verified: typecheck 0/0, 922 tests green, biome clean, build OK.
2026-06-27feat(sidebar-tabs): add dashboard home button + same-tab workspace openAdam Malczewski
2026-06-27feat(sidebar-tabs): prefix build hash with "build: "Adam Malczewski
2026-06-27fix(sidebar-tabs): align chat scrollbar with composer instead of flush ↵Adam Malczewski
against sidebar
2026-06-27fix(sidebar-tabs): drop sidebar left padding so it shares the chat gutterAdam Malczewski
2026-06-27feat(sidebar-tabs): remove sidebar left borderAdam Malczewski
2026-06-27feat(sidebar-tabs): replace hamburger with directional double-chevron toggleAdam Malczewski
2026-06-27fix(sidebar-tabs): inset top-bar title 8px left to match parent px-2 spacingAdam Malczewski
2026-06-27feat(sidebar-tabs): add 2px horizontal spacing to top-bar titleAdam Malczewski
2026-06-27feat(sidebar-tabs): add active tab title to top bar (New Tab for unstarted ↵Adam Malczewski
chats)
2026-06-27feat(vision): image paste + transcript image rendering + vision badgeAdam Malczewski
Vision & vision-handoff frontend (consumes the backend's additive [email protected] / [email protected] image types — no version bump). Contracts mirrored: - .dispatch/wire.reference.md: ImageChunk added to the Chunk union + ImageChunk/ImageInput interfaces. - .dispatch/transport-contract.reference.md: ChatRequest.images, ModelMetadata.vision, + ImageChunk/ImageInput re-exports. Core (core/chunks): - conformance: assertChunkExhaustive handles the new 'image' variant (the guard caught it — its purpose). - appendUserMessage(state, text, images?) echoes a [text, image, ...] user run; the user-message event dedup scans the trailing user run (not just the last chunk) so an image-bearing echo doesn't duplicate the text; applyHistory's during-gen dedup matches a multi-chunk echo by content equality (chunkContentEquals + trailingRun helpers). UI: - ChatView renders user 'image' chunks as lazy <img> bubbles; a non-vision model's persisted [image, analysis-text] both render. read_image tool renders generically (no special-casing). - Composer: clipboard paste / file picker / drag-drop of images -> base64 data URLs, thumbnail previews with remove, forwarded on chat.send (omitted when none). Image-only sends allowed; steering (chat.queue) never forwards images. - ModelSelector: vision badge (isVisionModel) marks vision-capable models; indicator shows native-vision vs vision-handoff hint. Store wiring: ChatStore.send + AppStore.send + App.svelte handleSend thread images through; chat.send still omits cwd (only images added). Verification: svelte-check 0/0; vitest 901/901 (run twice, +34 new); biome clean; vite build OK. See backend-handoff.md §2j. Not merged or pushed.
2026-06-27feat(concurrency): add provider concurrency limits UI — settings + live statusAdam Malczewski
2026-06-27feat(sidebar-tabs): move tab bar from top into sidebar as vertical listAdam Malczewski
2026-06-27Merge branch 'dev' into feature/heartbeatAdam Malczewski
# Conflicts: # src/app/App.svelte # src/app/store.svelte.ts # src/app/store.test.ts # src/features/workspaces/ui/WorkspaceCard.test.ts
2026-06-27feat(heartbeat): show next heartbeat timer + backend handoff for timestamp ↵Adam Malczewski
endpoint
2026-06-26style: switch from tabs to 2-space indentation (incl. svelte)Adam Malczewski
2026-06-26feat(heartbeat): default system prompt to workspace default + reset buttonAdam Malczewski
2026-06-26feat(heartbeat): prompt editor modal + hours/minutes timer + backend handoffAdam Malczewski
2026-06-26feat(heartbeat): heartbeat view with config, run list, live chat modal, and ↵Adam Malczewski
sidebar wiring
2026-06-25feat(computer): SSH computer selection + status + workspace default (handoff #2)Adam Malczewski
Mirrors the cwd/workspace UI for the SSH-computer feature: - New feature library src/features/computer/: - logic/view-model.ts (pure): viewComputer/viewComputerStatus/viewTestResult/ summarizeComputers/formatHost/knownHostLabel + state->badge for the 4 ComputerStatusResponse states + SaveComputer/LoadComputerStatus/ TestComputer/LoadComputers ports. 20 view-model tests. - ui/ComputerField.svelte: per-conversation selector (dropdown + connection-status badge + Test-connection, polling the selected alias). - ui/ComputerSelect.svelte: reusable Local/computers dropdown, shared with the workspace default-computer control. - AppStore: computerId state + refreshComputer (at every focus site, parallel to refreshCwd) + setComputer (PUT /conversations/:id/computer, null=clear) + global computers catalog (GET /computers on boot, like models) + computerStatus(alias) + testComputer(alias). chat.send UNCHANGED (resolved server-side like cwd). - App.svelte: ComputerField in the Model sidebar view next to CwdField; adapted ports wrap the store. - workspaces: setDefaultComputer on WorkspaceHttp+WorkspaceStore (PUT /workspaces/:id/default-computer); default-computer selector in WorkspaceCard (reuses ComputerSelect); router passes store.computers through. - Re-mirrored .dispatch/transport-contract.reference.md (Computers section + ChatRequest.computerId); updated .dispatch/wire.reference.md (Computer/ ComputerEntry/defaultComputerId + the provider-retry divergence note from handoff #1); GLOSSARY + backend-handoff.md (handoff #2, §2e). Transparency invariant: the computer is USER-facing only (a tool-execution target, never part of the model prompt); the agent never sees it. Verify: 795/795 tests green; biome clean; vite build succeeds; 0 typecheck errors from the computer feature. (11 pre-existing svelte-check errors remain from the open §2d provider-retry divergence — backend feature/ssh-support still lacks TurnProviderRetryEvent; not from this feature.)
2026-06-25feat: workspaces shell + cwd-lsp rename + mcp/settings/system-prompt ↵Adam Malczewski
features + app wiring - workspaces: URL-driven conversation grouping (home listing at /, routing, store, http adapter, WorkspaceCard) wired into the App.svelte shell - rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature) - new features: mcp (status view), settings (chat-limit field), system-prompt (prompt builder), all rendered via the generic surface host - chat: store + ChatView updates - tabs: tabs-store updates - app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte This commit makes HEAD typecheck clean for the first time: the prior HEAD (c95cc77) imported features/settings from app/App.svelte but never committed the feature, so only the full working tree was green.
2026-06-22feat: double-click tab to rename (PUT /conversations/:id/title)Adam Malczewski
Double-click a tab's title to enter inline edit mode. Enter or click away (blur) saves; Escape cancels. The rename is optimistic — the local tab updates immediately and PUT /title fires in the background. 683 tests green.
2026-06-22feat: consume context window + percentage-based compact handoffAdam Malczewski
1. Real context window: GET /models now returns modelInfo[model].contextWindow. The Composer uses this instead of the hardcoded MAX_CONTEXT = 1,000,000. Falls back to 1M when modelInfo is absent or the model has no contextWindow. 2. Percentage-based auto-compact: the compact-threshold endpoint is renamed to compact-percent. The CompactionView now shows a percent input (0-100, default 85, 0 = manual) instead of a token count input. Types renamed: CompactThresholdResponse → CompactPercentResponse, SetCompactThresholdRequest → SetCompactPercentRequest. Note: the field name in the backend types is still 'threshold' (not 'percent') — the FE maps between them. Re-mirrored .dispatch/transport-contract.reference.md. 686 tests green. 0 svelte-check errors + warnings.
2026-06-22feat: persist sidebar layout + open/closed state between refreshesAdam Malczewski
Sidebar panel layout (which views are open and their order) and the sidebar open/closed toggle are now persisted to localStorage. Default layout is just the Model view at the top. - ViewSidebar accepts an onChange callback that reports panel kinds - App.svelte creates two createLocalStore instances (dispatch.sidebar.views + dispatch.sidebar.open) using the store's storage adapter - AppStore exposes its storage instance so the shell persists via the same adapter (test-injectable, not globalThis.localStorage) - Tests pre-populate fake storage with ["extensions"] for the 4 tests that need the Extensions view visible 686 tests green. 0 svelte-check warnings (2 pre-existing errors from missing transport-contract exports, unchanged).
2026-06-22feat: show 5-char git hash next to hamburger for cache-bust detectionAdam Malczewski
Vite define bakes __APP_VERSION__ (git rev-parse --short=5 HEAD) at build time — survives bundling into the arch package deploy. Falls back to 'dev' when not in a git repo. Also fixes two noNonNullAssertion warnings in place.ts.
2026-06-22feat(chat): stop generation button — abort without closingAdam Malczewski
Consume the stop-generation handoff (no version bumps, no new types). - App store: stopGeneration() → POST /conversations/:id/stop (fire-and-forget) - Composer: stop button (square, error color) visible only while generating, next to the send/queue button - Existing event flow handles the rest: done with reason 'aborted' clears generating; conversation.statusChanged: idle updates the tab spinner 686 tests green.
2026-06-22feat(compaction): conversation compacting + auto-compact thresholdAdam Malczewski
Consume the compaction handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - New 'Compaction' sidebar view (CompactionView.svelte): - 'Compact now' button → POST /conversations/:id/compact (loading indicator + result: 'N messages summarized, M kept') - Auto-compact threshold number input → GET/PUT /conversations/:id/compact-threshold (0 = disabled, default 350000) - Re-mounts per conversation via {#key} - App store: compactNow() + compactThreshold reactive state + setCompactThreshold(), seeded on focus change (like reasoning-effort + cwd) - conversation.compacted WS handler: reloads the SAME conversation's history (ID unchanged — old history forked to an archive, not a tab switch) - WS adapter parses newConversationId field on ConversationCompactedMessage - conformance guards + tests cover the new type 686 tests green.
2026-06-22feat(tabs): cross-device tab sync via conversation lifecycleAdam Malczewski
Consume the conversation lifecycle handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - fetchOpenConversations() on connect: GET /conversations?status=active,idle restores the tab bar across devices (merges with localStorage — opens new tabs, removes closed ones, updates titles from backend) - conversation.statusChanged WS handler: closed → removeTabLocally (no re-POST); active → open tab + spinner; idle → update status map - conversation.compacted WS handler: dispose stale store + cache, reload history from server - TabBar shows a spinner on active conversations (statusFor prop) - closeTab refactored to use removeTabLocally (extracted cleanup) - conformance guards + WS adapter tests cover all 3 new WsServerMessage types 686 tests green.
2026-06-21fix: conversation.open opens tab without auto-switchingAdam Malczewski
CLI --open should add the tab to the strip but leave the user on their current tab. Add openTab reducer (add without selecting) + use it in openConversation instead of createTab+selectTab.
2026-06-21feat(ws): handle conversation.open broadcast — open/focus tab from CLI --openAdam Malczewski
Consume the conversation.open handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - WS adapter (logic.ts + index.ts): parse + route the new top-level "conversation.open" WsServerMessage to an onConversationOpen handler - app store: openConversation(id) opens (or focuses) a tab — creates a chat store, loads history, subscribes to live turns, creates+selects the tab - conformance guard + WS adapter tests cover the new type - backend also shipped conversation metadata endpoints (GET /conversations, GET /conversations/:id/last, GET/PUT /conversations/:id/title) — mirrored but not yet consumed by the FE 682 tests green.
2026-06-21feat(surfaces): todo task list sidebar viewAdam Malczewski
Add a dedicated "Tasks" sidebar view for the per-conversation todo surface (model-maintained via todo_write tool; read-only, conversation-scoped). - parseTodoPayload: pure parser for the rendererId: "todo" custom field (TodoItem { content, status, priority } — types defined FE-side, not in wire) - TodoList.svelte: renders the task list with status indicators (spinner for in_progress, checkmark for completed, X for cancelled, empty circle for pending) + priority dots (red/yellow/gray) - SurfaceView dispatches rendererId: "todo" to TodoList - App.svelte: "Tasks" view kind (always visible; "No tasks yet" empty state), todo surface pulled out of the generic Extensions list, re-mounts per conversation via {#key} 681 tests green.
2026-06-21feat(chat): message queue + steering — mid-turn injection at tool-result ↵Adam Malczewski
boundaries Consume the message-queue + steering handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - fold steering AgentEvent into the transcript as a provisional user bubble (after the tool-result it followed; no de-dup — the queue surface carried it) - add rendererId: "message-queue" custom renderer (pure parser + MessageQueueList) rendered as a compact panel above the Composer (hidden when queue is empty) - add ChatStore.queueMessage / AppStore.queueMessage — sends chat.queue WS op (trim/validate non-empty; auto-starts a turn if idle) - Composer switches to chat.queue while generating (button → Queue, placeholder → Steer the conversation...) - exhaustiveness guards updated for steering + chat.queue - carry-to-new-turn needs no special handling (normal new turn) 664 tests green.
2026-06-19fix: silence state_referenced_locally warning + sync draft model on catalog ↵Adam Malczewski
fetch - Use DEFAULT_MODEL directly when creating the initial draft store instead of reading the $state activeModel in a non-reactive context (init-time read only captures the initial value — semantically identical since activeModel === DEFAULT_MODEL at that point, but silences the Svelte state_referenced_locally warning). - Sync the draft store's model when the model catalog fetch corrects activeModel (default not in catalog). Without this, chat.send would use the stale DEFAULT_MODEL while the UI showed the corrected model.
2026-06-12feat(chat): reasoning-effort selector — sticky per-conversation ↵Adam Malczewski
thinking-depth knob Consume the backend's reasoning-effort handoff ([email protected] ReasoningEffort + [email protected] GET/PUT /conversations/:id/reasoning-effort, ChatRequest.reasoningEffort): a 5-level selector in the sidebar Model view, under the provider + model dropdowns. null renders as 'high (default)' per the server-owned resolution chain; PUT on change (effective next turn); error + revert on 400; per-conversation re-mount incl. drafts (the draft id survives promotion, so an effort set on a draft applies from turn 1). Re-mirrored .dispatch references; GLOSSARY 'reasoning effort'; handoff updated. 616 tests green; live curl probe passed.
2026-06-12feat(chat): consume CR-5 history windowing — server-windowed cold loads + ↵Adam Malczewski
show-earlier backfill Re-pinned [email protected]>0.10.0 + [email protected]>0.6.1 (reply frontend-history-windowing-handoff.md); re-mirrored both .dispatch references. - HistorySync port gains optional { limit?, beforeSeq? } (CR-5 params); the app's createHistorySync appends them to GET /conversations/:id. - COLD-cache fresh load now fetches ?sinceSeq=0&limit=<floor(0.75xL)> — a huge conversation no longer ships whole to show 192 chunks. A warm-cache tail sync stays unwindowed (windowing a tail that outgrew the limit would leave a silent seq gap behind the cache). - hasEarlier now derives from the [email protected] CONTRACT (1-based gap-free seqs): loaded window starting above seq 1 => older history exists — covering both locally-trimmed AND server-windowed transcripts (the watermark stays as the merge floor only). - showEarlier(): local cache first; when the cache doesn't reach far enough back, backfills the missing older run via ?beforeSeq=<oldestKnown>&limit= and persists it (next page-in is local). latestSeq windowed-read caveat is satisfied structurally (tail cursor derives from the cache's max seq). - live-probe: +6 CR-5 checks (seq origin, newest-k ascending, short-chat exactness, beforeSeq paging, 400 validation x2). NOT yet run live — backend was down at commit time; run pending. - backend-handoff.md: CR-5 RESOLVED, pins/mirrors current. 602 tests green x2.
2026-06-12feat(chat): chat limit — bulk quarter-unload, 75% fresh-load window, ↵Adam Malczewski
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