Skip to content

feat(6.26): per-role model dropdowns from seed + user override#31

Merged
fstamatelopoulos merged 9 commits intomainfrom
iteration-6/per-role-model-dropdowns
May 3, 2026
Merged

feat(6.26): per-role model dropdowns from seed + user override#31
fstamatelopoulos merged 9 commits intomainfrom
iteration-6/per-role-model-dropdowns

Conversation

@fstamatelopoulos
Copy link
Copy Markdown
Owner

Summary

Replaces the free-text per-role model field across web Settings,
workspace Config, and CLI init / config-edit with a registry-driven
dropdown. Registry = bundled seed in
packages/core/src/adapters/seed-models.ts (intentionally minimal --
generic aliases like opus, sonnet, haiku for claude-code;
gpt-5-codex, gpt-5, o3 for codex) merged with an optional user
override on CfcfGlobalConfig.agentModels[<adapter>]. Override is
managed via a new "Model registry" section on the Settings page.

Why no remote registry / API query

Researched first; documented in commits + CLAUDE.md:

  • Neither claude nor codex exposes a list-models subcommand
  • Anthropic / OpenAI Models APIs require auth (API key); not what
    Claude Code's OAuth subscription users have
  • Hosting our own remote registry shifts maintenance responsibility
    onto cfcf maintainers

Seed + user override side-steps all three. Hand-edited config values
not in the registry are preserved as (custom) entries on first
render so back-compat doesn't break.

Single edit surface (review feedback)

Original design had a "(custom model name…)" sentinel in every
picker as an inline escape hatch. Dropped after review: having two
add-paths created a UX question the design didn't answer cleanly
("does this persist?"). Settings → Model registry is now the one
place to add / remove / reset; pickers are read-only dropdowns.

Surfaces touched

  • Settings → Agent roles + Workspace → Config → Agent roles:
    free-text model field becomes <AgentModelSelect> (a <select>
    with (adapter default) first option + the resolved registry).
    Adapter swap re-targets the model dropdown.
  • Settings → Model registry: new section. Per-adapter card with
    the resolved list as deletable chips + add-model input + reset to
    seed.
  • CLI cfcf init / cfcf config edit: numbered selector
    matching the existing pickAgent pattern. Hand-edited per-role
    models that aren't in the registry survive as a numbered
    "(custom — from existing config)" line so re-running init keeps
    the user's pick.

UX / theming polish bundled in (review feedback)

  • Stripped two plan-item refs that had leaked into UI titles
    ("Model registry (item 6.26)", "Behaviour flags (item 5.1)").
    Saved a feedback memory so this doesn't recur.
  • Static section titles (FormSection h3, Memory page h3) no longer
    look clickable. The architect-review__summary class was reused
    on static h3s, inheriting cursor: pointer + hover from its
    legitimate use on <details>/<summary> blocks. Split into a
    new .section-title class without interactivity styling.
  • Bumped table label / cell font sizes to match the form-input
    scale (var(--text-md) ≈ 0.95rem) -- left-column labels on
    Server status + role-name cells in Agent roles read at the same
    size as the dropdown text next to them.

Tests

  • 8 new unit tests covering resolveModelsForAdapter (seed
    precedence, override precedence, empty-array fallback,
    malformed-entry filtering, per-adapter isolation, user-only
    adapter)
  • 4 new HTTP tests covering GET /api/agents/models

bun run typecheck clean. bun run test: 579 / 100 / 66 / 9 green.
bun run build:web clean.

Test plan

  • Build + tests + typecheck
  • Manual: rebuild + restart server → Settings + workspace Config
    dropdowns populated from registry; adapter swap re-targets
    model dropdown; Model registry section adds/removes
    reflected in pickers; "Reset to seed" works; CLI
    cfcf init --force walks the numbered picker; existing
    hand-edited model values preserved as "(custom)" rows

fstamatelopoulos and others added 9 commits May 2, 2026 22:05
Adds the data + helpers that drive per-role model dropdowns in the web
UI and CLI. Two layers:

  1. Bundled seed in packages/core/src/adapters/seed-models.ts.
     Intentionally minimal -- generic aliases (opus / sonnet / haiku
     for claude-code; gpt-5-codex / gpt-5 / o3 for codex) over
     date-bound full names so the seed ages slowly. Aliases stay
     stable as upstream releases new model versions.
  2. Optional user override on CfcfGlobalConfig.agentModels[<adapter>]
     -- managed via the web Settings → Model registry editor (next
     commit). When set + non-empty, supersedes the seed.

resolveModelsForAdapter(adapter, config) is the single source of truth
for "what models to surface in pickers"; resolveAllModels(config) is
the bulk version for the API endpoint. Pickers always also offer a
"(adapter default)" first option (empty value) and a "(custom model
name…)" sentinel last option for the long tail.

validateConfig coerces malformed agentModels entries to a no-op
(filters non-string + blank model names; drops the field if the whole
thing is shapeless) so a hand-edited config can't break startup.

8 new unit tests cover seed lookup, override precedence, empty-array
fallback, malformed-entry filtering, per-adapter isolation, and the
"user-only adapter with no seed" case.

Why no remote registry: hosting one shifts maintenance responsibility
onto cfcf maintainers (model-list curation, rate-limiting,
availability). Seed-plus-override side-steps that; the (custom)
fallback handles the long tail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns { adapters, seed }: `adapters` is the resolved per-adapter
list (user override else seed), `seed` is surfaced alongside so the
web Settings → Model registry editor can render a "Reset to seed"
affordance without having to know seed values out-of-band.

Web pickers (Settings agent-roles + workspace Config agent-roles)
fetch this on mount and after every save that touches `agentModels`.
The seed list is small -- response stays in single-digit-KB range --
so the cost of refreshing on every picker open is negligible.

4 new HTTP tests cover seed-only, user-override, seed-surfaced-
alongside, and pre-init (no config file) cases. Each test isolates
the user's real ~/.cfcf/ via CFCF_CONFIG_DIR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the free-text model field in two places (Settings → Agent
roles + workspace Config → Agent roles) with the new AgentModelSelect
component. Layout per role:

  - <select> populated from the resolved per-adapter registry (fetched
    once on mount + re-fetched after a Model registry save)
  - "(adapter default)" first option (empty value; current free-text
    empty semantic preserved)
  - "(custom model name…)" sentinel last option that swaps to a free-
    text input -- mirrors the ClioProjectDialog pattern shipped in 6.12
  - Hand-edited config values not in the registry render as
    `<option>{value} (custom)</option>` so we never silently lose them

When the role's adapter dropdown changes, the model dropdown's options
swap to the new adapter's registry; if the previously-selected model
isn't in the new list, custom mode kicks in automatically.

New "Model registry" section in Settings: per-adapter card showing the
current resolved list as deletable chips + an "Add model name" input
+ "Reset to seed" button when an override is in effect. Saving routes
through the existing global-config save path; an empty list (or one
identical to the seed) clears the override so future seed updates
flow through automatically.

types.ts: agentModels?: Record<string, string[]> on GlobalConfig to
mirror the core type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the free-text "X agent model" prompts in `cfcf init` (and
therefore `cfcf config edit`, which delegates) with a numbered
selector mirroring the existing `pickAgent` pattern. Layout per role:

  Dev model (adapter: claude-code):
    0. (use adapter default)
    1. opus
    2. sonnet
    3. haiku
    4. (custom -- type a name)
  Choose [2]:

The registry per role is resolved live from
resolveModelsForAdapter(adapter, config), so:
  - editing Settings → Model registry on the web side affects the
    next CLI prompt
  - the per-role adapter chosen earlier in the same init run drives
    which list appears (each role can pick from its own adapter's
    models)
  - hand-edited per-role models that aren't in the registry default
    to the "custom" branch (so re-running init keeps the user's pick
    on Enter)

CLAUDE.md gets a paragraph documenting the seed-vs-override model
and the maintenance workflow (when an upstream agent CLI ships a new
headline model, update the relevant array in seed-models.ts and ship
in the next release; user overrides survive the upgrade).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (6.26 review)

Two UX/polish fixes from the 6.26 review:

1. Plan-item refs leaking into rendered titles: "Model registry
   (item 6.26)" → "Model registry"; "Behaviour flags (item 5.1)" →
   "Behaviour flags". Internal references confuse end users and
   signal an unpolished product. Code comments still carry the
   trace, which is the right place for them. (Memory file added on
   the agent side so this doesn't happen again.)

2. Static section titles looked clickable. The .architect-review__
   summary class was reused on FormSection's <h3>s but it has
   `cursor: pointer` + a hover colour swap because it's also used on
   real <summary> elements inside <details> in ArchitectReview /
   JudgeDetail. Split: <summary> elements keep
   architect-review__summary; <h3> static titles get a new
   .section-title class with the same typography but no cursor /
   hover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced in review: having two ways to add a model -- the inline
"(custom model name…)" sentinel in every picker AND the Settings →
Model registry editor -- created a UX question the design didn't
answer cleanly ("does this persist?"). The custom sentinel was
one-shot per role with no persistence; the registry editor is the
real management surface.

Resolution: single edit surface. The picker is now a plain dropdown
of (adapter default) + registry models. To add a model that's not in
the registry, the user goes to Settings → Model registry → Add. To
remove a model, click the × on its chip. To reset, click "Reset to
seed". One place, three operations, no ambiguity.

Back-compat preserved: a hand-edited config value not in the
registry still renders as `<option>{value} (custom)</option>` on
first paint so we never silently coerce it. The user clears it by
picking another option, or makes it stick by adding it to the
registry. Same back-compat handling on the CLI side: an unknown
existing per-role model shows up as a numbered option labeled
"<value> (custom — from existing config)".

CLAUDE.md note + Settings page intro updated to reflect the simpler
model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… review)

Spotted in the screenshot: left-column labels in the Server section
(Status / Version / Port) and the Role-name cells in Agent roles
(Dev / Judge / Solution Architect) read noticeably smaller than the
right-hand values + the input dropdowns next to them. The values use
the new --text-md (0.95rem) input styling; the labels were stuck on
0.8rem from before the typography pass.

Three rules bumped:
  - .config-display__table th: 0.8rem  → var(--text-md)  (Server labels)
  - .project-history__time:    0.8rem  → var(--text-md)  (Role cells)
  - .project-history__table:   0.875rem → var(--text-md) (table body)
  - .project-history__agent:   0.8rem  → var(--text-sm)  (history rows)

Column headers (.project-history__table th, the uppercase ROLE /
ADAPTER / MODEL row) bumped a smaller notch -- 0.75rem → var(--text-xs)
0.825rem -- since they're already uppercase + letterspaced and the
contrast against body text is part of their visual signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caught by the user during 6.26 dogfooding. I fixed the same string in
ConfigDisplay (workspace Config tab) earlier but missed the parallel
"Behaviour flags (item 5.1)" in ServerInfo (the Settings page) -- both
forms have a Behaviour flags section and they drifted.

Audit of every user-facing string in packages/web/src/ +
packages/cli/src/ now comes back clean (regex:
`"[^"]*item [0-9]\.[0-9][^"]*"|>[^<]*item [0-9]\.[0-9][^<]*<`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cale

The previous bump only landed on the four table classes I'd identified
on the Settings page (config-display__table th, project-history__time,
etc.). Other views still had small text from two sources I missed:

  1. ~25 inline `fontSize: "0.7Xrem"` style literals scattered across
     12 components (ConfigDisplay, WorkspaceHistory, PaSessionDetail,
     LoopControls, ...). These bypass any CSS rule via specificity, so
     no amount of class-targeted bumping reaches them.
  2. ~50 small `font-size: 0.7Xrem` declarations in app.css for
     component-specific classes (phase-step__label, judge-assessment__
     tests, architect-review__counts, log-viewer__content, modal__title,
     status-badge, …).

Sweep replaces every literal with the corresponding token:
  0.75/0.78rem → var(--text-xs)   (~13.2px)
  0.8 /0.85rem → var(--text-sm)   (~14.4px)
  0.875rem     → var(--text-md)   (~15.2px)

So all small text now lives at three coordinated sizes that move
together when the scale changes. Net effect: every view (workspace
detail, history, logs, memory, help, judge/architect/reflection
detail panels, modals) gets a ~10-15% size bump on its small-text
elements, matching the bump that landed on Settings + workspace Config
tabs in the previous commit.

PaSessionDetail had three non-standard literals (0.72rem, 0.9rem,
0.92rem) cleaned up to the nearest token in the same pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fstamatelopoulos fstamatelopoulos merged commit 26ad7ae into main May 3, 2026
2 checks passed
@fstamatelopoulos fstamatelopoulos deleted the iteration-6/per-role-model-dropdowns branch May 3, 2026 05:34
fstamatelopoulos added a commit that referenced this pull request May 3, 2026
Following the same pattern as 6.20 + 6.12: PR #31 landed on main
(per-role model dropdowns + Model registry editor + typography polish
+ remaining plan-item ref stripping). The v0.18.0 tag was placed
after 6.12 but never released (release.yml is workflow_dispatch only
and hasn't been triggered), so re-targeting it at this commit
captures everything that ships in the actual release artefact.

CHANGELOG header now lists 6.20 + 6.12 + 6.26 with a unified
rationale; 6.20, Security, and 6.12 sections kept verbatim; new 6.26
section + Typography polish section inserted before the v0.17.1
boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fstamatelopoulos added a commit that referenced this pull request May 3, 2026
Plan housekeeping after the v0.18.0 release went out (PR #30 + #31
merged, tag pushed, npm publish completed):

- Row 6.12 ❌ → ✅ with shipped-state notes. Original audit-only scope
  expanded into "audit + close every gap surfaced + theming overhaul"
  in the same PR; preserved the original description for reference.
- Row 6.26 ❌ → ✅ with the resolved design (seed + user override + no
  remote registry; single edit surface in Settings → Model registry).
  Resolves the open questions from the original draft.
- Iter-6 active-set + headline summary lines updated: 6.20, 6.12, 6.26
  marked shipped; remaining set is now 6.9, 6.11, 6.13, 6.18, 6.24
  (+ 6.19 partial).

cli-usage.md: rewrote step 4 of "What `cfcf init` does" to describe
the new numbered model picker driven by the per-adapter registry,
with a pointer to Settings → Model registry for augmenting the list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant