feat(tui): expose VS Code Copilot SDD profiles in welcome and profile screens#509
Closed
Snakeblack wants to merge 12 commits into
Closed
feat(tui): expose VS Code Copilot SDD profiles in welcome and profile screens#509Snakeblack wants to merge 12 commits into
Snakeblack wants to merge 12 commits into
Conversation
Enable per-phase sub-agents for VS Code Copilot via .agent.md files,
matching the multi-mode capability already shipped for OpenCode, Cursor
and Kiro.
- VS Code adapter now reports SupportsSubAgents() == true and exposes
SubAgentsDir(homeDir) == ~/.copilot/agents/ plus EmbeddedSubAgentsDir()
== "vscode/agents".
- 10 embedded .agent.md templates under internal/assets/vscode/agents/
(one per SDD phase, sdd-init through sdd-onboard). Templates omit the
model field on the default set so Copilot uses the user's default
model.
- New profile generator GenerateVSCodeProfileFiles(profile, agentsDir)
([]string, error) reads templates, resolves {{VSC_MODEL}} via a
provider/model → Copilot display name table (vscModelEntries) and
writes suffixed files atomically.
- inject.go step 2c writes named-profile files for VS Code Copilot
when SDD mode is multi and a non-default profile is configured;
step-3c handles the default unsuffixed set unchanged.
- RemoveVSCodeProfileAgents(agentsDir, profileName) removes the 10
suffixed phase files for a named profile; default profile rejected,
non-gentle-ai files preserved.
- Post-injection verification extended to recognize the .agent.md
extension and validate sdd-apply.agent.md plus sdd-verify.agent.md
are non-empty.
TUI integration (welcome menu entry, profile create/edit screens, VS
Code-specific model picker) lands in a separate PR.
… screens
Make the VS Code Copilot SDD multi-mode profiles reachable from the
gentle-ai wizard, mirroring the OpenCode profile flow.
- New backend helper `vscode.DetectVSCodeProfiles(agentsDir)` scans
`~/.copilot/agents/sdd-*-{name}.agent.md` and returns deduplicated
profile names. Missing directory is not an error.
- New `Model.hasDetectedVSCode()`, `Model.VSCodeProfileList`, and
`Model.ActiveProfileAdapter` thread the active adapter through the
shared profile screens.
- Welcome menu shows a new "VS Code SDD Profiles (N)" entry next to
the existing OpenCode one when VS Code Copilot is detected.
- `RenderProfiles` and `RenderProfileDelete` take an adapter label /
isVSCode flag and adapt their wording (no more hardcoded
"OpenCode SDD Profiles" / "opencode.json").
- Profile create for VS Code uses a new `VSCodeModelPickerState`
driven by the static `vscModelEntries` table (9 known Copilot
models). Per-phase model assignment works exactly as for OpenCode,
with the model field resolved via `VSCodeModelID`.
- Profile create/delete for VS Code bypass the sync pipeline and
call `vscode.GenerateVSCodeProfileFiles` /
`vscode.RemoveVSCodeProfileAgents` directly — VS Code profiles
are file-based, not JSON-merged.
- `SyncDoneMsg` and `ScreenProfiles` entry refresh both profile
lists so badges stay accurate across both adapters.
Test coverage:
- `internal/agents/vscode/vscode_profiles_detect_test.go` — 5 cases
(happy path, empty/missing dir, non-SDD files filtered, default
unsuffixed files excluded).
- `internal/tui/model_profiles_vscode_test.go` — TUI integration
tests for adapter detection, menu rendering, ActiveProfileAdapter
routing, file generation, deletion (no sync), and adapter-aware
rendering for both OpenCode and VS Code.
Stacked on feat/vscode-copilot-sdd-multimode (PR Gentleman-Programming#1 backend).
Add two regression tests that explicitly invoke Inject() twice with
identical inputs and assert:
1. The second run reports InjectionResult.Changed = false.
2. The file inventory in ~/.copilot/agents/ is unchanged (same set,
same modification times) — proving filemerge.WriteFileAtomic's
content-equality short-circuit holds across the full VS Code
injection path.
Coverage:
- Default profile path (step 3c only, 10 files).
- Named profile path (step 2c writes 10 suffixed files on top of
step 3c's 10 defaults — total 20 files).
These tests blind the recent fix at inject.go:456 (changed = changed
|| len(profileFiles) > 0) against future regressions where a
contributor might re-introduce the unconditional Changed=true that
broke the idempotency contract.
…ilot-tui-profiles
The VS Code model picker previously listed only a hardcoded set of 9
display names from vscModelEntries. That static list:
- missed models GitHub Copilot exposes today (e.g. newer Claude
revisions, GPT-5 family, etc.) and would silently fall behind
whenever Copilot adds a model;
- stored assignments as {ProviderID: "copilot", ModelID: <display>},
which does not match what VSCodeModelID expects when resolving
{{VSC_MODEL}} during injection (it matches by ID substring against
vscModelEntries, then falls back to ProviderID/ModelID).
This change makes VSCodeModelPickerState load its catalog from the same
OpenCode models cache (~/.cache/opencode/models.json) that the OpenCode
profile picker uses, restricted to the `github-copilot` provider entry.
The picker now reflects whatever Copilot currently advertises and the
catalog refreshes whenever the user runs `opencode sync`.
Changes:
- screens.VSCodeModelPickerState gains Models []opencode.Model and
ConfigWarning string fields populated at construction time via the
new screens.NewVSCodeModelPickerState(cachePath) helper.
- HandleVSCodeModelPickerNav stores assignments as
{ProviderID: "github-copilot", ModelID: <real model.ID>} so
inject.go's VSCModelID substring matcher resolves them correctly.
- When the cache is missing or lacks a github-copilot entry, the
picker shows a "run opencode sync" warning instead of failing.
- Model.go entry points (handleProfileNameInput and the edit-mode
branch of confirmProfileCreate) initialize the picker via the new
helper, with the OpenCode-only branch unchanged.
- Drop now-unused exports VSCodeStaticModels and VSCodeModelEntry
from internal/agents/vscode/vscode_profiles.go.
User-visible outcome: when creating a VS Code SDD profile and assigning
per-phase models, the picker shows the full Copilot catalog the user
sees in the install wizard, not a static 9-item list.
The vscode-sdd-profiles-research.md doc was a working/exploration artifact useful during design but does not belong in the shipped codebase. The architectural decisions it captured are reflected in proposal/spec/design/tasks (kept in engram and locally in openspec/changes/, which is gitignored).
…ilot-tui-profiles
…ates agents VS Code Copilot scans both \`.agent.md\` (its native format) and Claude \`.md\` agent files in parallel. When a user installs SDD multi-mode for both VS Code Copilot and Claude Code through the gentle-ai wizard, the 8 sub-agent phases that both adapters ship (sdd-apply, sdd-archive, sdd-design, sdd-explore, sdd-propose, sdd-spec, sdd-tasks, sdd-verify) appear twice in the VS Code Agent customizations panel. This is not a bug — each agent file is correct and functional in its own host — but it looks broken to a user who sees \"sdd-verify\" listed twice and assumes gentle-ai wrote duplicates. The case was traced down in PR Gentleman-Programming#505 manual testing. Changes: - New ScreenSDDDuplicateAgentsWarning with render function that lists exactly the 8 phases that duplicate (sdd-init and sdd-onboard are excluded because the Claude adapter does not ship them as sub-agents). - shouldWarnAboutDuplicateAgents() returns true when SDD is selected AND both AgentVSCodeCopilot and AgentClaudeCode are in the agent set. Extensible: add other Claude-format adapters to the check as they are introduced. - Wired into the ScreenSDDMode handler: when SDDModeMulti is chosen and the warning condition holds, the warning screen is shown before the normal post-SDDMode flow (ModelPicker / StrictTDD / etc.). - Extracted advanceFromSDDModeSelection() helper so both the SDDMode handler (when no warning is required) and the warning's \"Continue anyway\" path can share the same forward navigation logic. - Warning screen offers two options: \"Continue anyway\" resumes the normal flow; \"Back to adapter selection\" returns to ScreenSDDMode so the user can unselect one adapter. Test coverage: - TestShouldWarnAboutDuplicateAgents (7 subtests) covers the detection helper across all relevant adapter combinations. - TestSDDMode_TriggersDuplicateAgentsWarning verifies that selecting multi-mode with the offending combination routes to the warning. - TestSDDMode_NoWarningWhenNotDuplicating verifies that a benign combination skips the warning. - TestSDDDuplicateAgentsWarning_ContinueAdvances and TestSDDDuplicateAgentsWarning_BackReturnsToSDDMode cover both branches of the warning handler. - TestRenderSDDDuplicateAgentsWarning_ListsExpectedPhases pins the rendered phase list to the contract so a future contributor cannot silently drop or add a phase.
Without an orchestrator template, SDD multi-mode on VS Code Copilot
relies on the default chat agent inferring the phase sequence from
sub-agent description fields alone. Weak Copilot models routinely
skip phases or reorder them, breaking the SDD contract.
This change adds an explicit orchestrator template that mirrors the
pattern OpenCode, Claude Code, and Kiro already use:
- New internal/assets/vscode/agents/sdd-orchestrator.agent.md
embedded template with tools: ['agent'], an agents: whitelist of
the 10 phases, user-invocable: true, and a body that documents
the strict explore → propose → spec → design → tasks → apply →
verify → archive sequence plus the init / onboard utility flows.
- {{VSC_PROFILE_SUFFIX}} placeholder so the same template serves
both the default (unsuffixed) install and named profiles where
every phase reference is suffixed with -{name}.
- vscode.OrchestratorPhase constant + generateOrchestratorAgent()
helper that renders the orchestrator inline for named profiles
(separate from the embedded template path used by 3c).
- GenerateVSCodeProfileFiles now writes 11 files per profile
(orchestrator + 10 phases). RemoveVSCodeProfileAgents already
matched the suffix pattern, so it cleans up the orchestrator
automatically.
- inject.go step 3c resolves {{VSC_PROFILE_SUFFIX}} to empty for
the default set; post-check conditionally validates the
orchestrator file only when the adapter ships one (VS Code does,
Claude Code does not — Claude uses CLAUDE.md as the root
orchestrator prompt instead of a separate agent file).
Tests:
- TestGenerateAgentFile_Orchestrator_DefaultProfile_HasAllRequiredFields
pins the YAML contract (tools, agents, user-invocable, etc.).
- TestGenerateAgentFile_Orchestrator_NamedProfile_SuffixesAgentNames
proves the agents: whitelist AND body references get suffixed in
lockstep — without that, the orchestrator would dispatch to
nonexistent agents.
- TestGenerateAgentFile_Orchestrator_OrchestratorModelAssignment
covers three model resolution paths (omit / known / fallback).
- TestGenerateVSCodeProfileFiles_IncludesOrchestrator regression
guard against the previous 10-file design.
- TestRemoveVSCodeProfileAgents_AlsoRemovesOrchestrator regression
guard against the orchestrator lingering after delete.
- Existing idempotency tests updated for the new counts (11 default,
22 with one named profile).
Docs:
- docs/prd-vscode-profiles.md captures the full product spec in
English, mirroring the format of docs/prd-opencode-profiles.md.
Covers problem statement (June 2026 AI Credits transition +
enterprise lock-in), vision, scope, detailed requirements,
technical design, UX flows, edge cases, and open questions.
…ilot-tui-profiles
- Add VSCodeOrchestratorPhase const (re-export of vscodeagent.OrchestratorPhase) - Prepend orchestrator as Row 0 in VSCodeModelRows(); shift Set-all to Row 1, SDD phases to rows 2-11 (12 rows total, option count is now 14) - Update HandleVSCodeModelPickerNav: Row 0 assigns only to sdd-orchestrator key, Row 1 (Set all) assigns to 10 sub-agents but NOT orchestrator, rows 2-11 assign to individual phases - Update confirmVSCodeProfileCreateStep1: extract OrchestratorModel from assignments on Continue, copy only phase keys to PhaseAssignments - Add vscode_model_picker_test.go: 10 tests for row layout, constant value, option count, and nav semantics
Adds user-invocable: false frontmatter to Claude agents to prevent them from showing up as duplicates in VS Code Copilot's chat menu when both tools are installed, while keeping them functional for Claude Code.
Contributor
|
Thanks for the contribution. I’m closing this PR because it does not link to an issue with Please follow the project process:
This keeps review scope clear and prevents CI/process failures. |
Contributor
|
Hey, thanks for contributing! Per CONTRIBUTING.md, every PR needs an issue linked with Closing for now. To pick the work back up:
Sorry for the friction — the gate is automated, there's no workaround. Come back when it's ready! |
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.
🔗 Linked Issue
Closes #506
🏷️ PR Type
type:feature— New feature (non-breaking change that adds functionality)📝 Summary
ScreenProfiles/ScreenProfileCreate/ScreenProfileDeletescreens via anActiveProfileAdapterfield — single source of truth, no duplicated handlers.VSCodeModelPickerState) backed by the live OpenCode models cache (github-copilotprovider entry), with full per-phase assignment parity to OpenCode profiles — including an independentsdd-orchestratorrow separate from the 10 SDD phase rows.user-invocable: falseto the YAML frontmatter of Claude-specific agents. Copilot respects this masking flag and hides them from its UI, while Claude Code continues to use them seamlessly.Why now? Same context as the linked issue and PR #505: with Copilot moving to AI Credits in June, per-phase model assignment is the cost lever for enterprise teams already standardized on Copilot + VS Code. Also fixes crucial agent duplication bugs where both environments operate in the same workspace.
📂 Changes
internal/assets/claude/agents/*.mduser-invocable: falseto frontmatter to mask these agents from VS Code Copilot's@menu, fixing duplication and model incompatibility errors.internal/agents/vscode/vscode_profiles.goDetectVSCodeProfiles(agentsDir)— scanssdd-*-{name}.agent.md, dedupes, returns sorted[]model.Profile. Missing dir = empty (not error).internal/agents/vscode/vscode_profiles_detect_test.gointernal/tui/model.gohasDetectedVSCode(),vscodeAgentsDirFn,readVSCodeProfilesFn,VSCodeProfileList,ActiveProfileAdapter. ScreenProfiles / ProfileCreate / ProfileDelete handlers branch on adapter. VS Code create/delete callGenerateVSCodeProfileFiles/RemoveVSCodeProfileAgentsdirectly (no sync).confirmVSCodeProfileCreateStep1extractsOrchestratorModelfrom assignments map on Continue; copies only phase-keyed entries intoPhaseAssignments.internal/tui/model_profiles_vscode_test.gointernal/tui/screens/welcome.goshowVSCodeProfiles bool, vscodeProfileCount intparams. New menu entryVS Code SDD Profiles (N)next to OpenCode.internal/tui/screens/profiles.goRenderProfilestakesadapterLabel string— title and subtitle adapt.internal/tui/screens/profile_delete.goRenderProfileDeletetakesisVSCode bool— wording adapts (opencode.jsonvs~/.copilot/agents/), button label adapts (Delete & SyncvsDelete).internal/tui/screens/vscode_model_picker.goVSCodeModelPickerState+RenderVSCodeModelPicker. Reads live Copilot model catalog from OpenCode cache (opencode.LoadModels+FilterModelsForSDD). ExportsVSCodeOrchestratorPhaseconst.VSCodeModelRows()returns 12 rows: Row 0 =sdd-orchestrator, Row 1 = "Set all phases", Rows 2–11 = 10 SDD phase executors.HandleVSCodeModelPickerNaventer-key: Row 0 assigns only to orchestrator key; Row 1 ("Set all") assigns to 10 sub-agents but NOT orchestrator; Rows 2–11 assign to individual phases.internal/tui/screens/vscode_model_picker_test.goVSCodeOrchestratorPhasevalue,VSCodeModelPickerOptionCount, and nav semantics for all three row types (orchestrator isolation, Set-all does not touch orchestrator, individual phase assignment).internal/tui/screens/{welcome,profiles,profile_delete}_test.gointernal/tui/model_test.goWelcomeOptionscall sites for new params.🧪 Test Plan
Unit tests (focused)
Result: all PASS locally.
Static analysis
Pre-existing test failures (NOT caused by this PR)
Three tests fail when running
go test ./...on this branch:TestStartUninstall_FullRemoveHomebrewManagedBinaryAddsManualAction,TestPresetSelectionNextScreenFlowMatrix,TestCustomPresetPostComponentFlowMatrix. Verified by checking outmaindirectly — these same tests fail onmainwith the same error signatures (golden file CRLF mismatch on Windows). Pre-existing on upstream, unrelated to VS Code or profiles.cd e2e && ./docker-test.sh) — will run in CI🤖 Automated Checks
model_profiles_vscode_test.go~300 LOC,vscode_model_picker_test.go~170 LOC) and the picker screen (~250 LOC). Requestingsize:exception— production logic alone is ~600 LOC; most of the diff is test coverage required to keep the adapter branching safe.Closes #506status:approvedstatus:needs-review— submitter has no maintainer permissionstype:*Labeltype:featureneeds maintainer to applymainwill still show (documented above)✅ Contributor Checklist
size:exceptiontype:featurelabel — needs maintainermain)Co-Authored-Bytrailers💬 Notes for Reviewers
ActiveProfileAdapter model.AgentIDfield inModelthreads the active adapter through the shared screens. The screens take an adapter label (OpenCode/VS Code) orisVSCode bool— no duplicated screen constants for list/create/delete. The only adapter-specific screen isVSCodeModelPickerState, because the OpenCode picker reads from a dynamic cache and isn't reusable.sdd-orchestrator(Row 0) is intentionally separate from "Set all phases" (Row 1). Selecting "Set all" bulk-assigns the 10 sub-agent phases but deliberately does not touch the orchestrator model — matching the design intention that the orchestrator and the phase runners can be different models (e.g. Sonnet for orchestration, GPT-4o-mini for cheaper phases). This is enforced in bothHandleVSCodeModelPickerNavandconfirmVSCodeProfileCreateStep1.~/.opencode/models.json) filtered togithub-copilot. If the cache is missing or the provider absent, aConfigWarningis surfaced inline — no crash, no empty list silently..agent.mdfiles directly. The sync pipeline is invoked only for OpenCode.GenerateVSCodeProfileFilesreturns only the paths that actually changed (viafilemerge.WriteFileAtomic). Re-confirming an unchanged profile does not flipInjectionResult.Changed.refreshUninstallProfiles; profile edit UX (technically works as re-generate but not polished).main: documented above. Happy to open a tracking issue for them if useful — not blockers for this PR.