From e5c01aa3640ac430155d96e9802dd523fe461319 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 28 Jun 2026 20:25:51 +0900 Subject: fix(concurrency): Set+X on same line, status line with in-flight count and badge --- src/features/concurrency/index.ts | 1 + src/features/concurrency/logic/view-model.test.ts | 37 ++++++++++++++++++++ src/features/concurrency/logic/view-model.ts | 13 ++++++++ .../concurrency/ui/ConcurrencyLimitRow.svelte | 39 ++++++++++++++++++++-- src/features/concurrency/ui/ConcurrencyView.svelte | 17 ++++++---- .../concurrency/ui/ConcurrencyView.test.ts | 29 ++++++++++++++++ 6 files changed, 126 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/features/concurrency/index.ts b/src/features/concurrency/index.ts index 9499989..c0870e3 100644 --- a/src/features/concurrency/index.ts +++ b/src/features/concurrency/index.ts @@ -43,6 +43,7 @@ export { pauseLabel, providerFromModel, providerOptions, + statusLabel, summarizeLimits, summarizeStatus, viewAutoReduce, diff --git a/src/features/concurrency/logic/view-model.test.ts b/src/features/concurrency/logic/view-model.test.ts index c284ad0..6e6b770 100644 --- a/src/features/concurrency/logic/view-model.test.ts +++ b/src/features/concurrency/logic/view-model.test.ts @@ -14,6 +14,7 @@ import { pauseLabel, providerFromModel, providerOptions, + statusLabel, summarizeLimits, summarizeStatus, viewAutoReduce, @@ -262,6 +263,42 @@ describe("viewConcurrencyStatus", () => { }); }); +// ── statusLabel (the row's status badge word) ────────────────────────────────── + +describe("statusLabel", () => { + it("idle (no in-flight) → Idle", () => { + expect( + statusLabel(viewConcurrencyStatus(status({ inFlight: 0, limit: 4, queued: 0 }), 0)), + ).toBe("Idle"); + }); + + it("serving under capacity → Active", () => { + expect( + statusLabel(viewConcurrencyStatus(status({ inFlight: 2, limit: 4, queued: 0 }), 0)), + ).toBe("Active"); + }); + + it("at capacity with a queue → At capacity", () => { + expect( + statusLabel(viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 3 }), 0)), + ).toBe("At capacity"); + }); + + it("at capacity but no queue → Active (not At capacity)", () => { + expect( + statusLabel(viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 0 }), 0)), + ).toBe("Active"); + }); + + it("paused → Paused (regardless of in-flight/queue)", () => { + expect( + statusLabel( + viewConcurrencyStatus(status({ paused: true, inFlight: 4, limit: 4, queued: 3 }), 0), + ), + ).toBe("Paused"); + }); +}); + // ── viewAutoReduce / autoReduceNotices (the auto-reduce banner view) ─────────── describe("viewAutoReduce", () => { diff --git a/src/features/concurrency/logic/view-model.ts b/src/features/concurrency/logic/view-model.ts index e05423b..8a37198 100644 --- a/src/features/concurrency/logic/view-model.ts +++ b/src/features/concurrency/logic/view-model.ts @@ -260,6 +260,19 @@ export function viewConcurrencyStatus( }; } +/** + * A short status word for a status view, for the row's status badge: + * "Paused" · "At capacity" (in-flight at the cap with a queue) · "Active" + * (in-flight > 0) · "Idle". Mirrors the badge-text branching so the template + * holds no branching logic. + */ +export function statusLabel(view: ConcurrencyStatusView): string { + if (view.paused) return "Paused"; + if (view.inFlight >= view.limit && view.queued > 0) return "At capacity"; + if (view.inFlight > 0) return "Active"; + return "Idle"; +} + /** * The auto-reduce banner view for a status entry, or `null` when it is not * auto-reduced. `message` prefers the backend's `notice` (verbatim, when present diff --git a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte index 666b769..505847a 100644 --- a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte +++ b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte @@ -1,9 +1,13 @@
+
{limit.providerId}
+ + + {#if status !== null} +
+ {status.inFlightLabel} in flight + + {#if status.busy} + + {/if} + {statusLabel(status)} + +
+ {/if} + {#if error} {error} {:else if justSaved && !dirty} diff --git a/src/features/concurrency/ui/ConcurrencyView.svelte b/src/features/concurrency/ui/ConcurrencyView.svelte index 2f7435a..05d178a 100644 --- a/src/features/concurrency/ui/ConcurrencyView.svelte +++ b/src/features/concurrency/ui/ConcurrencyView.svelte @@ -9,6 +9,8 @@ providerOptions, summarizeLimits, viewConcurrencyLimits, + viewConcurrencyStatus, + type ConcurrencyStatusView, } from "../logic/view-model"; import type { ConcurrencyLimitEntry, @@ -201,12 +203,13 @@ * INVISIBLE, mirroring the heartbeat runs list). */ let statusInFlight = false; - // Per-provider cooldown lookup (from the live status poll) so each saved limit - // row seeds its cooldown input. Defaults to the server default (350) when a - // provider has no status entry yet. - const cooldownByProvider = $derived.by(() => { - const map = new Map(); - for (const s of statusEntries) map.set(s.providerId, s.cooldownMs); + // Per-provider status view (from the live status poll) so each saved limit row + // renders its in-flight count + status badge + seeds its cooldown input. Null + // when a provider has no status entry yet (the row falls back to Idle + the + // server-default cooldown of 350). + const statusByProvider = $derived.by(() => { + const map = new Map(); + for (const e of statusEntries) map.set(e.providerId, viewConcurrencyStatus(e)); return map; }); @@ -346,7 +349,7 @@
  • { expect((cooldownInput as HTMLInputElement).value).toBe("350"); }); + it("renders a status line (in-flight count left + badge right) below the edit line", async () => { + // Default status: limit 4, inFlight 2, queued 1, not paused → Active, "2/4". + const fakes = makeFakes(); + render(ConcurrencyView, { props: props(fakes) }); + + expect(await screen.findByText("2/4 in flight")).toBeVisible(); + expect(await screen.findByText("Active")).toBeVisible(); + }); + + it("shows the At-capacity badge when in-flight is at the cap with a queue", async () => { + const fakes = makeFakes({ + status: [statusEntry({ inFlight: 4, limit: 4, queued: 3 })], + }); + render(ConcurrencyView, { props: props(fakes) }); + + expect(await screen.findByText("4/4 in flight")).toBeVisible(); + expect(await screen.findByText("At capacity")).toBeVisible(); + }); + + it("shows the Idle badge when no slots are in flight", async () => { + const fakes = makeFakes({ + status: [statusEntry({ inFlight: 0, limit: 4, queued: 0 })], + }); + render(ConcurrencyView, { props: props(fakes) }); + + expect(await screen.findByText("0/4 in flight")).toBeVisible(); + expect(await screen.findByText("Idle")).toBeVisible(); + }); + it("surfaces NO loading indicator during refresh (background poll is silent — no flicker)", async () => { // The 2s status poll + post-mutation reloads are SILENT: they never toggle a // visible loading state, so the Refresh button is plain-text (no spinner). -- cgit v1.2.3