feat(spawn): three-level provider selection, skip-layer resolution, one searchable select (ADR-0030) #67

Merged
dominik.polakovics merged 1 commit from afk/66 into main 2026-07-09 11:56:16 +02:00

Closes #66

What ships

Three-level agent-CLI selection (per-spawn pick → repo override → global default, AFK-symmetric, ADR-0030 amending ADR-0021):

  • Migration 0006 (sqlite + postgres): repos.provider nullable (stamped rows reset to NULL — never operator-chosen), new repos.afk_provider_default.
  • New settings: provider_default (seeded to the first registered provider) and spawn_provider_default_afk (empty = inherit). reposvc.DefaultProvider deleted.
  • POST .../instances accepts optional provider (strict); PATCH /repos/{id} + POST /repos accept provider / afk_provider_default (validated, null = inherit); settings PATCH validates the new keys.

Skip-layer resolution (ResolveProvider + reworked ResolveModelEffort): a default-layer value missing from the effective provider's catalog (registry, for provider ids) is treated as unset and falls through; final fallback = the catalog's first entry; an empty catalog resolves to "" and omits the CLI flag. Explicit request values stay strict (400). This defuses the seeded claude-shaped opus[1m] default 400-ing every AFK launch on a second provider's repo, and the HasOption([], "") == false empty-efforts trap. Backend now mirrors the SPA's resolveSpawnOption exactly.

One searchable select (web/src/components/Select.tsx, no new deps): chip + form-field skins, ARIA listbox with full keyboard nav, automatic search input at ≥8 options (case-insensitive over label+value), prefer-below flip+clamp placement measured against visualViewport — fixes the repo panel clipping off-screen above the composer. Absorbs CatalogSelect's inherit + "(not in catalog)" semantics and the repo picker's disabled/status rows; CatalogSelect deleted, all call sites migrated.

UI wiring: composer Agent chip only at ≥2 registered providers (single-provider UI stays pixel-identical); ephemeral per-spawn pick resets on repo change; model/effort picks reset when the effective provider changes (fixes the latent foreign-pick 400); effort chip hidden on an empty catalog; AddRepo + RepoSettings agent selects with inherit entries; RepoSettings base model/effort free-text → catalog selects (stale "M3" hint removed); global Settings renders base + AFK defaults against the drafted default providers instead of providers[0]; nothing auto-clears on a provider flip — foreign values show "(not in catalog)" and stay reversible.

Docs: ADR-0030 (amends ADR-0021, touches ADR-0025, references ADR-0026 unchanged); CONTEXT.md provider/AFK-default entries updated.

Verification

  • CGO_ENABLED=0 go test ./... green (only pre-existing sandbox-environmental internal/tmuxx integration failures, identical on pristine main); go vet + gofmt clean.
  • Web: 546 vitest tests green (36→37 files, +46 tests incl. 21 for the new Select), eslint clean, prettier --check clean, tsc --noEmit && vite build green; embedded-UI binary builds (go build -tags ui).
  • New backend tests pin the chains: manual/AFK provider layering, skip-unregistered, strict 400s, empty-catalog "", and THE two-provider scenario (AFK on provider B with the claude-shaped global default resolves B's first model + empty effort, no 400) at unit and HTTP level.

🤖 Generated with Claude Code

https://claude.ai/code/session_012iM5UvodpD714c2ASt9WHt

Closes #66 ## What ships **Three-level agent-CLI selection** (per-spawn pick → repo override → global default, AFK-symmetric, ADR-0030 amending ADR-0021): - Migration `0006` (sqlite + postgres): `repos.provider` nullable (stamped rows reset to NULL — never operator-chosen), new `repos.afk_provider_default`. - New settings: `provider_default` (seeded to the first registered provider) and `spawn_provider_default_afk` (empty = inherit). `reposvc.DefaultProvider` deleted. - `POST .../instances` accepts optional `provider` (strict); `PATCH /repos/{id}` + `POST /repos` accept `provider` / `afk_provider_default` (validated, null = inherit); settings PATCH validates the new keys. **Skip-layer resolution** (`ResolveProvider` + reworked `ResolveModelEffort`): a default-layer value missing from the effective provider's catalog (registry, for provider ids) is treated as unset and falls through; final fallback = the catalog's first entry; an empty catalog resolves to `""` and omits the CLI flag. Explicit request values stay strict (400). This defuses the seeded claude-shaped `opus[1m]` default 400-ing every AFK launch on a second provider's repo, and the `HasOption([], "") == false` empty-efforts trap. Backend now mirrors the SPA's `resolveSpawnOption` exactly. **One searchable select** (`web/src/components/Select.tsx`, no new deps): chip + form-field skins, ARIA listbox with full keyboard nav, automatic search input at ≥8 options (case-insensitive over label+value), prefer-below flip+clamp placement measured against `visualViewport` — fixes the repo panel clipping off-screen above the composer. Absorbs CatalogSelect's inherit + "(not in catalog)" semantics and the repo picker's disabled/status rows; `CatalogSelect` deleted, all call sites migrated. **UI wiring**: composer Agent chip only at ≥2 registered providers (single-provider UI stays pixel-identical); ephemeral per-spawn pick resets on repo change; model/effort picks reset when the effective provider changes (fixes the latent foreign-pick 400); effort chip hidden on an empty catalog; AddRepo + RepoSettings agent selects with inherit entries; RepoSettings base model/effort free-text → catalog selects (stale "M3" hint removed); global Settings renders base + AFK defaults against the drafted default providers instead of `providers[0]`; nothing auto-clears on a provider flip — foreign values show "(not in catalog)" and stay reversible. **Docs**: ADR-0030 (amends ADR-0021, touches ADR-0025, references ADR-0026 unchanged); CONTEXT.md provider/AFK-default entries updated. ## Verification - `CGO_ENABLED=0 go test ./...` green (only pre-existing sandbox-environmental `internal/tmuxx` integration failures, identical on pristine main); `go vet` + `gofmt` clean. - Web: 546 vitest tests green (36→37 files, +46 tests incl. 21 for the new Select), `eslint` clean, `prettier --check` clean, `tsc --noEmit && vite build` green; embedded-UI binary builds (`go build -tags ui`). - New backend tests pin the chains: manual/AFK provider layering, skip-unregistered, strict 400s, empty-catalog `""`, and THE two-provider scenario (AFK on provider B with the claude-shaped global default resolves B's first model + empty effort, no 400) at unit and HTTP level. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012iM5UvodpD714c2ASt9WHt
feat(spawn): three-level provider selection, skip-layer resolution, one searchable select (ADR-0030)
All checks were successful
ci / native (pull_request) Successful in 4m51s
096bff6ca9
The agent CLI becomes a layered spawn knob like model/effort (issue #66):
effective provider = per-spawn pick → repo override → global default, with
symmetric AFK overrides (repos.afk_provider_default → spawn_provider_default_afk),
resolved AFK-first exactly like ADR-0021. repos.provider goes nullable
(migration 0006, sqlite+postgres; stamped rows reset to NULL — they were
never operator-chosen); the deleted reposvc.DefaultProvider const is replaced
by the seeded provider_default setting (first registered provider).

Resolution turns skip-layer: a DEFAULT-layer model/effort/provider not in the
effective provider's catalog (registry, for ids) reads as unset and falls
through; final fallback is the catalog's first entry; an EMPTY catalog
resolves to "" and omits the CLI flag — fixing the claude-shaped opus[1m]
global default 400-ing every AFK launch on a second provider's repo, and the
empty-efforts-can-never-spawn trap. Explicit request values stay strict (400).
POST instances accepts provider; PATCH/POST repos accept provider +
afk_provider_default; settings PATCH validates the two new keys.

One hand-rolled Solid Select (chip + field skins, ARIA listbox, search at ≥8
options, prefer-below flip+clamp against visualViewport) replaces the
clipping repo popover, the native model/effort chips, and CatalogSelect
(deleted). Composer gains an Agent chip only at ≥2 providers (ephemeral pick,
resets on repo change); model/effort picks reset when the effective provider
changes; the effort chip hides on an empty catalog; AddRepo/RepoSettings gain
inherit-capable agent selects; global Settings renders base + AFK defaults
against the drafted default providers; the stale M3 hint is gone.

Closes #66

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012iM5UvodpD714c2ASt9WHt
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!67
No description provided.