| Age | Commit message (Collapse) | Author |
|
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.
|
|
against bin/up
|
|
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.
|
|
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
|
|
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
|
|
One main agent does planning, contracts, and the feature writing — no summoning. Removes ORCHESTRATOR.md and the now-obsolete summoning artifacts (.dispatch/package-agent.md + .dispatch/rules/frontend-*.md, whose code rules are restated in AGENTS.md); keeps the contract mirrors + daisyui reference. Fixes dangling refs in README.md and backend-handoff.md.
|
|
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
|
|
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
- 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)
|
|
+ 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).
|
|
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
|
|
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").
|
|
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.
|
|
The backend's AgentEvent union now includes TurnStepCompleteEvent
(wire/transport-contract 0.3.0). Add: no-op case in foldEvent
(transcript reducer ignores timing metadata), exhaustiveness case
in assertAgentEventExhaustive, and a step-complete sample in the
conformance test (now 12 variants).
|
|
This reverts commit 48c6d85c3cc5a57a729f14068e2346b17ed62088.
|
|
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.
|
|
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).
|
|
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.
|
|
Extend scripts/live-probe.ts with a second turn that elicits parallel
tool calls and asserts: live tool events carry stepId, replayed chunks
carry chunk.stepId, and groupRenderedChunks folds the batch identically
live and on replay. Deltas now routed by conversationId. Gated (not in
bun run test). Verified 13/13 against bin/up.
|
|
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.
|
|
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).
|
|
- 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.
|
|
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.
|
|
- add .dispatch/rules/frontend-styling.md (DaisyUI v5 + dracula; thin components)
- package-agent: verify section explains whole-project svelte-check under
parallel builds (judge only YOUR files)
|
|
Fold in the working practices that were implicit:
- §2a Waves: disjoint+no-compile-dep batching; widen waves by removing edges;
parallel = N summon calls in ONE message (resolves the no-background tension)
- §3: pre-author the cross-unit seam; tell agents about concurrent siblings;
make agents implement (not deliberate)
- §4: whole-project-check concurrency caveat + double-run for shared-global flakiness
- §5a Recovery: plan-only agent re-summon; sibling test fan-out; lane-stray
handling; flaky green
- §9 Live integration probe (gated scripts/live-probe.ts; backend is user's process)
- §10 Living backend-handoff.md as the cross-repo courier channel
- generalize contract mirroring to all .dispatch/*.reference.md; re-read rules
before each wave (a miss this session)
|
|
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.
|
|
|
|
- store.svelte.ts: tabs store over injected localStorage; one chat store per
conversation (Map); single WS routes chat.delta/error by conversationId;
draft (null active) mints a conversationId and becomes a tab on first send
(title from deriveTitle); GET /models catalog; default model flash; close tab
= dispose + cache.delete (local forget) + neighbour activation; restore tabs
from storage + load() on construct
- App.svelte: DaisyUI tab strip (+ / close), model selector, chat, surfaces
- AppStore: tabs/activeConversationId/activeChat/models/activeModel +
send/selectModel/newDraft/selectTab/closeTab; +localStorage inject opt
Verified: svelte-check 0/0, vitest 281 (stable x2), biome clean, build ok.
|
|
- 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.
|
|
- add tailwindcss@4 + @tailwindcss/vite + daisyui@5; vite tailwind plugin
- src/app.css: @import tailwindcss + @plugin daisyui { themes: dracula --default }
(theme BUNDLED, not just named); imported in main.ts
- index.html: <html data-theme="dracula">
- biome.json: enable css tailwindDirectives so @plugin parses
- GLOSSARY: define FE term 'tab' (workspace slot referencing a conversation;
holds conversationId + model + title; close = local forget)
Verified: dracula tokens bundled in CSS, svelte-check 0/0, vitest 222, biome
clean, build ok.
|
|
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.
|
|
|
|
- scripts/live-probe.ts: gated bun harness driving the real FE stack
(adapters/ws + core/chunks + conversation-cache + adapters/idb + HTTP
history) against bin/up; not part of `bun run test`
- backend-handoff.md: record the 9/9 live result; no backend mismatch
|
|
crypto.randomUUID() is secure-context-only — undefined on plain-HTTP
non-localhost origins (e.g. http://arch-razer:24204), so createAppStore threw
during mount and nothing rendered. Add src/app/uuid.ts randomId(): prefer
crypto.randomUUID when present, else build a v4 from crypto.getRandomValues
(available in insecure contexts), else Math.random fallback. Use it for the
conversation id.
Verified: svelte-check 0/0, vitest 221, build ok.
|
|
|
|
- app/store.svelte.ts: one WebSocket carries surfaces AND chat (onChat ->
chatStore.handleDelta); build the conversation cache over the IndexedDB
adapter; createChatStore wired to transport (socket.send), injected HTTP
historySync, and the cache; load() on construct
- app/resolve-http-url.ts: host-relative HTTP base (port 24203), mirrors
resolve-ws-url; injected fetch
- App.svelte: render ChatView + Composer alongside the surface picker
- createAppStore gains optional injection points (httpUrl/fetchImpl/indexedDB/
conversationId) for tests
- vitest-setup.ts: fake-indexeddb/auto for jsdom IndexedDB (orchestrator-owned
config; agent change adopted)
Verified green (x2, stable): svelte-check 0/0, vitest 218, biome clean, build ok.
Slice 2 (conversation transcript: cache + delta streaming) feature-complete.
|
|
- 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.
|
|
- 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.
|
|
- pin @dispatch/wire + @dispatch/transport-contract (+ ui-contract) as file:
deps @0.1.0; overrides{} workaround for their workspace:* deps (bun can't
resolve workspace: from outside the monorepo)
- add fake-indexeddb (dev) for the upcoming IndexedDB adapter tests
- mirror wire + transport-contract into .dispatch/*.reference.md for headless
agents; point package-agent.md at all three references
- backend-handoff.md: convert to a living FE<->backend seam doc
Verified green: svelte-check 0/0, vitest 91, biome clean, build ok; contract
import smoke-test passes.
|
|
- vite: add @testing-library/svelte's svelteTesting() plugin so component
render()/mount() resolves Svelte's browser build under vitest/jsdom
- dep: @testing-library/user-event for realistic interaction tests
- app: 7 component-render tests driving App.svelte through a fake socket
(catalog render, subscribe-on-click, unsub/sub ordering, aria-current,
error banner, action invoke)
Verified green: svelte-check 0/0, vitest 91 passed, biome clean, vite build ok.
|
|
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.
|