| Age | Commit message (Collapse) | Author |
|
|
|
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.
|
|
# Conflicts:
# backend-handoff.md
|
|
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.
|
|
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).
|
|
|
|
overflows
|
|
|
|
|
|
|
|
|
|
|
|
cooldown
|
|
|
|
|
|
|
|
# Conflicts:
# backend-handoff.md
# src/features/workspaces/ui/WorkspaceCard.test.ts
|
|
# 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
|
|
Backend (feature/workspace-star) shipped Workspace.starred: boolean (additive
to [email protected], no version bump) + PUT/DELETE /workspaces/:id/star endpoints
(no body; create-on-miss; return the updated Workspace). A starred workspace's
agents jump ahead of non-starred ones in the concurrency limiter queue
(oldest-agent-first within each group); takes effect immediately for
already-queued agents.
FE consumed:
- adapter/http.ts: star(id)/unstar(id) -> WorkspaceResult<Workspace>
(PUT/DELETE /workspaces/:id/star, no body).
- logic/view-model.ts: pure sortWorkspaces (starred-first, then lastActivityAt
desc, stable) + pure applyStarred (the optimistic apply/revert transform).
- store.svelte.ts: setStarred(id, starred) — optimistic flip with error revert;
the list is now a $derived sorted view (starred bubble to top reactively);
no full refresh on success (avoids flicker).
- ui/WorkspaceCard.svelte: star toggle button (filled gold when starred,
outline when not; spinner in flight; aria-pressed/aria-label; tooltip notes
concurrency priority).
- Re-mirrored .dispatch/wire.reference.md (starred + delta note).
- GLOSSARY.md: 'starred' term.
Tests (+27): http star/unstar (5), view-model sort+applyStarred (12), store
optimistic+revert+re-sort (6), WorkspaceCard star button (4).
Verification: typecheck 0/0, 1045 tests green, biome clean, build OK.
backend-handoff.md updated (workspace-star slice, no open backend asks).
|
|
percentages on active steps
|
|
|
|
# Conflicts:
# .dispatch/transport-contract.reference.md
# backend-handoff.md
# src/app/App.svelte
# src/features/chat/ui/Composer.svelte
|
|
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.
|
|
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 now persists concurrency limits across reboots (no API contract change
— same endpoints/shapes, so no re-pin/re-mirror needed). Taking the suggested
optional UX hint: after a successful Save the limit row shows a brief green
'Saved.' that clears on the next edit (mirrors the ChatLimitField pattern).
backend-handoff.md §2j: notes the persistence change + the two earlier
backend-only changes (200ms release cooldown; 'queued' status — already shipped
as CR-13). typecheck 0/0, 926 tests green, biome clean, build OK.
|
|
The 'Concurrency limits' and 'Live status' section headings are clear on their
own; the explanatory paragraphs under each were overbearing. Removed both.
|
|
The 'queued' (waiting-for-a-concurrency-slot) indicator on the tab + composer
corner used loading-spinner; switch to DaisyUI's loading-ring. The 'active'
(generating) indicator stays loading-dots. typecheck 0/0, 926 tests green,
biome clean, build OK.
|
|
The 2s status poll (and post-mutation limit reloads) toggled a visible
loading state, but since the refresh is near-instant the spinner flashed
for <1 frame — a visible flicker/jerk every poll. The polling is now
SILENT (mirrors the heartbeat runs list):
- refreshLimits/refreshStatus: re-entrancy guard (limitsInFlight/statusInFlight,
no UI), no loading-state toggle. Error cleared only on success so it stays
visible + stable during an in-flight retry (no vanish-mid-poll).
- Removed limitsLoading/statusLoading: the Refresh buttons are plain-text
(no spinner, not disabled); the empty-state guards use hasLoaded* only
(no && !loading), so the list area never reflows mid-refresh.
+1 regression-guard test (Refresh buttons never surface a spinner). typecheck
0/0, 926 tests green (x2), biome clean, build OK.
|
|
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.
|
|
|
|
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.
|
|
|
|
|
|
against sidebar
|
|
|
|
swapping text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chats)
|