feat(vscode): add TUI model flow and docs#740
Conversation
9aa7eee to
13739b4
Compare
📝 WalkthroughWalkthroughActivates VS Code Copilot native sub-agent SDD support: enables the adapter, embeds 11 ChangesVS Code Copilot SDD Native Sub-Agent Feature
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winInconsistency 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
mergeExplicitAgentInstallStateomits VS Code assignment merge branch.The explicit
install --agent ...merge path does not copynewState.VSCodeModelAssignmentsinto merged state. VS Code model assignments built innewStateare 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 winPreserve nil-vs-empty clear semantics for
VSCodeModelAssignmentsin persistence.
persistAssignmentscurrently writes VS Code assignments only for non-empty maps, so an explicit clear (non-nil, empty map) never clearsstate.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
⛔ Files ignored due to path filters (9)
testdata/golden/sdd-claude-agent-sdd-apply.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-archive.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-design.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-explore.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-propose.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-spec.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-tasks.goldenis excluded by!testdata/**testdata/golden/sdd-claude-agent-sdd-verify.goldenis excluded by!testdata/**testdata/golden/sdd-vscode-instructions.goldenis excluded by!testdata/**
📒 Files selected for processing (59)
README.mddocs/agents.mddocs/intended-usage.mddocs/platforms.mdgo.modinternal/agents/vscode/adapter.gointernal/agents/vscode/adapter_test.gointernal/app/app.gointernal/app/app_test.gointernal/assets/assets.gointernal/assets/assets_test.gointernal/assets/claude/agents/jd-fix-agent.mdinternal/assets/claude/agents/jd-judge-a.mdinternal/assets/claude/agents/jd-judge-b.mdinternal/assets/claude/agents/sdd-apply.mdinternal/assets/claude/agents/sdd-archive.mdinternal/assets/claude/agents/sdd-design.mdinternal/assets/claude/agents/sdd-explore.mdinternal/assets/claude/agents/sdd-init.mdinternal/assets/claude/agents/sdd-onboard.mdinternal/assets/claude/agents/sdd-propose.mdinternal/assets/claude/agents/sdd-spec.mdinternal/assets/claude/agents/sdd-tasks.mdinternal/assets/claude/agents/sdd-verify.mdinternal/assets/vscode/agents/sdd-apply.agent.mdinternal/assets/vscode/agents/sdd-archive.agent.mdinternal/assets/vscode/agents/sdd-design.agent.mdinternal/assets/vscode/agents/sdd-explore.agent.mdinternal/assets/vscode/agents/sdd-init.agent.mdinternal/assets/vscode/agents/sdd-onboard.agent.mdinternal/assets/vscode/agents/sdd-orchestrator.agent.mdinternal/assets/vscode/agents/sdd-propose.agent.mdinternal/assets/vscode/agents/sdd-spec.agent.mdinternal/assets/vscode/agents/sdd-tasks.agent.mdinternal/assets/vscode/agents/sdd-verify.agent.mdinternal/cli/install_test.gointernal/cli/run.gointernal/cli/sync.gointernal/cli/sync_test.gointernal/components/filemerge/replace_default.gointernal/components/filemerge/replace_windows.gointernal/components/filemerge/writer.gointernal/components/filemerge/writer_test.gointernal/components/sdd/inject.gointernal/components/sdd/inject_test.gointernal/components/sdd/vscode_agent_visibility.gointernal/components/sdd/vscode_models.gointernal/components/sdd/vscode_models_test.gointernal/components/uninstall/service_test.gointernal/model/selection.gointernal/model/selection_test.gointernal/state/state.gointernal/state/state_test.gointernal/tui/model.gointernal/tui/model_test.gointernal/tui/screens/model_config.gointernal/tui/screens/model_config_test.gointernal/tui/screens/model_picker.gointernal/tui/screens/vscode_model_picker_test.go
| ``` | ||
| proposal -> specs --> tasks -> apply -> verify -> archive | ||
| ^ | ||
| | | ||
| design | ||
| ``` |
There was a problem hiding this comment.
🧹 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
🧩 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:
- 1: https://osv.dev/vulnerability/GO-2026-5024
- 2: https://pkg.go.dev/vuln/GO-2026-5024
- 3: https://groups.google.com/g/golang-announce/c/6MMI8Lj-Atg
- 4: https://pkg.go.dev/golang.org/x/sys@v0.32.0
🏁 Script executed:
#!/bin/bash
# Check if UTF16PtrFromString or MoveFileEx code paths invoke NewNTUnicodeString
rg -n "NewNTUnicodeString" --type goRepository: 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.goRepository: 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 goRepository: 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:
- 1: https://pkg.go.dev/vuln/GO-2026-5024
- 2: https://osv.dev/vulnerability/GO-2026-5024
- 3: https://groups.google.com/g/golang-announce/c/6MMI8Lj-Atg
- 4: https://www.resolvedsecurity.com/vulnerability-catalog/CVE-2026-39824
- 5: x/sys/windows, internal/syscall/windows: integer overflow in NewNTUnicodeString golang/go#78916
🌐 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:
- 1: https://github.com/golang/sys/blob/master/windows/syscall_windows.go
- 2: https://go.dev/src/syscall/syscall_windows.go?m=text
- 3: https://github.com/golang/go/blob/go1.21.5/src/syscall/syscall_windows.go
- 4: https://android.googlesource.com/platform/external/golang-x-sys/+/a6f4650873b0647f4a7562d1bc45c27d4da686d7
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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
| // 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 | ||
| } |
There was a problem hiding this comment.
🧹 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)) |
There was a problem hiding this comment.
🧹 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.
| 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) | ||
|
|
There was a problem hiding this comment.
🧹 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.
| 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.
| if state.Screen == ScreenModelPicker { | ||
| t.Fatalf("screen = ScreenModelPicker without VS Code agent; want a later non-VS Code flow screen") | ||
| } |
There was a problem hiding this comment.
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.
| 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.
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 onlytype:refactor- Code refactoring (no functional changes)type:chore- Build, CI, or tooling changestype:breaking-change- Breaking change (fix or feature that changes existing behavior)Chain Context
PR 4 of the VS Code Copilot SDD subagents stack.
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
mainconflict-resolution merge:Snakeblack/gentle-ai@fix/vscode-copilot-agent-hardening...27e6100
Conflict-resolution merge commit:
9aa7eee(origin/mainv1.34.0).Summary
sdd-orchestratorshould be visible in the native VS Code dropdown, while the TUI can configure phase models.Changes
internal/tui/model.gointernal/tui/screens/model_picker.gointernal/tui/**/*_test.goREADME.md,docs/agents.mddocs/intended-usage.md,docs/platforms.mdSize 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
Results:
go test ./...) in Linux/LF verification clone before PR creationgo test ./...) in Linux/LF verification clone after resolving theorigin/mainconflictgo vet ./...) in Linux/LF verification clone before PR creationgo vet ./...) in Linux/LF verification clone after resolving theorigin/mainconflictcd e2e && ./docker-test.sh)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
size:exception; focused work unit is cohesiveCloses #504status:approvedstatus:approvedtype:*Labeltype:featurerequested; maintainer label neededgo test ./...Contributor Checklist
status:approvedsize:exceptionwith rationale documentedtype:*label to this PR - maintainer label needed (type:feature)go test ./...) before PR creation and after conflict resolutioncd e2e && ./docker-test.sh)Co-Authored-BytrailersNotes 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
Documentation
Bug Fixes