summaryrefslogtreecommitdiffhomepage
path: root/src/features/concurrency
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-08-12 22:01:59 +0900
committerAdam Malczewski <[email protected]>2026-08-12 22:01:59 +0900
commit5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443 (patch)
tree2f1d5ee2674d1dcc9909e89f06a82a7b10b4191a /src/features/concurrency
parent774519e052fd869952cc5e05aca159e4bf978fa1 (diff)
downloaddispatch-web-5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443.tar.gz
dispatch-web-5f10a40b26dbfdc10b2c02cdf23ec3a7edfa9443.zip
feat(models): numbered OpenCode credential selection, labels, migration
Adopt the numbered OPENCODE_API_KEY_N scheme in the model picker and concurrency surfaces. - model-select: numeric sort of opencode-<n> keys (#2 before #10); friendly OpenCode #N labels (raw opencode-N/<model> value still selected/persisted); migrateLegacyModelName rewrites opencode/<model> -> opencode-1/<model>. - ModelSelector: renders friendly key labels; surfaces a disappeared saved credential as UNAVAILABLE (disabled option + notice) without silently switching to another model. - app store: DEFAULT_MODEL -> opencode-1/...; catalogs no longer auto-fallback the active model when the selected credential is absent; boot restore migrates persisted legacy opencode/<model> tab models idempotently. - concurrency: providerLabel renders opencode-<n> as OpenCode #<n> in the limit rows and Add dropdown (raw provider id preserved on the wire). - Tests: parsing/sorting/labels/migration, unavailable-state selector behavior, no-silent-fallback, and concurrency provider label; fixtures updated to the numbered scheme. Verification: bun install, bun run typecheck (0 errors), bun run test (1179 passed), bun run check (clean, exit 0), bun run build (vite OK).
Diffstat (limited to 'src/features/concurrency')
-rw-r--r--src/features/concurrency/logic/view-model.test.ts28
-rw-r--r--src/features/concurrency/logic/view-model.ts12
-rw-r--r--src/features/concurrency/ui/ConcurrencyLimitRow.svelte5
-rw-r--r--src/features/concurrency/ui/ConcurrencyView.svelte3
-rw-r--r--src/features/concurrency/ui/ConcurrencyView.test.ts4
5 files changed, 42 insertions, 10 deletions
diff --git a/src/features/concurrency/logic/view-model.test.ts b/src/features/concurrency/logic/view-model.test.ts
index 6e6b770..9c557f5 100644
--- a/src/features/concurrency/logic/view-model.test.ts
+++ b/src/features/concurrency/logic/view-model.test.ts
@@ -13,6 +13,7 @@ import {
parseLimitInput,
pauseLabel,
providerFromModel,
+ providerLabel,
providerOptions,
statusLabel,
summarizeLimits,
@@ -95,7 +96,7 @@ describe("cooldownLabel", () => {
describe("providerFromModel", () => {
it("takes the part before the first slash", () => {
expect(providerFromModel("openai/gpt-4o")).toBe("openai");
- expect(providerFromModel("openai-compat/gpt-4o-mini")).toBe("openai-compat");
+ expect(providerFromModel("opencode-1/gpt-4o-mini")).toBe("opencode-1");
});
it("returns the whole string when there is no slash", () => {
expect(providerFromModel("umans")).toBe("umans");
@@ -127,6 +128,23 @@ describe("providerOptions", () => {
});
});
+describe("providerLabel", () => {
+ it("labels a numbered OpenCode provider as OpenCode #N", () => {
+ expect(providerLabel("opencode-2")).toBe("OpenCode #2");
+ expect(providerLabel("opencode-10")).toBe("OpenCode #10");
+ });
+
+ it("leaves non-OpenCode provider ids unchanged", () => {
+ expect(providerLabel("umans")).toBe("umans");
+ expect(providerLabel("anthropic")).toBe("anthropic");
+ });
+
+ it("rejects noncanonical numbered ids (label passthrough)", () => {
+ expect(providerLabel("opencode")).toBe("opencode");
+ expect(providerLabel("opencode-0")).toBe("opencode-0");
+ });
+});
+
// ── pauseLabel + formatPauseDuration ───────────────────────────────────────────
describe("formatPauseDuration", () => {
@@ -450,12 +468,12 @@ describe("normalizeConcurrencyLimits", () => {
const limits = normalizeConcurrencyLimits({
limits: [
{ providerId: "umans", limit: 4 },
- { providerId: "openai-compat", limit: 5 },
+ { providerId: "opencode-1", limit: 5 },
],
});
expect(limits).toEqual([
{ providerId: "umans", limit: 4 },
- { providerId: "openai-compat", limit: 5 },
+ { providerId: "opencode-1", limit: 5 },
]);
});
@@ -510,7 +528,7 @@ describe("normalizeConcurrencyStatus", () => {
providers: [
{ providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false },
{
- providerId: "openai-compat",
+ providerId: "opencode-1",
limit: 5,
inFlight: 5,
queued: 3,
@@ -532,7 +550,7 @@ describe("normalizeConcurrencyStatus", () => {
});
expect(first !== undefined && !("pausedUntil" in first)).toBe(true);
expect(second).toEqual({
- providerId: "openai-compat",
+ providerId: "opencode-1",
limit: 5,
inFlight: 5,
queued: 3,
diff --git a/src/features/concurrency/logic/view-model.ts b/src/features/concurrency/logic/view-model.ts
index 8a37198..80c155c 100644
--- a/src/features/concurrency/logic/view-model.ts
+++ b/src/features/concurrency/logic/view-model.ts
@@ -170,6 +170,18 @@ export function providerOptions(
return out;
}
+/**
+ * Friendly display label for a concurrency provider id. A numbered OpenCode
+ * provider (`opencode-<n>`) renders as "OpenCode #<n>" (so "opencode-2" reads
+ * as a coherent credential identity, matching the model picker); any other id is
+ * shown unchanged. The RAW id is still what is sent to / displayed-with the
+ * backend — this only affects presentation.
+ */
+export function providerLabel(providerId: string): string {
+ const m = /^opencode-([1-9][0-9]*)$/.exec(providerId);
+ return m === null ? providerId : `OpenCode #${m[1]}`;
+}
+
// ── Status → display view ──────────────────────────────────────────────────────
const NO_LIMITS = "No limits configured";
diff --git a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte
index bf06ac0..c8f6c8f 100644
--- a/src/features/concurrency/ui/ConcurrencyLimitRow.svelte
+++ b/src/features/concurrency/ui/ConcurrencyLimitRow.svelte
@@ -4,6 +4,7 @@
DEFAULT_COOLDOWN_MS,
parseCooldownInput,
parseLimitInput,
+ providerLabel,
statusLabel,
type Badge,
type ConcurrencyLimitView,
@@ -132,8 +133,8 @@
the buttons never wrap; tight gap + narrow inputs keep the provider name
visible; the provider shrinks via flex-1 + min-w-0). -->
<div class="flex flex-nowrap items-center gap-1">
- <span class="min-w-0 flex-1 truncate font-medium font-mono" title={limit.providerId}
- >{limit.providerId}</span
+ <span class="min-w-0 flex-1 truncate font-medium" title={limit.providerId}
+ >{providerLabel(limit.providerId)}</span
>
<input
type="text"
diff --git a/src/features/concurrency/ui/ConcurrencyView.svelte b/src/features/concurrency/ui/ConcurrencyView.svelte
index aadb8d1..2e23f5c 100644
--- a/src/features/concurrency/ui/ConcurrencyView.svelte
+++ b/src/features/concurrency/ui/ConcurrencyView.svelte
@@ -6,6 +6,7 @@
DEFAULT_COOLDOWN_MS,
parseCooldownInput,
parseLimitInput,
+ providerLabel,
providerOptions,
summarizeLimits,
viewConcurrencyLimits,
@@ -376,7 +377,7 @@
<option value="" disabled>No providers available</option>
{:else}
{#each providerOpts as provider (provider)}
- <option value={provider}>{provider}</option>
+ <option value={provider}>{providerLabel(provider)}</option>
{/each}
{/if}
</select>
diff --git a/src/features/concurrency/ui/ConcurrencyView.test.ts b/src/features/concurrency/ui/ConcurrencyView.test.ts
index a8163c2..e214692 100644
--- a/src/features/concurrency/ui/ConcurrencyView.test.ts
+++ b/src/features/concurrency/ui/ConcurrencyView.test.ts
@@ -11,8 +11,8 @@ import type {
} from "../logic/types";
import ConcurrencyView from "./ConcurrencyView.svelte";
-// Available models → provider ids are "umans", "anthropic", "openai-compat".
-const MODELS = ["umans/umans-glm-5.2", "anthropic/claude-sonnet", "openai-compat/gpt-4o"] as const;
+// Available models → provider ids are "umans", "anthropic", "opencode-1".
+const MODELS = ["umans/umans-glm-5.2", "anthropic/claude-sonnet", "opencode-1/gpt-4o"] as const;
// A status entry factory (defaults to a healthy limited provider). The new
// concurrency-fixes fields (`cooldownMs`, `autoReduced`) are always present.