| Age | Commit message (Collapse) | Author |
|
# Conflicts:
# backend-handoff.md
|
|
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).
|
|
active chats
|
|
# Conflicts:
# .dispatch/transport-contract.reference.md
# backend-handoff.md
# src/app/App.svelte
# src/features/chat/ui/Composer.svelte
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
|
|
# Conflicts:
# src/app/App.svelte
# src/app/store.svelte.ts
# src/app/store.test.ts
# src/features/workspaces/ui/WorkspaceCard.test.ts
|
|
endpoint
|
|
|
|
sidebar wiring
|
|
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.)
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
+ 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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|