summaryrefslogtreecommitdiffhomepage
path: root/src/features
AgeCommit message (Collapse)Author
7 daysfeat: 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.
7 daysfeat: 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.
7 daysfeat: 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).
7 daysfeat: collapsible tool output (collapsed by default like thinking)Adam Malczewski
Tool calls and results now use the same DaisyUI collapse pattern as thinking blocks — collapsed by default, click to expand. Each card shows the tool name + a wrench icon in the title; expanding reveals the input/output with overflow-x-auto for long lines and max-h-96 overflow-y-auto for very long output. Batched tool calls: each entry is its own collapse card (was a DaisyUI list). Pending results show a spinner in the title. Errors show a red badge. 686 tests green.
7 daysfix: tool output scrolls within its container instead of bleeding outAdam Malczewski
Add overflow-hidden to tool card containers and overflow-x-auto to <pre> elements so long tool output (file contents, JSON, etc.) gets its own scrollbar instead of expanding the chat width and creating a horizontal scrollbar on the entire transcript. 686 tests green.
7 daysfix: trim provisional chunks during long turns (browser stays responsive)Adam Malczewski
trimTranscript now drops oldest provisional chunks (the in-flight turn) when committed chunks are exhausted. Previously it bailed with drop=0 when committed was empty, allowing unbounded provisional growth during long generating turns (300+ chunks → browser crawls). Root cause of the syncTail approach failing: the kernel emits step-complete (line 360) BEFORE calling onStepComplete (line 542) — chunks are persisted only after tool results come back, not when step-complete fires. So syncTail on step-complete found nothing. Reverted the applyHistory + syncTail-on-step-complete changes from 4e1d041. The new approach is simpler: trim provisional directly in trimTranscript. Dropped chunks are lost temporarily (no Show Earlier) but come back as committed when the turn seals and syncTail fetches everything from the server. 686 tests green.
7 daysfeat: trim chunks during generation via step-complete syncTail (CR-6)Adam Malczewski
The backend now persists chunks at step boundaries during generation (CR-6). The FE calls syncTail on each step-complete event to fetch the newly committed chunks. applyHistory clears the provisional array when new committed chunks arrive mid-generation (they're duplicates of what was folded from live events). The accumulating chunk (current in-progress step) is kept. This means trimTranscript can now drop oldest committed chunks uniformly during a long turn — no unbounded provisional growth. The browser never holds more than chatLimit chunks, even mid-generation. 3 new tests: clears provisional on new committed during generation, keeps provisional when no new chunks, keeps accumulating when clearing. 689 tests green.
7 daysfeat(metrics): show turn number in metrics bubble (turn N · ...)Adam Malczewski
The turn number comes from the entry's position in the metrics array (1-based), which is correct regardless of trimming since stepId matching aligns segments to the right entry. Now displays 'turn 3 · 12k tok' instead of just 'turn · 12k tok'.
7 daysfix(metrics): skip unanchored step-metrics — no more empty bubbles at tailAdam Malczewski
Step-metrics are only shown when anchored to their tool content (inline after the tool-call/result group). Steps whose chunks were trimmed (or text-only steps with no tool chunks) are now SKIPPED instead of piling up at the segment tail as empty 'step N · X tok' bubbles. The turn-total metrics row still shows the aggregate (tokens, duration, cache rate), so the conversation-level summary is preserved. Updated place.test.ts + ui.test.ts to anchor steps with tool-call groups where step-metrics are expected.
7 daysfix(composer): single context-aware button — Send/Queue/StopAdam Malczewski
One button to the right of the text input: - idle → Send (starts a turn) - generating + text → Queue (steers via chat.queue) - generating + empty → Stop (aborts via POST /stop)
7 daysfeat(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.
7 daysfeat(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.
7 daysfeat(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.
7 daysfix: 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.
8 daysrefactor(todo): drop priority field — keep the tool simpleAdam Malczewski
Backend removed priority from TodoItem; FE parser + renderer updated to match (status-only: pending/in_progress/completed/cancelled).
8 daysfix(todo): use static filled circle for in_progress instead of spinnerAdam Malczewski
8 daysfeat(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.
8 daysfeat(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-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
2026-06-12feat(cache-warming): consume CR-4 lifecycle — tab-close cancel + ↵Adam Malczewski
scope-aware subscriptions - closeTab now POSTs /conversations/:id/close (abort in-flight turn + stop/disable warming server-side); disconnect still leaves both running ([email protected]) - syncSubscriptions honors catalog scope ([email protected]): global surfaces are not re-subscribed on conversation switch - fix(ws): the surface-message parser dropped the conversationId echo (CR-4d was ours, not the backend's) — preserved + unit-tested - secondsUntilNext: 3s stale guard — a past nextWarmAt renders as waiting, not 0s - re-pinned + re-mirrored [email protected] / [email protected] - scripts/probe-cache-warming.ts: live CR-4 probe (default-off, future nextWarmAt, repeated warms, mid-turn close abort, idempotent re-close) — 17/17 against bin/up
2026-06-12feat(chat): multi-client live view — watch in-flight turns + user prompt ↵Adam Malczewski
on stream - subscribe every open conversation on load + WS reconnect (resync), unsubscribe on tab close - derive a stream-based 'generating' state for watchers (Composer running indicator) - fold the user-message turn event so watchers render the prompt mid-turn (de-dup vs sender's optimistic echo) - re-pin [email protected] / [email protected]; re-mirror contracts; add user-message to the exhaustiveness guard
2026-06-12feat(chat): old-Dispatch composer layout — textarea + send + status barAdam Malczewski
Restore the ergonomic composer from old Dispatch: an auto-resizing textarea (1→7 lines) with a fixed-width Send button beside it, and a status bar BELOW holding a status icon · context-window fill bar (escalating success/warning/ error color) · compact token count (current / limit · pct%). The bar reuses the latest turn's contextSize as current usage and HARDCODES a 1,000,000-token window limit as a placeholder (real per-model limit is the next backend ask). Absorbs the standalone ContextSizeBadge (removed). Pure helpers computeContextUsage + formatCompactTokens added to core/metrics (tested). 540 tests green.
2026-06-12feat(metrics): consume contextSize — current context-usage readoutAdam Malczewski
Backend context-size handoff: re-pin [email protected] / [email protected] (+ re-mirror .dispatch reference snapshots). Thread the optional contextSize through core/metrics (done fold + durable + selectCurrentContextSize: latest turn's defined value, undefined=>unknown never 0, durable-wins-over-live). Chat store exposes currentContextSize; ContextSizeBadge renders "N tokens in context" / "context size unknown" above the composer. GLOSSARY: add context size / context window. 533 tests green.
2026-06-12feat(workspace,smart-scroll): per-conversation cwd + LSP view; smart auto-scrollAdam Malczewski
workspace ([email protected]): a cwd field in the Model sidebar view (GET/PUT /conversations/:id/cwd) + a new 'Language Servers' view (GET /conversations/:id/lsp) with per-server connected/starting/error badges, spinner, error text, and refresh. Store-owned reactive cwd, re-seeded on focus change; works for DRAFTS too (targets the draft's client-minted id, which survives promotion, so turn 1 runs in the chosen cwd). Network seam normalizes the untyped LSP body. smart-scroll: pure stick-to-bottom reducer + injected controller shell (scroll/scrollend + a ResizeObserver on the content so the view follows async height changes — markdown/highlight, images, collapses, viewport reflow), plus a floating scroll-to-bottom button. FIX: restore the transcript scrollbar — the refactor moved overflow-y-auto to an inner child, so the flex-1 container needed min-h-0 to constrain instead of growing to content. harness: vitest-setup polyfills Element.scrollTo + ResizeObserver (jsdom implements neither), fixing App component tests. docs: backend-handoff pruned (CR-3 resolved/removed); added cwd/LSP verification courier (backend confirmed all 6 asks ✅); removed the resolved cache-warming-timer courier. Verified: svelte-check 0 errors, biome clean, 523 tests pass, vite build OK.
2026-06-11feat(cache-warming,surfaces,metrics,markdown): conversation-scoped surfaces, ↵Adam Malczewski
cache warming + retention, markdown Consumes the backend cache-warming + cache-rate handoffs end-to-end and adds supporting infra: - protocol/transport: conversation-scoped surfaces (conversationId on subscribe/invoke/surface + staleness routing); store auto-subscribes the catalog with the focused conversation and re-scopes on switch. - surface-host: generic Number field renderer + custom rendererId dispatch (graceful skip on unknown). - cache-warming feature: enabled toggle, min+sec interval, AUTHORITATIVE countdown from the surface's cache-warming-timer nextWarmAt, manual Warm now (POST /chat/warm), lastWarmAt-keyed history, cache-retention stat, expectedCacheRate headline. - metrics: cross-turn expected-cache (retention) derivation + bubble badge; cache-rate fix needs no code change (inputTokens now total). - markdown feature: marked + marked-highlight + highlight.js + dompurify, rendered in ChatView. - fixes (gemini review): {#key activeConversationId} remount of CacheWarmingView to stop history/feedback leaking across tabs; guard NaN interval inputs from committing 0. - docs/contracts: regenerated transport/ui-contract mirrors; backend-handoff updated (CR-3 resolved). Verified: svelte-check 0 errors, biome clean, 494 tests pass, vite build OK.
2026-06-10feat(chat,app): Model view in sidebar + split key/model selectorsAdam Malczewski
- move the model picker out of the chat header into a dedicated "Model" sidebar view; sidebar now seeds two default panels (Model on top, Extensions below) - split the single model dropdown into two stacked selects: a key selector (distinct credential keys) + a model selector (models under the current key) - pure model-select helpers (splitModelName/joinModelName/modelKeys/modelsForKey), split on the FIRST slash so multi-slash model names stay intact - onSelect still emits the full `<key>/<model>` string (ChatRequest.model unchanged)
2026-06-10feat(views,surface-host): Extensions sidebar view — auto-expanded surfaces ↵Adam Malczewski
+ tables views (new feature): - pure panel-stack reducer + thin generic ViewSidebar (dropdown picker + add/remove), switches on view KIND, never a surface id Extensions view (composition root): - folds frontend modules + backend surfaces into one "Extensions" view - frontend module list AGGREGATED from each feature's public `manifest` export (can't drift); no per-module version (FE features are internal to dispatch-web) - surfaces are AUTO-SUBSCRIBED on catalog + rendered expanded (no catalog buttons) surface-host: - consecutive `stat` fields coalesce into one aligned label/value table (StatTable) - generic custom-field renderer: dispatch on rendererId === "table" → SurfaceTable (pure parseTablePayload), so a backend `custom`/table field renders generically - shared presentational components/Table.svelte (used by both, neither feature depends on the other) store: - auto-subscribe every catalog entry, unsubscribe vanished ones, re-subscribe all on reconnect; expose all received specs via `surfaces` (drops single-selection) backend-handoff: CR-1 — emit Loaded Extensions as a custom/table field; notes what's already covered FE-side (renderer shipped, stat-table fallback works).
2026-06-10feat(tabs,app): tab id handles, fixed-width tabs-lift, slim shell + ↵Adam Malczewski
full-height sidebar Tabs: - short-handle ID badge per tab (shortest unique conversationId prefix, min 4) - fixed-width (w-48) tabs with tabs-lift folder borders Shell (composition root): - drop the Dispatch title bar; tabs sit at the very top with a 5px gap - big faded "Dispatch" watermark centered on an empty chat - collapsible right sidebar (empty shell) spanning full window height: a permanently right-pinned hamburger in the tab row toggles it; in-flow push that shrinks the whole left column (tabs included) at >=lg, overlay + backdrop below lg; open-by-default on wide / closed on narrow - main is overflow-hidden with a min-w-0 shrink chain; app.css pins html/body/#app height + body overflow hidden so the page never overflows
2026-06-10feat(metrics): inline cache hit-rate badges (last turn + chat total)Adam Malczewski
Derive cache hit rate (cacheReadTokens / inputTokens) from data already folded in core/metrics — no backend/contract change. - core/metrics: computeCachePct + viewCacheRate (pct + success/warning/error level by 66/33 thresholds + isHit); thread a running cumulativeUsage onto each finalized turn-metrics row for the conversation total. - ChatView: render two labelled, colour-coded percentage badges in the turn-total bubble — "Last turn:" (that turn) and "Chat Total:" (cumulative). - Honour backend caveats: absent cache fields -> 0, divide-by-zero guarded, a legitimate 0% renders plainly (not "no data").
2026-06-10feat(metrics): per-turn + per-step token/timing metrics bubblesAdam Malczewski
Consume [email protected] / [email protected] metrics: usage.stepId, step-complete (ttft/decode/genTotal), done.durationMs/usage, and the durable GET /conversations/:id/metrics endpoint. - core/metrics: pure live-fold + durable-merge reducer; decode-rate TPS; head-aligned, stable placement; progressive per-step rows (each shown as its step ends) with the turn-total row gated on the done event. - features/chat: store folds metric events + hydrates durable TurnMetrics; ChatView renders inline step bubbles + a turn-total bubble. - app: MetricsSync HTTP effect (tolerates 404) injected into chat stores. - scripts/live-probe: drives the metrics path; live-verified 17/17 vs bin/up. - docs: regenerate .dispatch wire/transport mirrors to 0.4.0; glossary terms (turn/step metrics, TTFT, decode time, TPS, metrics bubble); trim handoff.
2026-06-07Revert "feat(chat): live turn metrics — telemetry reducer + rendering"Adam Malczewski
This reverts commit 48c6d85c3cc5a57a729f14068e2346b17ed62088.
2026-06-07feat(chat): live turn metrics — telemetry reducer + renderingAdam Malczewski
Consume wire/transport-contract 0.3.0 (step-complete event + timing fields on usage/tool-result/done). Pure core/telemetry module: foldMetricEvent (reducer) + derived selectors (stepTps, turnTps, etc). TelemetryState is pure data, no active-turn tracking — consumers pass turnId to selectors. ChatStore wires foldMetricEvent into handleDelta and exposes telemetry + currentTurnId. ChatView shows step-metrics footer (time/TPS/tokens) on assistant text bubbles and durationMs badge on tool cards. New TurnSummary component renders turn-level stats (wall-clock, tokens, steps, TPS) in a DaisyUI stats block. Extended live-probe to verify telemetry events against bin/up (pending backend restart). 336 tests, typecheck 0, biome clean, build ok.
2026-06-07feat(chat): restyle thinking — visible bubble, collapse, title swap, ↵Adam Malczewski
persisted open Thinking renders inside a visible rounded-card bubble (like tool calls), capped to the same max-w-5xl column as assistant text. Uses a DaisyUI checkbox collapse (no arrow/plus icon) with smooth animation. Title reads "Thinking" + loading-dots while the model is actively generating, then flips to "Thoughts" with no dots once done. Open/closed state persists across the generating→completed→sealed transition via stable ordinal keys (per-conversation isolation via {#key} in App). Added optional streaming flag to RenderedChunk (pure selector, only on the accumulating chunk).
2026-06-07feat(chat): drop the faded opacity on in-flight messagesAdam Malczewski
Remove the opacity-50 dimming applied to provisional (streaming) chunks across user/assistant/tool/batch rendering; in-flight content now renders at full opacity. Test updated to assert no dimming.
2026-06-07feat(chat): group batched tool calls into one DaisyUI listAdam Malczewski
Consume the backend's new stepId grouping key (wire/transport-contract 0.1.0 -> 0.2.0). foldEvent copies event.stepId onto live tool chunks so live and replay group identically. New pure selector groupRenderedChunks (core/chunks) folds a step's 2+ tool calls into one tool-batch group, pairing each call with its result by toolCallId; single/no-stepId calls stay as cards. ChatView renders a batch as a DaisyUI list (list-row per pair). Fixtures updated for the now-required event stepId.
2026-06-07feat(chat): restyle transcript — left-aligned, bubbleless assistant, tool ↵Adam Malczewski
cards All messages flow left in one column via the DaisyUI chat-start grid: - user keeps a primary speech bubble; - assistant/system/error render in a transparent (invisible) chat-bubble so they read as plain prose yet inherit identical left spacing, capped to a readable max-w-5xl column; - tool call/result render as regular (non-speech) rounded cards, nested in the same grid so they line up too; - role header labels dropped; chat-wide left padding added. Alignment uses specificity-based variants (no !important).
2026-06-07feat(tabs): polish new-chat button — stuck-only square edge, New Chat labelHEADmainAdam Malczewski
- isStuckToEnd (pure): square the sticky '+' right edge only while it floats over scrolled tabs; rounded at rest. Edge-measured in TabBar via a disposed scroll + ResizeObserver effect (RO guarded for non-browser envs). - Show a temporary 'New Chat' title when the draft is selected, with the '+' moved to the trailing close-button slot for consistency with real tabs.
2026-06-07feat(tabs): extract TabBar component with horizontal scroll + sticky end '+'Adam Malczewski
Move inline tab-bar markup from the composition root into a thin presentational TabBar in the tabs feature (feature-as-a-library: pure reducer -> reactive store -> UI). Adds overflow-x scroll (min-w-max strip) and a sticky right-pinned new-chat '+' that floats over scrolling tabs. Draft-on-select / create-on-send behavior unchanged.
2026-06-07fix: optimistic user message echo + tabs persistenceAdam Malczewski
Bug 1 (sent message didn't appear until turn end): the transcript only folded assistant AgentEvents, so the user's own message showed only after turn-sealed resync. Add core/chunks appendUserMessage() (provisional user chunk, superseded on history sync) and call it in chat send() — the message now renders instantly. Bug 2 (tabs didn't persist on refresh): the app passed { storage: undefined } to createLocalStore, which the adapter treats as a no-op store, so nothing was saved. Default to globalThis.localStorage. Regression test exercises the non-injected path. Also updated app store tests for the echo (assistant-vs-user chunk filtering). Verified: svelte-check 0/0, vitest 288 (stable x2), biome clean, build ok.
2026-06-07Slice 3 wave A: tabs model, model selector, cache delete, localStorageAdam Malczewski
- features/tabs: pure tab-workspace reducer (create/select/close/setModel/ setTitle/deriveTitle, draft=null active) + injected-persistence runes store - features/chat: mutable per-tab model (setModel) + delta routing guard (ignore foreign conversationId) + ModelSelector.svelte + DaisyUI chat bubbles / composer (keeps streaming <details> keying fix) - features/conversation-cache: surface delete(conversationId) on the wrapper for tab-close local-forget - adapters/local-storage: generic injected JSON localStore<T> (quota/corrupt-safe) Verified: svelte-check 0/0, vitest 273, biome clean, build ok.
2026-06-07fix(chat): keep thinking <details> open while streamingAdam Malczewski
ChatView keyed the transcript each-block by object identity, but core/chunks returns new RenderedChunk objects per delta, so Svelte recreated each <article>/<details> every frame — an opened Thinking element snapped shut on the next token. Key by stable identity instead (c${seq} for committed, p${i} for append-only provisional) so streaming reuses the DOM. Adds a regression test that the <details> stays open across a streaming update. Verified: svelte-check 0/0, vitest 222, biome clean, build ok.
2026-06-07Slice 2 wave 2: IndexedDB cache adapter + chat featureAdam Malczewski
- adapters/idb: createIdbChunkStore implements the ConversationChunkStore port over IndexedDB (compound [conversationId,seq] key, idempotent append, meta store for lastAccess); 8 tests with fake-indexeddb - features/chat: createChatStore (runes-thin over the core/chunks reducer, all effects injected via ChatTransport/HistorySync/ConversationCache ports) + ChatView/Composer svelte-thin UI; folds chat.delta, syncs on turn-sealed, hydrates from cache then catches up; 25 tests Verified green: svelte-check 0/0, vitest 202, biome clean, build ok.
2026-06-07Slice 2 wave 1: transcript reducer, wire conformance, ws chat, cache coreAdam Malczewski
- core/chunks: the one pure transcript reducer (foldEvent live deltas + applyHistory seq-keyed reconcile + selectChunks/selectMessages); 27 tests - core/wire: FE-side contract-conformance exhaustiveness guards + drift smoke tests over wire/transport-contract unions (§2.9 drift signal); 10 tests - adapters/ws: additively multiplex chat.send/chat.delta/chat.error on the existing surface socket (onChat + widened send); surface API unchanged - features/conversation-cache: pure reconcileCache/nextSinceSeq/selectEvictions + ConversationChunkStore port + injected createConversationCache; 26 tests Verified green: svelte-check 0/0, vitest 169, biome clean, build ok.
2026-06-06Slice 1: surface system + WS transport + composition rootAdam Malczewski
Pure-core feature libraries assembled at the composition root: - core/protocol: pure reducer over surface catalog/spec/error messages - features/surface-host: generic field-kind interpreter (toggle/progress/ selector/stat/button) + pure plan logic; no surface-id special-casing - adapters/ws: injected WebSocket client (effects at the edge) - app: composition root store (Svelte 5 runes over the pure reducer), host-relative surface WS URL resolution (resolveWsUrl), root App.svelte Verified green: svelte-check 0/0, vitest 84 passed, biome clean, vite build ok.