summaryrefslogtreecommitdiffhomepage
path: root/src/app
AgeCommit message (Collapse)Author
2026-06-29Merge branch 'feature/heartbeat-inactive-only' into predevAdam Malczewski
2026-06-29feat(heartbeat): add inactiveOnly checkbox (skip fires while workspace is busy)Adam Malczewski
Add the frontend UI for the backend's per-workspace heartbeat `inactiveOnly` setting (default on): when on, the heartbeat skips a scheduled fire whenever the configured workspace has any active agents (a conversation whose status is "active" or "queued"); it stays quiet while the user is actively working and only fires when the workspace is idle. When off, the heartbeat fires unconditionally on every interval (the pre-existing behavior). Contract (backend handoff: notes/heartbeat-setting-handoff.md): - GET /workspaces/:id/heartbeat now includes inactiveOnly: boolean - PUT /workspaces/:id/heartbeat accepts a partial { inactiveOnly?: boolean } (absent = unchanged; explicit false = opt-out; non-boolean → 400) The heartbeat shapes are FE-owned locally (consumer-defines-port — not in @dispatch/transport-contract; verified the mirror has no heartbeat symbol), so this is a FE-local type + view-model + UI change — no pinned file: dep bump or .dispatch mirror regen needed. The network seam (store.svelte.ts) needed no change: setHeartbeatConfig already JSON.stringify's the patch verbatim and normalizes the response via normalizeHeartbeatConfig (now producing the field). Implementation (pure core / injected shell): - types.ts: inactiveOnly on HeartbeatConfig + HeartbeatConfigPatch - view-model.ts (pure): inactiveOnly in HeartbeatFormState; formFromConfig/ emptyForm/patchFromForm/formDiffers carry it; normalizeHeartbeatConfig coerces with `d.inactiveOnly !== false` (default ON — mirrors the backend config-store deserializer; only explicit false opts out, so a legacy config persisted before the field reads back as true) - HeartbeatView.svelte: a checkbox (default checked) "Only run when idle" as the first config section, save-on-change (mirrors the enable toggle): a partial PUT { inactiveOnly } on toggle; a failed save reverts + surfaces the error inline Tests: new HeartbeatView.test.ts (renders checked/unchecked, partial-patch PUT, failed-save revert); view-model.test.ts form/normalize/diff coverage; updated PromptEditor.test.ts + store.test.ts echo fixtures for the new required field. Verification: typecheck 0 errors; test 1133 passing (stable x2); biome clean; build succeeds. Live read-only check: the running main backend (not the feature worktree backend) has no inactiveOnly yet; the FE's defensive default renders the checkbox checked and degrades gracefully — no crash. No backend was booted by the agent. backend-handoff.md §2l records the consumed contract + FE status.
2026-06-29Merge branch 'feature/cancel-queued-message' into predevAdam Malczewski
# Conflicts: # backend-handoff.md
2026-06-29feat(chat): cancel a queued steering message from the UIAdam Malczewski
Implements the frontend for the cancel-queued-message backend feature (transport-contract 0.23.0 → 0.24.0, ADDITIVE). While a turn is generating and a user message is queued (awaiting steering delivery), the user can now cancel a single queued message by id so it never runs. - Consume the contract: regenerate the .dispatch/transport-contract.reference.md mirror to 0.24.0; add chat.queue.cancel to the exhaustive WsClientMessage guard (core/wire/conformance.ts) + its test. - ChatTransport port accepts ChatQueueCancelMessage; cancelQueuedMessage(id) on the chat store + app store sends chat.queue.cancel { conversationId, messageId } (fire-and-forget, idempotent — the server no-ops an already- drained/unknown id). - UI: a × cancel affordance per queued row in MessageQueueList.svelte, threaded through SurfaceView's onCancelQueuedMessage (dispatched on rendererId, never the surface id) and wired to store.cancelQueuedMessage. Optimistic removal (pure selectVisibleMessages/reconcileCancelledIds in logic/message-queue.ts) hides the row on click and reconciles from the message-queue surface's post-cancel snapshot. No new event handling — the existing surface subscription reflects the result. Tests: +12 (chat store cancel op ×3, optimistic-removal logic ×11 already existed pattern, MessageQueueList component cancel behavior ×9). Repo fix: package.json + bun.lock were still pinning file:../dispatch-backend/... (stale from the dispatch-backend → backend rename) — corrected to file:../backend/packages/...; bun install now resolves natively with no worktree symlink hack. typecheck 0/0, 1140 tests green (run twice), biome clean, build OK.
2026-06-28feat(chat): add "Off" option to Reasoning Effort dropdown (thinking on/off)Adam Malczewski
Add an "Off" choice as the FIRST option in the per-conversation Reasoning Effort selector. Selecting it disables extended thinking ENTIRELY for the conversation's turns — NOT "set the effort to its lowest level". The two axes are kept SEPARATE on the wire (per the umans API model, where thinking-off is `reasoning_effort: "none"`, distinct from the effort level): - `reasoningEffort` is the thinking-DEPTH ladder (low→max) — UNCHANGED. - `thinking` is the on/off switch — a NEW per-conversation boolean. Turning thinking off PRESERVES the persisted effort level, so an off→on toggle restores the previously-chosen depth. The selector CONFLATES the two axes into one <select> (UX); the wire does not. Pure logic (reasoning-effort.ts): ThinkingSelection = "off" | ReasoningEffort, selectionOptions() (Off first + the ladder, default marked), isThinkingSelection, effectiveSelection(persistedEffort, persistedThinking), + FE-local PROPOSED wire types (ThinkingResponse, SetThinkingRequest) + SaveThinkingSelection port. The existing effortOptions()/isReasoningEffort()/effectiveEffort() are UNCHANGED — the heartbeat feature reuses them (its own dropdown is unaffected). Store: `thinking` state (boolean|null, null⇒ON default), refreshThinking() (GET, alongside refreshReasoningEffort() on every focus/draft/switch), setThinking() (PUT). App.svelte's saveThinkingSelection adapter fans one selection out: "off" → PUT /thinking {thinking:false} (effort untouched); a level → ensure thinking ON (if off) then PUT /reasoning-effort {level}. Backend contract gap (CR-14, backend-handoff.md §2l): the backend has NO thinking-off mechanism today (umans mapReasoningEffort can't emit "none"). The FE is built against the PROPOSED additive contract (GET/PUT /conversations/:id/thinking + ThinkingResponse/SetThinkingRequest; umans maps thinking===false → reasoning_effort:"none"). Until shipped, GET /thinking 404s and refreshThinking() leaves `thinking` null⇒ON — the selector gracefully shows the effort level, no crash. Verification: typecheck 0/0, 1128 tests green (run TWICE — touches the shared fetch fake), biome clean, build OK. 9 files (8 source + backend-handoff.md).
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.