summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 19:46:24 +0900
committerAdam Malczewski <[email protected]>2026-06-27 19:46:24 +0900
commita9ca756de8cd023c0f2cb9954f344fff11146bc2 (patch)
treee91b69e31cf070c1c0652839b4415555c03cff70
parent92936c2577aa4d46c67c6109ad017958d8db382a (diff)
downloaddispatch-web-a9ca756de8cd023c0f2cb9954f344fff11146bc2.tar.gz
dispatch-web-a9ca756de8cd023c0f2cb9954f344fff11146bc2.zip
feat(concurrency): 'Saved.' confirmation on the limit row + handoff update
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.
-rw-r--r--backend-handoff.md20
-rw-r--r--src/features/concurrency/ui/ConcurrencyLimitRow.svelte13
2 files changed, 26 insertions, 7 deletions
diff --git a/backend-handoff.md b/backend-handoff.md
index 4197f24..67edf1a 100644
--- a/backend-handoff.md
+++ b/backend-handoff.md
@@ -5,9 +5,9 @@
> **From:** dispatch-web orchestrator · **To:** `../backend` orchestrator · **Courier:** the user.
> `lsp` does NOT span the repos (AGENTS.md § Backend seam) — every cross-repo ask flows through here.
-_Last updated: 2026-06-26 (CR-13 RESOLVED — backend shipped `"queued"` ConversationStatus (additive to `[email protected]`,
-no bump); FE consumed: WS parser accepts `"queued"`, TabList shows a loading-ring for queued / dots for active, Composer
-corner status gains `"queued"` → ring. 925 tests green. dev was merged earlier — merge commit e81df4c.) §2j concurrency unchanged._
+_Last updated: 2026-06-26 (backend: concurrency limits now PERSISTED across reboots — no API contract change, no FE
+re-pin/re-mirror needed; §2j updated. FE: brief "Saved." confirmation on the limit row after a successful save. 926 tests
+green.) Prior: CR-13 (`"queued"` ConversationStatus) RESOLVED; dev merged (e81df4c)._
**FE is current on `[email protected]` / `[email protected]` / `[email protected]`.** Open asks: **CR-9**
(`system:os` should detect WSL + include Linux distro — backend behavior change, no contract bump). The SSH-divergence
(§2d) is RESOLVED. CR-13 (`"queued"` ConversationStatus) is RESOLVED.
@@ -767,10 +767,16 @@ down, confirm it matches when a run actually fires). Until CR-HB-3 ships, the FE
The backend tracks + limits how many concurrent token-generating API requests are in flight PER
PROVIDER. When the cap is reached, further requests QUEUE and are granted slots oldest-agent-first
-(a 429 backoff PAUSES a provider's queue until `pausedUntil`). The cap is in-memory + per-provider
-(no persistence), managed via a new GLOBAL REST surface under `/concurrency/...` provided by the
-`concurrency` extension. **`[email protected]`** added the 5 types:
-`ConcurrencyLimitsResponse`, `SetConcurrencyLimitRequest`, `ConcurrencyLimitResponse`,
+(a 429 backoff PAUSES a provider's queue until `pausedUntil`). The cap is per-provider, managed via a
+new GLOBAL REST surface under `/concurrency/...` provided by the `concurrency` extension. **The limits
+are now PERSISTED across reboots** (backend update on `feature/provider-concurrency` — `PUT`/`DELETE`
+also write to storage; `GET /concurrency/limits` reads in-memory state pre-populated from storage on
+boot). **No API contract change** — the endpoints, request bodies, and response shapes are identical,
+so the FE needs no re-pin/re-mirror; a limit set via the UI now survives a server restart. (Earlier
+backend-only changes since the last handoff, also no FE impact: a 200ms release cooldown for internal
+slot recycling, and the `"queued"` `ConversationStatus` — which the FE already shipped, CR-13.)
+**`[email protected]`** added the 5 types: `ConcurrencyLimitsResponse`,
+`SetConcurrencyLimitRequest`, `ConcurrencyLimitResponse`,
`ConcurrencyStatusEntry`, `ConcurrencyStatusResponse`.
**Backend API (plain REST — the types ARE in `[email protected]`):**
diff --git a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte
index 411a3cd..5aacb88 100644
--- a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte
+++ b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte
@@ -23,6 +23,9 @@
let saving = $state(false);
let removing = $state(false);
let error = $state<string | null>(null);
+ /** Brief "Saved" confirmation after a successful save (mirrors ChatLimitField).
+ * Cleared when the field is edited again. */
+ let justSaved = $state(false);
$effect(() => {
const incoming = String(limit.limit);
@@ -35,6 +38,12 @@
const parsed = $derived(parseLimitInput(draft));
const dirty = $derived(parsed !== null && parsed !== limit.limit);
+ // Clear the "Saved" hint + any error as soon as the user edits the field.
+ function onInput(): void {
+ justSaved = false;
+ error = null;
+ }
+
async function handleSave(): Promise<void> {
if (parsed === null || parsed === limit.limit) return;
saving = true;
@@ -46,6 +55,7 @@
// also re-assert it via the seed effect above once the parent reloads).
draft = String(result.limit);
lastSeed = draft;
+ justSaved = true;
} else {
error = result.error;
}
@@ -72,6 +82,7 @@
class="input input-bordered input-xs w-20 font-mono"
aria-label={`Concurrency limit for ${limit.providerId}`}
bind:value={draft}
+ oninput={onInput}
disabled={saving || removing}
/>
<button
@@ -102,5 +113,7 @@
</div>
{#if error}
<span class="font-mono text-xs text-error">{error}</span>
+ {:else if justSaved && !dirty}
+ <span class="text-xs text-success">Saved.</span>
{/if}
</div>