feat(executors): Fable/Claude-5 model family, ultracode effort level, free-text model entry + effort for preset models#29
Merged
Conversation
… free-text model entry + effort for preset models
…ke model_supports_effort case-insensitive Council round 1 hardening for the Claude effort changes: - Fix headless_arg doc comment (2.1.154 hard-rejects unknown --effort values, not warn-and-ignore) to match verified CLI behavior and the match-arm comment. - Add headless_arg_matches_as_ref_except_ultracode: pins that headless mode emits the same wire value as the interactive path for every tier except the deliberate ultracode->xhigh delta, so a future strum serialization change can't silently diverge the two paths. - Add effort_wire_strings_are_stable golden test enforcing the locked ADD-only never-rename policy for ClaudeEffort (serialized into profiles.json + stored ExecutorActions history) at CI time instead of by comment alone. - Make model_supports_effort case-insensitive so a free-text/custom id like Claude-Fable-5 is gated identically in the UI (TS mirror lowercases) and at interactive launch, closing a silent --effort drop; add reciprocal sync comment and a boundary test (haiku stays excluded).
…code /simplify pass on the effort/model changes (behavior-preserving): - Derive the picker's effort_options from ClaudeEffort::VARIANTS (strum VariantNames) instead of a hand-maintained string array, so a future effort tier can't be silently missing from the list; pin the derived wire strings in a test. - Drop the redundant reasoning_label "ultracode" arm: the Title-case fallback already yields "Ultracode" (like low/medium/high/max); only xhigh needs an override. - ModelSelectorPopover: remove the vestigial showSearch variable (always true since MODEL_LIST_PAGE_SIZE was removed) and render the search box directly; document the deliberate flat-picker-only scope of the custom-model entry. Kept the explicit exhaustive headless_arg match rather than collapsing to `_ => self.as_ref()`: the compile-time exhaustiveness forces every future variant to make a headless decision at the version-skew boundary (a wildcard would let a future ultracode-like alias silently reach the pinned headless CLI).
… to Claude Addresses a Codex xhigh review of the picker changes: - appendPresetModel now also injects the current override model (which may be a free-text custom id the user typed), not just the profile preset, so a custom effort-capable Claude id shows an effort selector instead of silently having none (and headless launches then carry the chosen --effort). - Gate the reasoning-options fallback behind an explicit enableEffortFallback flag, set only for CLAUDE_CODE. Keeps the shared appendPresetModel helper executor-agnostic: previously any provider-less executor whose id happened to contain opus/sonnet/fable would inherit effort options it does not support. - Extend the vitest spec: custom-id inheritance + fallback-disabled cases.
code-review (xhigh) caught that the free-text "Use <query>" entry and the always-visible search box leaked to every flat (no-provider) executor — Codex, Gemini, Copilot, Droid, Qwen, Cursor are all flat pickers, not just Claude. That let a user type an arbitrary model id for executors whose downstream does not tolerate one (only Claude's was verified), and a bare typed id has no provider so it produced a malformed provider-less override; Codex custom models would also silently lose their reasoning options. - Add an explicit `allowCustomModel` prop to ModelSelectorPopover (default false); the container passes it only for CLAUDE_CODE. - Custom-entry row and the forced search box are now gated on it; other flat pickers keep their exact prior behavior (search only once the list exceeds MODEL_LIST_PAGE_SIZE, which is restored — the earlier /simplify collapse of showSearch assumed a single flat consumer, which was wrong). - Fold the effort-fallback + custom-entry gating into one `isClaude` flag.
Council round 2: the override-model injection was unconditional (only the effort-fallback flag was gated on isClaude), so a non-Claude executor with a stale override id would still get it injected as a provider-less flat model. Benign (non-Claude pickers can't create free-text ids, so no malformed launch), but it widened behavior past the Claude-only intent. Gate the override injection on isClaude too; non-Claude executors now get exactly the pre-PR preset-only injection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Claude executor picker was missing the Fable / Claude-5 family, had no way to enter a custom model id, and could not attach an effort level to preset-injected models (e.g. Miguel's
claude-fable-5default surfaced with empty reasoning options). Two real bugs compounded this: (1)model_supports_effort()excludedfable, so CLI-mode launches ofclaude-fable-5silently dropped--effort; (2) there was noultracodeeffort level, and any UI-only effort id not in theClaudeEffortenum is silently discarded byapply_overrides().What changed
Backend (
crates/executors):fable(alias, "Fable") to the top of the model list indefault_discovered_options(), newest-first: Fable, Opus, Opus (1M context), Sonnet, Haiku.model_supports_effort()to includefable, so effort is no longer dropped for Fable models at interactive CLI launch.Ultracodevariant toClaudeEffort(serde/strum lowercase →ultracodeon the wire), appended to the discovered effort options, with rank 6 + label "Ultracode" inmodel_selector.rs.ultracodealias) passes--effort ultracodeverbatim. Headless path mapsUltracode → xhighvia a newClaudeEffort::headless_arg()because the pinned headless CLI (@2.1.154) hard-rejectsultracodeand its dynamic-workflow orchestration is interactive-only.shared/types.tsandshared/schemas/claude_code.json(flat string enum preserved — the variant note is a plain//comment so the schema shape is unchanged for the settings agent-config editor).Frontend:
ModelSelectorPopover/ModelList: free-text model entry — when the trimmed search query matches no model, aUse "<query>"row is offered that selects the arbitrary id. The flat (no-provider) picker now always shows the filter/entry box.appendPresetModel(): gated reasoning-options fallback — an injected/custom effort-capable Claude id (opus/sonnet/fable, provider-less) inherits the effort options from the first config model that has them, so Fable presets and custom ids can pick an effort (including ultracode). Provider-scoped models (codex path) are unaffected.modelSelector.useCustomi18n key across all locales.How verified
cargo test -p executors— 84 passed. New tests: fable+xhigh red/green for themodel_supports_effortfix,ultracodeFromStr/AsRef round-trip,Ultracode.headless_arg() == "xhigh", interactive passthrough ofultracode.npx -y @anthropic-ai/claude-code@2.1.154 --effort ultracode --version→error: option '--effort <level>' argument 'ultracode' is invalid. It must be one of: low, medium, high, xhigh, max— confirms the headless→xhigh mapping is mandatory.pnpm run generate-types→ultracodepresent inshared/types.tsandshared/schemas/claude_code.json; schema stays a flat enum (one-line diff).modelSelector.test.ts(5 passed) covering the gated reasoning-options fallback.pnpm run local-web:check,web-core:check,ui:checkpass;ui:lint,local-web:lint, andcheck-unused-i18n-keys.mjsclean;cargo clippy -p executorsclean;pnpm run formatrun.fable[1m]validity (see Risks); actual workflow-orchestration behavior ofultracodein a live session.Risks / notes
fable[1m]was intentionally left out: onlyopus/sonnet[1m]handling is evidenced in the 2.1.204 binary; not adding an unverified entry.default_model/CLI_DEFAULT_MODELwere left asopusper the brief — flipping the repo fallback tofableis a separate conscious call (Miguel's profile already defaults to fable). Possible follow-up.@2.1.154) was NOT bumped (blast radius: loop automation + title_gen). Full model ids likeclaude-fable-5pass through on 2.1.154 regardless; only the barefablealias needs ≥2.1.204, which affects headless spawns only.ClaudeEffortis serialized in profiles.json / stored ExecutorActions — this only ADDS a variant (backward compatible); nothing renamed or removed.normalize_claude_model_idonly fixes dottedclaude-*ids.ship-it battery
Automated multi-pass review battery run on this PR; all local checks green before each push. Follow-up commits:
3193fd57,44650e12,c90b75ca,987df99a,14a6046f.Council round 1 (mixed: Codex ×4 personas + Claude cross-check) — all CAUTION, no P0/BLOCK. Applied: corrected the
headless_argdoc comment (2.1.154 hard-rejects unknown--effort, not warn-and-ignore); mademodel_supports_effortcase-insensitive to faithfully mirror the TSmodelSupportsEffort(closes a mixed-case silent--effortdrop); addedheadless_arg_matches_as_ref_except_ultracode(pins headless == interactive wire value for every tier except the deliberate ultracode→xhigh delta) andeffort_wire_strings_are_stable(golden test enforcing the locked ADD-only never-rename policy at CI). Rejected: tolerant#[serde(other)]deserialization (would reintroduce the silent-drop this PR fixes; downgrade isn't a product requirement) and coupling the genericmodel_selectorranker to the Claude enum (inverts dependency direction)./simplify (4 cleanup agents) — applied: derive the picker effort list from
ClaudeEffort::VARIANTS(strum, verified to honorserialize_all) instead of a hand-maintained array, so a future tier can't be silently missing; drop the redundantreasoning_label("ultracode")arm (Title-case fallback already yields "Ultracode"). Rejected: collapsingheadless_argto_ => self.as_ref()(keeps compile-time exhaustiveness at the version-skew boundary) and extracting a shared row component (pre-existing triplication, out of diff scope).Codex + /code-review xhigh — Codex: injected the current override model (not just the profile preset) so a free-text custom effort-capable Claude id gets an effort selector, and gated the
appendPresetModeleffort fallback behind an explicit flag set only forCLAUDE_CODE. xhigh caught the key defect: the free-text custom-entry row + always-on search box had leaked to every flat (no-provider) executor (Codex, Gemini, Copilot, Droid, Qwen, Cursor), not just Claude — a footgun (arbitrary/typo ids for executors whose downstream wasn't verified; Codex customs would lose reasoning options). Fixed by gating both behind a newallowCustomModelprop set only for Claude; non-Claude flat pickers keep exact prior behavior (MODEL_LIST_PAGE_SIZErestored). Skipped (minor, noted): a custom Claude id isn't written to the recently-used LRU (it still persists as the active selection).Council round 2 (mixed, updated diff) — 4 APPROVE + 1 CAUTION, no P0/P1/BLOCK. Applied: gated the override-model injection on
isClaudetoo, so non-Claude executors get exactly the pre-PR preset-only injection. Rejected: wiring the new Vitest spec into CI (pre-existing repo-wide condition — vitest is intentionally not a devDep and tsc excludes*.test.ts; ran locally, 7 pass).CodeRabbit — CLI could not authenticate in this non-interactive environment (
environment_unsupported: no API key, no localhost-callback/stdin fallback) on both--plainand--agent. Deterministic env limitation, not a transient rate-limit, so per the fallback a Codex xhigh review was substituted on the final diff → no actionable findings.Verification:
cargo test -p executors(all pass, incl. new tests),cargo clippy -p executorsclean,pnpm run check(frontend tsc all green),npx vitest run modelSelector.test.ts(7 pass),pnpm run formatapplied. No new migrations; generatedshared/types.ts+shared/schemas/claude_code.jsonregenerated (not hand-edited). Locked decisions honored: nodefault_modelflip, no headless pin bump, ADD-only enum.