From 0f1b04bd7976ea416f11d83e4b7b78cf537bfdf3 Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
Date: Sun, 28 Jun 2026 13:18:25 +0900
Subject: feat(concurrency): configurable cooldown + adaptive-headroom
 auto-reduce banner
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Consumes the backend's concurrency-fixes (commit 2d27666) — additive to
transport-contract@0.23.0, 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).
---
 src/app/App.svelte | 4 ++++
 1 file changed, 4 insertions(+)

(limited to 'src/app/App.svelte')

diff --git a/src/app/App.svelte b/src/app/App.svelte
index 59f949c..3e7d05a 100644
--- a/src/app/App.svelte
+++ b/src/app/App.svelte
@@ -76,6 +76,7 @@
 		type DeleteConcurrencyLimit,
 		type LoadConcurrencyLimits,
 		type LoadConcurrencyStatus,
+		type SaveConcurrencyCooldown,
 		type SaveConcurrencyLimit,
 	} from "../features/concurrency";
 	import type { ChatStore } from "../features/chat";
@@ -491,6 +492,8 @@
 	const deleteConcurrencyLimit: DeleteConcurrencyLimit = (providerId) =>
 		store.deleteConcurrencyLimit(providerId);
 	const loadConcurrencyStatus: LoadConcurrencyStatus = () => store.concurrencyStatus();
+	const saveConcurrencyCooldown: SaveConcurrencyCooldown = (providerId, cooldownMs) =>
+		store.setConcurrencyCooldown(providerId, cooldownMs);
 </script>
 
 <main class="relative flex h-screen overflow-hidden">
@@ -841,6 +844,7 @@
 			saveLimit={saveConcurrencyLimit}
 			deleteLimit={deleteConcurrencyLimit}
 			loadStatus={loadConcurrencyStatus}
+			saveCooldown={saveConcurrencyCooldown}
 		/>
 	{/if}
 {/snippet}
-- 
cgit v1.2.3

