Skip to content

feat(subagents): per-skill flash/pro override + Settings UI#1632

Merged
esengine merged 4 commits into
mainfrom
feat/subagent-model-config
May 23, 2026
Merged

feat(subagents): per-skill flash/pro override + Settings UI#1632
esengine merged 4 commits into
mainfrom
feat/subagent-model-config

Conversation

@esengine

Copy link
Copy Markdown
Owner

PR 2 of the 3-step plan from earlier (remove auto / per-subagent model / postmortem). Adds a Settings → Skills selector so the user can tune individual subagent skills between flash and pro without editing skill .md frontmatter.

What

Subagent skills today take a single model from the model: line in their .md frontmatter. That means the four built-in subagents (/explore, /research, /review, /security-review) can't be retuned without shadowing them, and neither desktop nor web Settings had a surface for it.

This PR threads a per-skill override map through every layer:

Layer Change
config.json new subagentModels?: Record<skillName, "flash" | "pro"> field
src/config.ts loadSubagentModels() / saveSubagentModels() helpers (filter invalid values, drop the field when empty)
src/skills.ts SkillStore accepts subagentModels in its constructor; applyModelOverride rewrites only runAs: subagent entries (inline skills with the same name stay untouched)
src/tools/skills.ts registerSkillTools(...subagentModels) plumbed through
src/code/setup.ts buildCodeToolset reads loadSubagentModels() so the spawn site picks up the live value
src/cli/commands/desktop.ts settings_save accepts the field, persists via saveSubagentModels, re-emits the skill list so the UI badge stays in sync; emitSettings carries the current map
src/server/api/settings.ts POST /api/settings validates the shape; GET surfaces the current map; appliesAt.subagentModels = "next-skill-run"
src/server/api/skills.ts listing endpoint reads with override applied
desktop/ + dashboard/ protocol.ts SettingsEvent + SettingsPatch gain the field
desktop/ + dashboard/ App.tsx Settings type + $settings reducer mapping
desktop/ + dashboard/ ui/settings.tsx PageSkills renders a <select flash | pro> per subagent skill
desktop/ + dashboard/ i18n EN + zh-CN subagentModelFlash / subagentModelPro / subagentModelHint
dashboard/lib/tauri-bridge.ts emitServerSettings carries subagentModels from GET /api/settings

flash / pro map to deepseek-v4-flash / deepseek-v4-pro inside SkillStore (kept local to avoid pulling the CLI preset bundle in via cycle), so the config stays human-readable even if model ids swap later.

Inline skills (e.g. /test ships as runAs: inline) are intentionally untouched: the selector only renders for subagent skills, and the override map is filtered at apply time.

Tests

  • tests/config.test.ts — 3 new cases under subagentModels: round-trip, filter unknown values, clear-all removes the field
  • tests/skills.test.ts — 4 new cases under subagentModels override: builtin gets remapped to deepseek-v4-pro, inline test skill stays untouched even when its name is in the override map, frontmatter model: still wins when override is absent, override beats frontmatter when both are set

Verify

  • npm run typecheck (root + dashboard) clean
  • npm run lint clean (only pre-existing welcome-banner warning)
  • npm run test — 3583 pass (3576 baseline + 7 new)
  • npm run verify — full build + lint + typecheck + tests green
  • Manual desktop: Settings → Skills, pick "pro" on /explore, save closes; restart app; pick re-renders as "pro"; invoke /explore and confirm spawn uses deepseek-v4-pro via the usage log
  • Manual dashboard: same flow over POST /api/settings

Net 20 files, +272 / -6.

Next PR in the series: postmortem / per-turn telemetry.

Subagent skills (`/explore`, `/research`, `/review`, `/security-review`
out of the box, plus any user-authored `runAs: subagent` skill) had only
one knob for picking the spawn model: a `model:` line in the .md
frontmatter. That meant users couldn't tune the four built-ins without
shadowing them by copying the bodies into a custom file, and the desktop
/ web settings had no surface for it at all.

Adds a per-skill `flash | pro` selector:
- `config.json` gets `subagentModels?: Record<skillName, "flash" | "pro">`
  with `load`/`save` helpers that filter to valid values
- `SkillStore` accepts `subagentModels` in its constructor and applies
  the override only to `runAs: subagent` entries (inline skills with
  the same name are untouched, frontmatter `model:` is overridden
  when an entry exists)
- `registerSkillTools` plumbs it through; `buildCodeToolset` reads
  `loadSubagentModels()` so the runtime path picks up the live value
- desktop `settings_save` accepts the field, persists via
  `saveSubagentModels`, re-emits the skill list so the UI's
  inline-rendered model badge stays in sync
- dashboard `POST /api/settings` validates the same shape, GET surfaces
  the current map, `appliesAt.subagentModels` = "next-skill-run"
- both desktop and dashboard Settings → Skills page render a
  `<select flash | pro>` per subagent skill — inline skills get no
  selector

`flash`/`pro` map to `deepseek-v4-flash`/`deepseek-v4-pro` inside
`SkillStore` so the config stays human-readable across model id swaps.

Tests:
- config: round-trip, filter unknown values, clear-all removes field
- SkillStore: override applies to builtins, doesn't touch inline `test`,
  override beats frontmatter `model:` when both set, frontmatter still
  wins absent override
Comment thread src/server/api/settings.ts Fixed
reasonix added 3 commits May 23, 2026 08:23
CodeQL js/remote-property-injection — `__proto__` / `constructor` /
`prototype` in a POST /api/settings body would land as own properties
on the sanitized map and persist into config.json.
The previous fix's explicit string blocklist wasn't recognized by
CodeQL's js/remote-property-injection rule as a barrier. Object.create(null)
is the canonical sanitizer — keeps the explicit blocklist so dangerous
keys still get dropped before persistence.
CodeQL js/remote-property-injection didn't accept the explicit blocklist
or Object.create(null) as a barrier. Map.set bypasses the writeAccess
sink CodeQL tracks, and Object.fromEntries constructs via
[[CreateDataProperty]] which the rule trusts.
@esengine esengine merged commit 7d817f8 into main May 23, 2026
5 of 6 checks passed
@esengine esengine deleted the feat/subagent-model-config branch May 23, 2026 16:53
esengine pushed a commit that referenced this pull request May 24, 2026
…moved, persisted usage stats, plan dispatch gate

Headline themes:
- Desktop: bundle the CLI-hosted React dashboard, retire Tauri+Preact duplicate (#1418)
- Config: drop preset abstraction; flash/pro are direct model selections (#1657, #1630)
- Stats: persist cumulative usage to session meta + auto-restore on startup (#1667, #1680, #1643, #1628)
- Plans: editMode="plan" enforced at the ToolRegistry dispatch gate (#1681); step advance fix (#1629)
- Context: fold once at turn start, drop pre-flight + byte-ceiling (#1642, #1646); collapsible compacted card (#1649)
- Subagents: per-skill flash/pro override + Settings UI (#1632)
- Desktop polish: sidebar drag-resize (#1688), responsive collapse (#1585), copy/edit overlay + msg-history nav (#1645), Esc closes modal not turn (#1685), QQ tab isolation (#1672), DiffCard for edits (#1662), theme-aware highlighting (#1655), system events toggle (#1654/#1650), macOS TCC inheritance (#1614), dashboard.enabled (#1612)
- Dashboard polish: persistent session URL (#1586, #1589, #1599), theme-aware highlighting (#1664), IME confirm-enter guard (#1689), code-fence lang fix (#1677), vendor chunk split (#1587), markdown table h-scroll (#1562)
- TUI: Alt+S input stash/recall; static history isolated from input rerenders (#1635); legacy mouse drop (#1637, #1648); multi-edit gated in review (#1647)
- Diff: SplitDiff column border holds under CJK (#1686)
- MCP: workspace roots passed to servers (#1625); codeCommand honors mcpServers (#1603)
- Config plumbing: (baseUrl, apiKey) resolved as a tuple (#1658); stale model id self-heal (#1663)

See CHANGELOG for the full list.
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.

2 participants