Skip to content

feat(vscode): add TUI model flow and docs#740

Open
Snakeblack wants to merge 4 commits into
Gentleman-Programming:mainfrom
Snakeblack:feat/vscode-copilot-subagents-pr3
Open

feat(vscode): add TUI model flow and docs#740
Snakeblack wants to merge 4 commits into
Gentleman-Programming:mainfrom
Snakeblack:feat/vscode-copilot-subagents-pr3

Conversation

@Snakeblack

@Snakeblack Snakeblack commented Jun 2, 2026

Copy link
Copy Markdown

Linked Issue

Closes #504

Depends on: #738
Related context: #731, #708


PR Type

What kind of change does this PR introduce?

  • type:bug - Bug fix (non-breaking change that fixes an issue)
  • type:feature - New feature (non-breaking change that adds functionality)
  • type:docs - Documentation only
  • type:refactor - Code refactoring (no functional changes)
  • type:chore - Build, CI, or tooling changes
  • type:breaking-change - Breaking change (fix or feature that changes existing behavior)

Chain Context

PR 4 of the VS Code Copilot SDD subagents stack.

#708 PR1 native VS Code `.agent.md` agents
   ->
#731 PR2 VS Code model assignment foundation
   ->
#738 PR3 VS Code Copilot agent install hardening
   ->
[THIS] PR4 TUI + docs final

This PR depends on #738. Because the stack is currently opened from a contributor fork and upstream dependency branches are not available as base branches, this PR targets main.

Focused review diff for the PR4 work unit before the main conflict-resolution merge:
Snakeblack/gentle-ai@fix/vscode-copilot-agent-hardening...27e6100

Conflict-resolution merge commit: 9aa7eee (origin/main v1.34.0).


Summary

  • Adds VS Code Copilot model assignment flow to the TUI, including coordinator and per-phase SDD agent choices.
  • Keeps VS Code UX aligned with the hardening PR: only sdd-orchestrator should be visible in the native VS Code dropdown, while the TUI can configure phase models.
  • Updates user-facing docs for VS Code support layers, intended usage, agents, and platform behavior.

Changes

File / Area What Changed
internal/tui/model.go Wires VS Code model assignment state through the TUI flow
internal/tui/screens/model_picker.go Adds VS Code-aware coordinator and per-phase model picker behavior
internal/tui/**/*_test.go Adds coverage for VS Code model picker flows and persisted choices
README.md, docs/agents.md Documents VS Code agents, skills, instructions, and model configuration
docs/intended-usage.md, docs/platforms.md Clarifies intended VS Code Copilot usage and platform support

Size Exception Rationale

This PR requests size:exception.

The focused PR4 diff is above the 400-line budget because it contains a cohesive user-facing slice: TUI state flow, picker behavior, regression tests, and final docs. Splitting tests or docs away from the TUI change would make review harder because the user contract is the behavior being documented.


Test Plan

Linux/LF full suite

go test ./...
go vet ./...

Focused review checks

git diff --check

Results:

  • Unit tests pass (go test ./...) in Linux/LF verification clone before PR creation
  • Unit tests pass (go test ./...) in Linux/LF verification clone after resolving the origin/main conflict
  • Static checks pass (go vet ./...) in Linux/LF verification clone before PR creation
  • Static checks pass (go vet ./...) in Linux/LF verification clone after resolving the origin/main conflict
  • E2E tests pass (cd e2e && ./docker-test.sh)
  • Manually tested locally

Note: local Windows full-suite runs still have known CRLF/env-sensitive golden noise unrelated to this PR. Final validation was done in a clean Linux/LF runner.


Automated Checks

Check Status Description
Check PR Cognitive Load Warning Requests size:exception; focused work unit is cohesive
Check Issue Reference Pending PR body contains Closes #504
Check Issue Has status:approved Pending #504 has status:approved
Check PR Has type:* Label Pending type:feature requested; maintainer label needed
Unit Tests Pending CI should run go test ./...
E2E Tests Pending CI should run Docker E2E if configured

Contributor Checklist

  • PR is linked to an issue with status:approved
  • PR stays within 400 changed lines, or I have requested/obtained maintainer-applied size:exception with rationale documented
  • I have added the appropriate type:* label to this PR - maintainer label needed (type:feature)
  • Unit tests pass (go test ./...) before PR creation and after conflict resolution
  • E2E tests pass (cd e2e && ./docker-test.sh)
  • I have updated documentation if necessary
  • My commits follow Conventional Commits format
  • My commits do not include Co-Authored-By trailers

Notes for Reviewers

The important UX distinction is intentional: the TUI can configure coordinator and per-phase VS Code models, but the native VS Code agent dropdown should expose only the user-facing sdd-orchestrator.

Summary by CodeRabbit

  • New Features

    • Added VS Code Copilot native sub-agent support with orchestrator-directed phase delegation for SDD workflows.
    • Expanded multi-mode SDD across VS Code Copilot, OpenCode, Kilo Code, Kiro IDE, and Pi platforms.
    • Added TUI configuration for VS Code Copilot model assignments per SDD phase.
  • Documentation

    • Updated README and agent docs with VS Code SDD capabilities matrix and configuration guidance.
    • Clarified multi-mode SDD as a cross-agent feature with per-platform implementation details.
  • Bug Fixes

    • Improved sync warning propagation and display.

@Snakeblack Snakeblack force-pushed the feat/vscode-copilot-subagents-pr3 branch from 9aa7eee to 13739b4 Compare June 14, 2026 23:05
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Activates VS Code Copilot native sub-agent SDD support: enables the adapter, embeds 11 .agent.md phase-agent assets, implements per-agent Copilot model assignment injection with cache validation and sync warnings, hides internal Claude agents from the VS Code picker, extends state/CLI/app/TUI plumbing for VSCodeModelAssignments, adds Windows-atomic file replace, and updates documentation.

Changes

VS Code Copilot SDD Native Sub-Agent Feature

Layer / File(s) Summary
VS Code adapter enablement and embedded agent assets
internal/agents/vscode/adapter.go, internal/agents/vscode/adapter_test.go, internal/assets/assets.go, internal/assets/vscode/agents/*.agent.md, internal/assets/claude/agents/sdd-*.md, internal/assets/claude/agents/jd-*.md
Enables SupportsSubAgents/SubAgentsDir/EmbeddedSubAgentsDir on the VS Code adapter, embeds the orchestrator and 10 phase .agent.md assets under vscode/agents, adds all:claude to the embed directive, and adds user-invocable: false to all Claude-internal sdd-*/jd-* agent frontmatters.
VS Code model assignment validation and agent frontmatter injection
internal/components/sdd/vscode_models.go, internal/components/sdd/vscode_models_test.go, internal/components/sdd/vscode_agent_visibility.go
Implements the closed assignment-key whitelist, Copilot-provider and tool-call validation, model cache/variant loading, effort checks, model: frontmatter rewriting in .agent.md files, and the hideManagedClaudeInternalAgentsForVSCode component that ensures user-invocable: false in ~/.claude/agents.
SDD inject.go VS Code wiring and InjectionResult.Warnings
internal/components/sdd/inject.go, internal/components/sdd/inject_test.go
Adds Warnings to InjectionResult and VS Code model fields to InjectOptions, calls renderVSCodeAgentModelAssignment per copied native agent, expands the critical-sub-agent check to .agent.md, calls hideManagedClaudeInternalAgentsForVSCode post-copy, and adds the sddOrchestratorContent VS Code support-layers helper.
Asset frontmatter contract tests
internal/assets/assets_test.go
Adds TestVSCodeNativeAgentAssetsFrontmatter (required/absent keys, deprecated aliases, phase tools, body sections) and TestClaudeInternalAgentAssetsAreHiddenFromVSCodePicker, with all supporting helpers.
Cross-platform atomic file replace
internal/components/filemerge/replace_default.go, internal/components/filemerge/replace_windows.go, internal/components/filemerge/writer.go, internal/components/filemerge/writer_test.go, go.mod
Splits os.Rename in WriteFileAtomic into build-tag-dispatched replaceFileAtomic (non-Windows os.Rename; Windows MoveFileEx with replace+write-through flags) and adds tests for both paths.
State and model data shapes
internal/model/selection.go, internal/model/selection_test.go, internal/state/state.go, internal/state/state_test.go
Adds VSCodeModelAssignments to Selection, SyncOverrides, and InstallState (with MergeAgents carry-forward), with round-trip, merge, and backward-compat test coverage.
app.go persistence and override plumbing
internal/app/app.go, internal/app/app_test.go
Propagates VSCodeModelAssignments through tuiExecute state writes, applyOverrides (whole-map replacement), loadPersistedAssignments (populate-if-empty), and persistAssignments (write-back), with override, populate, no-overwrite, and round-trip tests.
CLI sync wiring, SyncResult.Warnings, and reporting
internal/cli/sync.go, internal/cli/sync_test.go, internal/cli/run.go, internal/cli/install_test.go
Adds Warnings []string to SyncResult, wires warning accumulation through componentSyncStep, passes VSCodeModelAssignments into InjectOptions, restores from state on plain sync, deduplicates and renders warnings in RenderSyncReport, and validates VS Code assignment persistence and warning propagation in tests.
Uninstall selective removal test
internal/components/uninstall/service_test.go
Adds TestComponentOperationsSDD_VSCodeRemovesOnlyManagedAgentFiles verifying only managed VS Code agent files are deleted while user-authored agents remain.
TUI VS Code model picker screen
internal/tui/screens/model_picker.go, internal/tui/screens/model_config.go, internal/tui/screens/model_config_test.go, internal/tui/screens/vscode_model_picker_test.go
Adds ForVSCode field to ModelPickerState, exports VSCodeCoordinatorPhase, NewVSCodeModelPickerState, ModelPickerRowsForVSCode, and VSCodeSDDPhases; refactors applyAssignment* and renderPhaseList to be VS Code-aware; adds "Configure VS Code Copilot SDD models" to the model-config menu; and adds comprehensive VS Code picker unit tests.
TUI model.go VS Code flow integration
internal/tui/model.go, internal/tui/model_test.go
Integrates the VS Code picker into the TUI flow with initVSCodeModelPicker, continueAfterVSCodeModelPicker, backFromVSCodeModelPicker, and shouldShowVSCodeModelPickerScreen; routes preset-flow transitions; loads persisted assignments on model-config entry; updates ScreenModelPicker cursor/separator logic; adds new TUI navigation tests.
Documentation
README.md, docs/agents.md, docs/intended-usage.md, docs/platforms.md
Updates agent matrix, multi-mode SDD section, VS Code Copilot agent notes, Windows config paths, and sub-agents capability table to reflect native sub-agent support and per-phase model assignments.

Sequence Diagram(s)

sequenceDiagram
  participant TUI as TUI Model Picker
  participant App as app.go
  participant CLI as sync.go
  participant Inject as sdd/inject.go
  participant Models as vscode_models.go
  participant Visibility as vscode_agent_visibility.go
  participant FS as ~/.copilot/agents & ~/.claude/agents

  TUI->>App: Selection.VSCodeModelAssignments
  App->>App: persistAssignments → state.json VSCodeModelAssignments
  CLI->>CLI: RunSync: restore VSCodeModelAssignments from state.json
  CLI->>Inject: InjectOptions{VSCodeModelAssignments, CachePath, VariantsPath}
  Inject->>Models: renderVSCodeAgentModelAssignment per .agent.md
  Models->>Models: loadVSCodeModelCatalog(cachePath)
  Models->>Models: resolveVSCodeModelAssignment → label or warning
  Models->>Inject: renderedContent, warnings
  Inject->>FS: WriteFileAtomic → ~/.copilot/agents/sdd-*.agent.md
  Inject->>Visibility: hideManagedClaudeInternalAgentsForVSCode(homeDir)
  Visibility->>FS: rewrite user-invocable:false in ~/.claude/agents/sdd-*.md
  Inject-->>CLI: InjectionResult{Warnings}
  CLI->>CLI: dedupStrings → SyncResult.Warnings
  CLI-->>TUI: SyncResult with Warnings rendered in report
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Gentleman-Programming/gentle-ai#816: Extends the same VSCodeModelAssignments state/app/sync/inject persistence pathways for a related model-assignment feature, making the code paths directly overlapping with this PR.

Suggested labels

type:feature, size:exception

Suggested reviewers

  • Alan-TheGentleman
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs/agents.md (1)

16-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistency with README.md: Agent Matrix delegation model descriptor.

Line 16 shows "Full (runSubagent)" while README.md line 37 changed to "Full (native subagents)" for the same agent. These should be consistent across documentation. "runSubagent" is the internal VS Code Copilot API term; "native subagents" is the user-facing feature description. Consider aligning line 16 to match README for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/agents.md` at line 16, Change the delegation model descriptor for the VS
Code Copilot agent in the agent matrix table from "Full (runSubagent)" to "Full
(native subagents)" to match the user-facing terminology used consistently
across the documentation. The internal API term runSubagent should be replaced
with the clearer native subagents description.
internal/cli/run.go (1)

199-234: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

mergeExplicitAgentInstallState omits VS Code assignment merge branch.

The explicit install --agent ... merge path does not copy newState.VSCodeModelAssignments into merged state. VS Code model assignments built in newState are therefore not persisted for explicit-agent installs.

Suggested fix
 	if newState.ModelAssignments != nil {
 		merged.ModelAssignments = newState.ModelAssignments
 	}
+	if newState.VSCodeModelAssignments != nil {
+		merged.VSCodeModelAssignments = newState.VSCodeModelAssignments
+	}
 	if newState.ClaudeModelAssignments != nil {
 		merged.ClaudeModelAssignments = newState.ClaudeModelAssignments
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/run.go` around lines 199 - 234, The
mergeExplicitAgentInstallState function is missing a merge branch for VS Code
model assignments. After the existing assignment merge conditionals
(ModelAssignments, ClaudeModelAssignments, KiroModelAssignments,
CodexModelAssignments, etc.) and before the Persona assignment check, add a new
conditional block that checks if newState.VSCodeModelAssignments is not nil and
copies it to merged.VSCodeModelAssignments, following the same pattern as the
other assignment merges in the function.
internal/app/app.go (1)

682-751: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve nil-vs-empty clear semantics for VSCodeModelAssignments in persistence.

persistAssignments currently writes VS Code assignments only for non-empty maps, so an explicit clear (non-nil, empty map) never clears state.InstallState.VSCodeModelAssignments. That leaves stale assignments persisted and reloaded on later syncs.

Suggested fix
-	if len(selection.VSCodeModelAssignments) > 0 {
-		current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
-	}
+	if selection.VSCodeModelAssignments != nil {
+		if len(selection.VSCodeModelAssignments) > 0 {
+			current.VSCodeModelAssignments = modelAssignmentsToState(selection.VSCodeModelAssignments)
+		} else {
+			current.VSCodeModelAssignments = nil
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/app/app.go` around lines 682 - 751, The VSCodeModelAssignments
handling currently only checks if the length is greater than 0 and writes the
value, but it does not follow the nil-vs-empty semantics used for other
assignment fields like ModelAssignments, ClaudeModelAssignments, etc. Refactor
the VSCodeModelAssignments block to first check if
selection.VSCodeModelAssignments is not nil, and then if it is non-nil, check
the length: if len is greater than 0, write the value via
modelAssignmentsToState; if len is 0, explicitly set
current.VSCodeModelAssignments to nil to clear stale assignments. This ensures
that an explicit empty map clears persisted state instead of leaving stale
assignments behind.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/assets/vscode/agents/sdd-orchestrator.agent.md`:
- Around line 177-182: Add a language identifier to the fenced code block
containing the dependency graph diagram. Change the opening fence from triple
backticks to specify the language as "mermaid" (i.e., ```mermaid) since this
diagram illustrates a Mermaid-style flowchart showing the proposal -> specs ->
tasks -> apply -> verify -> archive dependency relationship with the design
feedback loop.

In `@internal/cli/sync_test.go`:
- Around line 2092-2119: The test function
TestRunSyncWithSelectionPropagatesVSCodeModelWarnings is missing command
execution stubs that all other RunSyncWithSelection tests include. Add
runCommand and cmdLookPath stubs to this test (similar to those used in other
tests like the ones at lines 1475-1476, 1528-1529, and 1600-1601) to ensure
consistency and prevent test failures if the sync operation attempts to execute
or look up external commands during SDD injection or verification.

In `@internal/components/filemerge/replace_windows.go`:
- Line 5: The import of golang.org/x/sys/windows references a vulnerable version
(v0.38.0 affected by GO-2026-5024). Update the golang.org/x/sys dependency to
v0.45.0 or later in your module dependencies and run the dependency update
command to sync the go.mod and go.sum files with the new version.

In `@internal/components/sdd/vscode_agent_visibility.go`:
- Around line 15-52: The function hideManagedClaudeInternalAgentsForVSCode
modifies files in ~/.claude/agents/ without creating backups, violating the
coding guideline that user config writes must have a backup/restore path. Before
calling filemerge.WriteFileAtomic for each file in the loop (where the updated
content differs from the original text), create a backup of the original file
with a .backup suffix. This ensures users have a recovery mechanism if the write
operation fails or corrupts content.

In `@internal/components/sdd/vscode_models.go`:
- Line 211: The line building the YAML model field uses strconv.Quote to quote
the modelLabel, which produces Go-style quoted strings that are not idiomatic
YAML. Replace the strconv.Quote call on line 211 with a plain YAML-aware
approach: for model labels without special characters, use the modelLabel
directly without quotes, or use a YAML-appropriate quoting mechanism if special
characters are present. This will generate cleaner, more idiomatic YAML output.
- Around line 97-138: The function resolveVSCodeModelAssignment is loading the
model cache from disk via loadVSCodeModelCatalog on every invocation, which
causes redundant disk reads and parsing when the function is called once per
agent in the injection loop. Refactor resolveVSCodeModelAssignment to accept the
pre-loaded cache as a parameter instead of calling loadVSCodeModelCatalog
internally. Then in the Inject function in inject.go, load the cache once before
entering the loop that calls resolveVSCodeModelAssignment, and pass the
pre-loaded cache to each call rather than reloading it each time.

In `@internal/tui/model_test.go`:
- Around line 2470-2474: The TestModelConfig_VSCodeOptionOpensCopilotPicker test
hard-codes m.Cursor = 4 to reference the VS Code Copilot option, which breaks if
the menu order changes. Instead of using the hard-coded index value 4,
dynamically compute the VS Code Copilot option's index by calling
screens.ModelConfigOptions() and finding the position of the VS Code Copilot
entry, following the same resilient pattern already established in the codebase
(as referenced at Line 1889). This ensures the test remains valid regardless of
future changes to the menu order.
- Around line 2553-2555: The current assertion in the ScreenModelPicker block
only checks what the screen should NOT be, which is too weak and doesn't verify
the correct route is taken. Replace this negative assertion with a positive one
that explicitly checks state.Screen equals the exact expected screen for the
Codex + SDD preset non-VSCode flow, or alternatively add a check that if
state.Screen does equal ScreenModelPicker, then verify that
state.ModelPicker.ForVSCode is false to ensure the correct flag is set for this
code path.

---

Outside diff comments:
In `@docs/agents.md`:
- Line 16: Change the delegation model descriptor for the VS Code Copilot agent
in the agent matrix table from "Full (runSubagent)" to "Full (native subagents)"
to match the user-facing terminology used consistently across the documentation.
The internal API term runSubagent should be replaced with the clearer native
subagents description.

In `@internal/app/app.go`:
- Around line 682-751: The VSCodeModelAssignments handling currently only checks
if the length is greater than 0 and writes the value, but it does not follow the
nil-vs-empty semantics used for other assignment fields like ModelAssignments,
ClaudeModelAssignments, etc. Refactor the VSCodeModelAssignments block to first
check if selection.VSCodeModelAssignments is not nil, and then if it is non-nil,
check the length: if len is greater than 0, write the value via
modelAssignmentsToState; if len is 0, explicitly set
current.VSCodeModelAssignments to nil to clear stale assignments. This ensures
that an explicit empty map clears persisted state instead of leaving stale
assignments behind.

In `@internal/cli/run.go`:
- Around line 199-234: The mergeExplicitAgentInstallState function is missing a
merge branch for VS Code model assignments. After the existing assignment merge
conditionals (ModelAssignments, ClaudeModelAssignments, KiroModelAssignments,
CodexModelAssignments, etc.) and before the Persona assignment check, add a new
conditional block that checks if newState.VSCodeModelAssignments is not nil and
copies it to merged.VSCodeModelAssignments, following the same pattern as the
other assignment merges in the function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9f4633a5-2db1-468b-a79c-bf4703b68673

📥 Commits

Reviewing files that changed from the base of the PR and between bcd231f and 13739b4.

⛔ Files ignored due to path filters (9)
  • testdata/golden/sdd-claude-agent-sdd-apply.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-archive.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-design.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-explore.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-propose.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-spec.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-tasks.golden is excluded by !testdata/**
  • testdata/golden/sdd-claude-agent-sdd-verify.golden is excluded by !testdata/**
  • testdata/golden/sdd-vscode-instructions.golden is excluded by !testdata/**
📒 Files selected for processing (59)
  • README.md
  • docs/agents.md
  • docs/intended-usage.md
  • docs/platforms.md
  • go.mod
  • internal/agents/vscode/adapter.go
  • internal/agents/vscode/adapter_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/assets/assets.go
  • internal/assets/assets_test.go
  • internal/assets/claude/agents/jd-fix-agent.md
  • internal/assets/claude/agents/jd-judge-a.md
  • internal/assets/claude/agents/jd-judge-b.md
  • internal/assets/claude/agents/sdd-apply.md
  • internal/assets/claude/agents/sdd-archive.md
  • internal/assets/claude/agents/sdd-design.md
  • internal/assets/claude/agents/sdd-explore.md
  • internal/assets/claude/agents/sdd-init.md
  • internal/assets/claude/agents/sdd-onboard.md
  • internal/assets/claude/agents/sdd-propose.md
  • internal/assets/claude/agents/sdd-spec.md
  • internal/assets/claude/agents/sdd-tasks.md
  • internal/assets/claude/agents/sdd-verify.md
  • internal/assets/vscode/agents/sdd-apply.agent.md
  • internal/assets/vscode/agents/sdd-archive.agent.md
  • internal/assets/vscode/agents/sdd-design.agent.md
  • internal/assets/vscode/agents/sdd-explore.agent.md
  • internal/assets/vscode/agents/sdd-init.agent.md
  • internal/assets/vscode/agents/sdd-onboard.agent.md
  • internal/assets/vscode/agents/sdd-orchestrator.agent.md
  • internal/assets/vscode/agents/sdd-propose.agent.md
  • internal/assets/vscode/agents/sdd-spec.agent.md
  • internal/assets/vscode/agents/sdd-tasks.agent.md
  • internal/assets/vscode/agents/sdd-verify.agent.md
  • internal/cli/install_test.go
  • internal/cli/run.go
  • internal/cli/sync.go
  • internal/cli/sync_test.go
  • internal/components/filemerge/replace_default.go
  • internal/components/filemerge/replace_windows.go
  • internal/components/filemerge/writer.go
  • internal/components/filemerge/writer_test.go
  • internal/components/sdd/inject.go
  • internal/components/sdd/inject_test.go
  • internal/components/sdd/vscode_agent_visibility.go
  • internal/components/sdd/vscode_models.go
  • internal/components/sdd/vscode_models_test.go
  • internal/components/uninstall/service_test.go
  • internal/model/selection.go
  • internal/model/selection_test.go
  • internal/state/state.go
  • internal/state/state_test.go
  • internal/tui/model.go
  • internal/tui/model_test.go
  • internal/tui/screens/model_config.go
  • internal/tui/screens/model_config_test.go
  • internal/tui/screens/model_picker.go
  • internal/tui/screens/vscode_model_picker_test.go

Comment on lines +177 to +182
```
proposal -> specs --> tasks -> apply -> verify -> archive
^
|
design
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add language specifier to fenced code block (line 177).

The dependency graph diagram should specify a language identifier for the fenced code block to comply with Markdown lint standards.

♻️ Proposed fix
-```
+```mermaid
 proposal -> specs --> tasks -> apply -> verify -> archive
              ^
              |
            design
-```
+```

Alternatively, use ```text if mermaid diagram syntax is not intended.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 177-177: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/assets/vscode/agents/sdd-orchestrator.agent.md` around lines 177 -
182, Add a language identifier to the fenced code block containing the
dependency graph diagram. Change the opening fence from triple backticks to
specify the language as "mermaid" (i.e., ```mermaid) since this diagram
illustrates a Mermaid-style flowchart showing the proposal -> specs -> tasks ->
apply -> verify -> archive dependency relationship with the design feedback
loop.

Comment thread internal/cli/sync_test.go
Comment on lines +2092 to 2119
func TestRunSyncWithSelectionPropagatesVSCodeModelWarnings(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
selection := model.Selection{
Agents: []model.AgentID{model.AgentVSCodeCopilot},
Components: []model.ComponentID{model.ComponentSDD},
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "gpt-4.1"},
},
}

result, err := RunSyncWithSelection(home, selection)
if err != nil {
t.Fatalf("RunSyncWithSelection() error = %v", err)
}
if !containsSubstring(result.Warnings, "models cache") {
t.Fatalf("Warnings = %v, want missing cache warning", result.Warnings)
}

content, err := os.ReadFile(filepath.Join(home, ".copilot", "agents", "sdd-apply.agent.md"))
if err != nil {
t.Fatalf("ReadFile(sdd-apply.agent.md): %v", err)
}
if strings.Contains(string(content), "model:") {
t.Fatalf("missing cache should omit model line; got:\n%s", content)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing test environment stubs.

All other RunSyncWithSelection tests in this file set runCommand and cmdLookPath stubs (e.g., lines 1475-1476, 1528-1529, 1600-1601), even when the sync operation may not invoke external commands. This test uses t.Setenv for home directory discovery but omits the command stubs, creating an inconsistency that could cause test failures if the sync runtime attempts to execute or look up external commands during SDD injection or verification.

🔧 Add missing test stubs
 func TestRunSyncWithSelectionPropagatesVSCodeModelWarnings(t *testing.T) {
 	home := t.TempDir()
+	restoreCommand := runCommand
+	restoreLookPath := cmdLookPath
+	t.Cleanup(func() {
+		runCommand = restoreCommand
+		cmdLookPath = restoreLookPath
+	})
+	runCommand = func(string, ...string) error { return nil }
+	cmdLookPath = func(name string) (string, error) { return "/usr/local/bin/" + name, nil }
+
 	t.Setenv("HOME", home)
 	t.Setenv("USERPROFILE", home)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestRunSyncWithSelectionPropagatesVSCodeModelWarnings(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
selection := model.Selection{
Agents: []model.AgentID{model.AgentVSCodeCopilot},
Components: []model.ComponentID{model.ComponentSDD},
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "gpt-4.1"},
},
}
result, err := RunSyncWithSelection(home, selection)
if err != nil {
t.Fatalf("RunSyncWithSelection() error = %v", err)
}
if !containsSubstring(result.Warnings, "models cache") {
t.Fatalf("Warnings = %v, want missing cache warning", result.Warnings)
}
content, err := os.ReadFile(filepath.Join(home, ".copilot", "agents", "sdd-apply.agent.md"))
if err != nil {
t.Fatalf("ReadFile(sdd-apply.agent.md): %v", err)
}
if strings.Contains(string(content), "model:") {
t.Fatalf("missing cache should omit model line; got:\n%s", content)
}
}
func TestRunSyncWithSelectionPropagatesVSCodeModelWarnings(t *testing.T) {
home := t.TempDir()
restoreCommand := runCommand
restoreLookPath := cmdLookPath
t.Cleanup(func() {
runCommand = restoreCommand
cmdLookPath = restoreLookPath
})
runCommand = func(string, ...string) error { return nil }
cmdLookPath = func(name string) (string, error) { return "/usr/local/bin/" + name, nil }
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
selection := model.Selection{
Agents: []model.AgentID{model.AgentVSCodeCopilot},
Components: []model.ComponentID{model.ComponentSDD},
VSCodeModelAssignments: map[string]model.ModelAssignment{
"sdd-apply": {ProviderID: "github-copilot", ModelID: "gpt-4.1"},
},
}
result, err := RunSyncWithSelection(home, selection)
if err != nil {
t.Fatalf("RunSyncWithSelection() error = %v", err)
}
if !containsSubstring(result.Warnings, "models cache") {
t.Fatalf("Warnings = %v, want missing cache warning", result.Warnings)
}
content, err := os.ReadFile(filepath.Join(home, ".copilot", "agents", "sdd-apply.agent.md"))
if err != nil {
t.Fatalf("ReadFile(sdd-apply.agent.md): %v", err)
}
if strings.Contains(string(content), "model:") {
t.Fatalf("missing cache should omit model line; got:\n%s", content)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/sync_test.go` around lines 2092 - 2119, The test function
TestRunSyncWithSelectionPropagatesVSCodeModelWarnings is missing command
execution stubs that all other RunSyncWithSelection tests include. Add
runCommand and cmdLookPath stubs to this test (similar to those used in other
tests like the ones at lines 1475-1476, 1528-1529, and 1600-1601) to ensure
consistency and prevent test failures if the sync operation attempts to execute
or look up external commands during SDD injection or verification.


package filemerge

import "golang.org/x/sys/windows"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What is the latest patched version of golang.org/x/sys that fixes GO-2026-5024 (NewNTUnicodeString integer overflow)?

💡 Result:

The latest patched version of golang.org/x/sys that fixes the GO-2026-5024 (also tracked as CVE-2026-39824) integer overflow vulnerability is v0.45.0 [1][2][3]. The Go Security Team officially announced this fix on May 21, 2026, noting that version v0.45.0 resolves the issue where the NewNTUnicodeString function failed to correctly check for string length overflows [3]. As of June 14, 2026, later versions of golang.org/x/sys, such as v0.46.0, also contain this fix [4]. Users should update to v0.45.0 or any subsequent release to ensure protection against this vulnerability [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
# Check if UTF16PtrFromString or MoveFileEx code paths invoke NewNTUnicodeString
rg -n "NewNTUnicodeString" --type go

Repository: Gentleman-Programming/gentle-ai

Length of output: 57


🏁 Script executed:

# Check the golang.org/x/sys version in go.mod
cat go.mod | grep -A 2 "golang.org/x/sys"

Repository: Gentleman-Programming/gentle-ai

Length of output: 104


🏁 Script executed:

# Read the replace_windows.go file to see actual usage
cat -n internal/components/filemerge/replace_windows.go

Repository: Gentleman-Programming/gentle-ai

Length of output: 714


🏁 Script executed:

# Search for what golang.org/x/sys package documentation says about UTF16PtrFromString
rg -A 10 "UTF16PtrFromString|MoveFileEx" --type go

Repository: Gentleman-Programming/gentle-ai

Length of output: 1545


🌐 Web query:

golang.org/x/sys windows.UTF16PtrFromString NewNTUnicodeString GO-2026-5024 vulnerability affected

💡 Result:

The vulnerability GO-2026-5024 (also tracked as CVE-2026-39824) affects the NewNTUnicodeString function within the golang.org/x/sys/windows package [1][2][3]. The issue is an integer overflow caused by the function failing to validate the input string length against the maximum capacity of a NTUnicodeString, which is limited by a 16-bit number of bytes [1][4]. Previously, if provided with a string exceeding this length, the function would return a silently truncated string instead of an error, potentially leading to data integrity issues or unexpected program logic [1][4]. The vulnerability was addressed in version v0.45.0 of golang.org/x/sys [3]. Although some security advisories initially mentioned v0.44.0 as the fix [2][4], the official Go Security Team announcement confirmed that v0.45.0 was tagged specifically to resolve this issue [3]. To remediate this vulnerability, users should update their golang.org/x/sys dependency to v0.45.0 or later [3]. It is important to note that NewNTUnicodeString is not widely used in typical Go Windows applications; the standard library's primary use of similar functionality is in specific symlink handling, which developers have noted is generally not exploitable due to Windows path length limitations [5]. Despite this, users are advised to update to ensure safe handling of system strings [3].

Citations:


🌐 Web query:

golang.org/x/sys v0.38.0 UTF16PtrFromString implementation details

💡 Result:

In the golang.org/x/sys/windows package (including version v0.38.0), the UTF16PtrFromString function is implemented by delegating the conversion logic to UTF16FromString and returning a pointer to the first element of the resulting UTF-16 slice [1]. The implementation follows these steps: 1. It calls UTF16FromString(s) to convert the input UTF-8 string into a null-terminated []uint16 slice [1][2]. 2. If UTF16FromString returns an error—specifically if the input string contains a NUL byte—UTF16PtrFromString propagates that error [1][2]. 3. If successful, it returns the address of the first element of the slice (&a), providing a pointer compatible with Windows APIs that expect a *uint16 (or LPCWSTR) [1][2]. UTF16FromString itself handles the conversion by checking for NUL bytes, pre-allocating a buffer, and using an internal encodeWTF16 function to process the UTF-8 string, ensuring that unpaired surrogates are encoded using WTF-8 [2][3]. It concludes by appending a terminating zero (NUL) to the slice [2]. Note that this function is the modern replacement for the deprecated StringToUTF16Ptr, which would panic rather than return an error if a NUL byte was encountered [1][2][3]. Modern implementations of windows.UTF16PtrFromString in the x/sys package are designed to forward to the standard library's syscall package implementation, ensuring consistency and benefiting from shared improvements and bug fixes [4].

Citations:


Update golang.org/x/sys to v0.45.0 or later for security patching.

The codebase uses golang.org/x/sys v0.38.0, which is affected by GO-2026-5024. While the vulnerability is specific to the NewNTUnicodeString function (not the UTF16PtrFromString and MoveFileEx functions used here), updating to v0.45.0 or later is recommended as a general security best practice to ensure all dependencies are current with patches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/components/filemerge/replace_windows.go` at line 5, The import of
golang.org/x/sys/windows references a vulnerable version (v0.38.0 affected by
GO-2026-5024). Update the golang.org/x/sys dependency to v0.45.0 or later in
your module dependencies and run the dependency update command to sync the
go.mod and go.sum files with the new version.

Comment on lines +15 to +52
func hideManagedClaudeInternalAgentsForVSCode(homeDir string) (InjectionResult, error) {
agentsDir := filepath.Join(homeDir, ".claude", "agents")
if info, err := os.Stat(agentsDir); err != nil {
if errors.Is(err, os.ErrNotExist) {
return InjectionResult{}, nil
}
return InjectionResult{}, fmt.Errorf("inspect Claude agents dir: %w", err)
} else if !info.IsDir() {
return InjectionResult{}, nil
}

result := InjectionResult{}
for _, fileName := range managedClaudeInternalAgentFiles() {
path := filepath.Join(agentsDir, fileName)
content, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return InjectionResult{}, fmt.Errorf("read managed Claude agent %s: %w", fileName, err)
}

text := string(content)
updated := hideClaudeAgentFromVSCodePicker(text)
if updated == text {
continue
}
writeResult, err := filemerge.WriteFileAtomic(path, []byte(updated), 0o644)
if err != nil {
return InjectionResult{}, fmt.Errorf("write managed Claude agent %s: %w", fileName, err)
}
if writeResult.Changed {
result.Changed = true
result.Files = append(result.Files, path)
}
}
return result, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Missing backup/restore path for user config writes.

This function modifies files in ~/.claude/agents/ without creating backups. Per coding guidelines for internal/components/**/*.go: "Any write to user config must have a backup/restore path." Even though these are managed files, users could have modified them, and there's no recovery mechanism if the write fails or corrupts content.

🛡️ Suggested approach

Before modifying each file, create a backup with a .backup suffix:

 		text := string(content)
 		updated := hideClaudeAgentFromVSCodePicker(text)
 		if updated == text {
 			continue
 		}
+		backupPath := path + ".backup"
+		if err := os.WriteFile(backupPath, content, 0o644); err != nil {
+			return InjectionResult{}, fmt.Errorf("backup managed Claude agent %s: %w", fileName, err)
+		}
 		writeResult, err := filemerge.WriteFileAtomic(path, []byte(updated), 0o644)
 		if err != nil {
+			// Restore from backup on write failure
+			_ = os.Rename(backupPath, path)
 			return InjectionResult{}, fmt.Errorf("write managed Claude agent %s: %w", fileName, err)
 		}

Alternatively, document that these files are fully managed and users should not edit them, then implement a restore-from-assets path.

As per coding guidelines, internal/components/ files that write to user config must have a backup/restore path.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func hideManagedClaudeInternalAgentsForVSCode(homeDir string) (InjectionResult, error) {
agentsDir := filepath.Join(homeDir, ".claude", "agents")
if info, err := os.Stat(agentsDir); err != nil {
if errors.Is(err, os.ErrNotExist) {
return InjectionResult{}, nil
}
return InjectionResult{}, fmt.Errorf("inspect Claude agents dir: %w", err)
} else if !info.IsDir() {
return InjectionResult{}, nil
}
result := InjectionResult{}
for _, fileName := range managedClaudeInternalAgentFiles() {
path := filepath.Join(agentsDir, fileName)
content, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return InjectionResult{}, fmt.Errorf("read managed Claude agent %s: %w", fileName, err)
}
text := string(content)
updated := hideClaudeAgentFromVSCodePicker(text)
if updated == text {
continue
}
writeResult, err := filemerge.WriteFileAtomic(path, []byte(updated), 0o644)
if err != nil {
return InjectionResult{}, fmt.Errorf("write managed Claude agent %s: %w", fileName, err)
}
if writeResult.Changed {
result.Changed = true
result.Files = append(result.Files, path)
}
}
return result, nil
}
func hideManagedClaudeInternalAgentsForVSCode(homeDir string) (InjectionResult, error) {
agentsDir := filepath.Join(homeDir, ".claude", "agents")
if info, err := os.Stat(agentsDir); err != nil {
if errors.Is(err, os.ErrNotExist) {
return InjectionResult{}, nil
}
return InjectionResult{}, fmt.Errorf("inspect Claude agents dir: %w", err)
} else if !info.IsDir() {
return InjectionResult{}, nil
}
result := InjectionResult{}
for _, fileName := range managedClaudeInternalAgentFiles() {
path := filepath.Join(agentsDir, fileName)
content, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return InjectionResult{}, fmt.Errorf("read managed Claude agent %s: %w", fileName, err)
}
text := string(content)
updated := hideClaudeAgentFromVSCodePicker(text)
if updated == text {
continue
}
backupPath := path + ".backup"
if err := os.WriteFile(backupPath, content, 0o644); err != nil {
return InjectionResult{}, fmt.Errorf("backup managed Claude agent %s: %w", fileName, err)
}
writeResult, err := filemerge.WriteFileAtomic(path, []byte(updated), 0o644)
if err != nil {
// Restore from backup on write failure
_ = os.Rename(backupPath, path)
return InjectionResult{}, fmt.Errorf("write managed Claude agent %s: %w", fileName, err)
}
if writeResult.Changed {
result.Changed = true
result.Files = append(result.Files, path)
}
}
return result, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/components/sdd/vscode_agent_visibility.go` around lines 15 - 52, The
function hideManagedClaudeInternalAgentsForVSCode modifies files in
~/.claude/agents/ without creating backups, violating the coding guideline that
user config writes must have a backup/restore path. Before calling
filemerge.WriteFileAtomic for each file in the loop (where the updated content
differs from the original text), create a backup of the original file with a
.backup suffix. This ensures users have a recovery mechanism if the write
operation fails or corrupts content.

Source: Coding guidelines

Comment on lines +97 to +138
// resolveVSCodeModelAssignment validates an assignment against the dynamic
// OpenCode model cache and returns the VS Code display label to write. Any
// invalid or stale assignment returns a warning plus an empty label; the caller
// must then omit `model:` instead of failing sync.
func resolveVSCodeModelAssignment(agentKey string, assignments map[string]model.ModelAssignment, cachePath, variantsPath string) (string, []string) {
assignment, ok := assignments[agentKey]
if !ok || assignment.ProviderID == "" || assignment.ModelID == "" {
return "", nil
}
if assignment.ProviderID != githubCopilotProviderID {
return "", []string{fmt.Sprintf("VS Code model assignment for %s skipped: provider %q is not %q", agentKey, assignment.ProviderID, githubCopilotProviderID)}
}

providers, warnings := loadVSCodeModelCatalog(agentKey, cachePath, variantsPath)
if len(warnings) > 0 {
return "", warnings
}

provider, ok := providers[githubCopilotProviderID]
if !ok {
return "", []string{fmt.Sprintf("VS Code model assignment for %s skipped: provider %q missing from models cache", agentKey, githubCopilotProviderID)}
}
cachedModel, ok := provider.Models[assignment.ModelID]
if !ok {
return "", []string{fmt.Sprintf("VS Code model assignment for %s skipped: model %q missing from %q models cache", agentKey, assignment.ModelID, githubCopilotProviderID)}
}
if !cachedModel.ToolCall {
return "", []string{fmt.Sprintf("VS Code model assignment for %s skipped: model %q does not support tool calls", agentKey, assignment.ModelID)}
}
if warning := validateVSCodeEffort(agentKey, assignment, cachedModel); warning != "" {
return "", []string{warning}
}

label := strings.TrimSpace(cachedModel.Name)
if label == "" {
label = strings.TrimSpace(cachedModel.ID)
}
if label == "" {
return "", []string{fmt.Sprintf("VS Code model assignment for %s skipped: model %q has insufficient metadata", agentKey, assignment.ModelID)}
}
return label, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider loading the model cache once instead of per-agent.

resolveVSCodeModelAssignment calls loadVSCodeModelCatalog (line 110) which reads and parses the models cache from disk. Since this function is called once per agent file in the injection loop (inject.go line 690), the cache is loaded 11 times during a sync. For efficiency, consider loading the cache once in Inject() and passing it as a parameter.

♻️ Suggested optimization

Refactor renderVSCodeAgentModelAssignment to accept pre-loaded cache:

-func renderVSCodeAgentModelAssignment(content, fileName string, opts InjectOptions) (string, []string) {
+func renderVSCodeAgentModelAssignment(content, fileName string, opts InjectOptions, providers map[string]opencode.Provider) (string, []string) {
 	agentKey, ok := vscodeAgentKey(fileName)
 	if !ok {
 		return content, nil
 	}
 	if opts.VSCodeModelAssignments == nil {
 		return injectVSCodeModelLine(content, ""), nil
 	}
 
-	modelLabel, warnings := resolveVSCodeModelAssignment(
+	modelLabel, warnings := resolveVSCodeModelAssignmentWithCache(
 		agentKey,
 		opts.VSCodeModelAssignments,
-		opts.VSCodeModelCachePath,
-		opts.VSCodeModelVariantsPath,
+		providers,
 	)
 	return injectVSCodeModelLine(content, modelLabel), warnings
 }

Then in inject.go, load once before the loop and pass to each call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/components/sdd/vscode_models.go` around lines 97 - 138, The function
resolveVSCodeModelAssignment is loading the model cache from disk via
loadVSCodeModelCatalog on every invocation, which causes redundant disk reads
and parsing when the function is called once per agent in the injection loop.
Refactor resolveVSCodeModelAssignment to accept the pre-loaded cache as a
parameter instead of calling loadVSCodeModelCatalog internally. Then in the
Inject function in inject.go, load the cache once before entering the loop that
calls resolveVSCodeModelAssignment, and pass the pre-loaded cache to each call
rather than reloading it each time.

updated = append(updated, lines[i])
}
if modelLabel != "" {
updated = append(updated, "model: "+strconv.Quote(modelLabel))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

YAML quoting uses Go-style quotes.

Line 211 uses strconv.Quote which produces Go-style quoted strings. While valid YAML, consider using plain YAML strings for labels without special characters, or a YAML-aware quoting function for better idiomatic output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/components/sdd/vscode_models.go` at line 211, The line building the
YAML model field uses strconv.Quote to quote the modelLabel, which produces
Go-style quoted strings that are not idiomatic YAML. Replace the strconv.Quote
call on line 211 with a plain YAML-aware approach: for model labels without
special characters, use the modelLabel directly without quotes, or use a
YAML-appropriate quoting mechanism if special characters are present. This will
generate cleaner, more idiomatic YAML output.

Comment on lines +2470 to +2474
func TestModelConfig_VSCodeOptionOpensCopilotPicker(t *testing.T) {
m := NewModel(system.DetectionResult{}, "dev")
m.Screen = ScreenModelConfig
m.Cursor = 4 // Claude(0) OpenCode(1) Kiro(2) Codex(3) VS Code Copilot(4)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Avoid hard-coded Model Config index for VS Code option.

Line 2473 hard-codes m.Cursor = 4, which makes this test brittle when menu order changes. Compute the VS Code option index from screens.ModelConfigOptions() (same resiliency pattern already used in Line 1889).

Suggested change
 func TestModelConfig_VSCodeOptionOpensCopilotPicker(t *testing.T) {
 	m := NewModel(system.DetectionResult{}, "dev")
 	m.Screen = ScreenModelConfig
-	m.Cursor = 4 // Claude(0) OpenCode(1) Kiro(2) Codex(3) VS Code Copilot(4)
+	options := screens.ModelConfigOptions()
+	vscodeIdx := -1
+	for i, opt := range options {
+		if strings.Contains(strings.ToLower(opt), "vs code") {
+			vscodeIdx = i
+			break
+		}
+	}
+	if vscodeIdx < 0 {
+		t.Fatal("VS Code option not found in ModelConfigOptions")
+	}
+	m.Cursor = vscodeIdx
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestModelConfig_VSCodeOptionOpensCopilotPicker(t *testing.T) {
m := NewModel(system.DetectionResult{}, "dev")
m.Screen = ScreenModelConfig
m.Cursor = 4 // Claude(0) OpenCode(1) Kiro(2) Codex(3) VS Code Copilot(4)
func TestModelConfig_VSCodeOptionOpensCopilotPicker(t *testing.T) {
m := NewModel(system.DetectionResult{}, "dev")
m.Screen = ScreenModelConfig
options := screens.ModelConfigOptions()
vscodeIdx := -1
for i, opt := range options {
if strings.Contains(strings.ToLower(opt), "vs code") {
vscodeIdx = i
break
}
}
if vscodeIdx < 0 {
t.Fatal("VS Code option not found in ModelConfigOptions")
}
m.Cursor = vscodeIdx
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/model_test.go` around lines 2470 - 2474, The
TestModelConfig_VSCodeOptionOpensCopilotPicker test hard-codes m.Cursor = 4 to
reference the VS Code Copilot option, which breaks if the menu order changes.
Instead of using the hard-coded index value 4, dynamically compute the VS Code
Copilot option's index by calling screens.ModelConfigOptions() and finding the
position of the VS Code Copilot entry, following the same resilient pattern
already established in the codebase (as referenced at Line 1889). This ensures
the test remains valid regardless of future changes to the menu order.

Comment on lines +2553 to +2555
if state.Screen == ScreenModelPicker {
t.Fatalf("screen = ScreenModelPicker without VS Code agent; want a later non-VS Code flow screen")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen this negative assertion to lock the expected route.

Lines 2553-2555 only assert “not ScreenModelPicker,” which can still pass if navigation regresses to the wrong non-VSCode screen. Assert the exact expected next screen for this setup (Codex + SDD preset path), or at minimum assert state.ModelPicker.ForVSCode == false when ScreenModelPicker is reached.

Suggested change
-	if state.Screen == ScreenModelPicker {
-		t.Fatalf("screen = ScreenModelPicker without VS Code agent; want a later non-VS Code flow screen")
-	}
+	if state.Screen != ScreenCodexModelPicker {
+		t.Fatalf("screen = %v, want %v (Codex + SDD should follow Codex picker path, not VS Code picker path)",
+			state.Screen, ScreenCodexModelPicker)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if state.Screen == ScreenModelPicker {
t.Fatalf("screen = ScreenModelPicker without VS Code agent; want a later non-VS Code flow screen")
}
if state.Screen != ScreenCodexModelPicker {
t.Fatalf("screen = %v, want %v (Codex + SDD should follow Codex picker path, not VS Code picker path)",
state.Screen, ScreenCodexModelPicker)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/model_test.go` around lines 2553 - 2555, The current assertion
in the ScreenModelPicker block only checks what the screen should NOT be, which
is too weak and doesn't verify the correct route is taken. Replace this negative
assertion with a positive one that explicitly checks state.Screen equals the
exact expected screen for the Codex + SDD preset non-VSCode flow, or
alternatively add a check that if state.Screen does equal ScreenModelPicker,
then verify that state.ModelPicker.ForVSCode is false to ensure the correct flag
is set for this code path.

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.

feat(agents): add VS Code Copilot SDD multi-mode support (backend)

1 participant