feat(subagents): per-skill flash/pro override + Settings UI#1632
Merged
Conversation
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
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
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.
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.
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
.mdfrontmatter.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:
config.jsonsubagentModels?: Record<skillName, "flash" | "pro">fieldsrc/config.tsloadSubagentModels()/saveSubagentModels()helpers (filter invalid values, drop the field when empty)src/skills.tsSkillStoreacceptssubagentModelsin its constructor;applyModelOverriderewrites onlyrunAs: subagententries (inline skills with the same name stay untouched)src/tools/skills.tsregisterSkillTools(...subagentModels)plumbed throughsrc/code/setup.tsbuildCodeToolsetreadsloadSubagentModels()so the spawn site picks up the live valuesrc/cli/commands/desktop.tssettings_saveaccepts the field, persists viasaveSubagentModels, re-emits the skill list so the UI badge stays in sync;emitSettingscarries the current mapsrc/server/api/settings.tsPOST /api/settingsvalidates the shape; GET surfaces the current map;appliesAt.subagentModels = "next-skill-run"src/server/api/skills.tsdesktop/+dashboard/protocol.tsSettingsEvent+SettingsPatchgain the fielddesktop/+dashboard/App.tsxSettingstype +$settingsreducer mappingdesktop/+dashboard/ui/settings.tsxPageSkillsrenders a<select flash | pro>per subagent skilldesktop/+dashboard/i18n EN + zh-CNsubagentModelFlash/subagentModelPro/subagentModelHintdashboard/lib/tauri-bridge.tsemitServerSettingscarriessubagentModelsfromGET /api/settingsflash/promap todeepseek-v4-flash/deepseek-v4-proinsideSkillStore(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.
/testships asrunAs: 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 undersubagentModels: round-trip, filter unknown values, clear-all removes the fieldtests/skills.test.ts— 4 new cases undersubagentModels override: builtin gets remapped to deepseek-v4-pro, inlinetestskill stays untouched even when its name is in the override map, frontmattermodel:still wins when override is absent, override beats frontmatter when both are setVerify
npm run typecheck(root + dashboard) cleannpm run lintclean (only pre-existing welcome-banner warning)npm run test— 3583 pass (3576 baseline + 7 new)npm run verify— full build + lint + typecheck + tests green/explore, save closes; restart app; pick re-renders as "pro"; invoke/exploreand confirm spawn uses deepseek-v4-pro via the usage logPOST /api/settingsNet 20 files, +272 / -6.
Next PR in the series: postmortem / per-turn telemetry.