fix(settings): always prefix custom agent ids with custom: so the AI Agents page round-trips - #123
Conversation
`SaveCustomAcpAgent` / `SaveCustomDelegateAgent` previously only added the
`custom:` prefix when the derived bare id collided with a built-in agent
name. For everything else (e.g. user types `qwen.cmd --acp`, bare id
`qwen`) the bare id was stored verbatim in `AcpAgent` / `DelegateAgent`.
That bare id breaks every downstream consumer that uses `custom:` as the
discriminator:
* `AIAgentsViewModel::_MaybeAppendCustomEntry` early-returns when the
saved id does not start with `custom:`, so the rebuilt dropdown never
surfaces the entry and `CurrentAcpAgent` falls back to the first
built-in. This is why the AI Agents page reverts to Copilot right
after clicking the page-level Save.
* `GlobalAppSettings::Effective{Acp,Delegate}Agent` treats anything
without the prefix as a built-in id and runs it through the
`AllowedAgents` GPO allowlist, returning empty for unknown ids -
downstream consumers then think no agent is selected.
* `TerminalPage::_ResolveEffectiveAgentCliPath` only honours
`AcpCustomCommand` when the id has the prefix; without it the
launcher falls back to `_BuildAgentCommandLine`, which returns the
bare id verbatim instead of the full custom command line.
* `DeleteCustomAcpAgent` / `EditCustomAcpAgent` /
`IsCustom*AgentSelected` / `CustomAcpCommandPreview` / `ShowAcpModel`
all gate on the prefix - the user can neither edit nor delete the
entry after reload.
* `AllowCustomAgents` GPO is bypassed (policy code only inspects the
prefix), and telemetry serializes the raw user-chosen binary name
instead of the privacy-preserving `custom:` discriminator.
Always prefix the saved id with `custom:` regardless of whether the
bare id matches a built-in. The `displayName` branch is preserved so a
custom override of a built-in still shows `copilot (custom)`. Also
update `_MaybeAppendCustomEntry` to compute the same id so list rebuilds
round-trip correctly.
Note: users who already saved a bare id from a previous build keep the
same broken UX until they re-add the agent (`EffectiveAcpAgent` was
already returning empty for them); no auto-migration is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes custom AI agent persistence by ensuring custom agent IDs are always saved with the custom: discriminator, allowing the AI Agents settings page and downstream agent resolution paths to round-trip custom entries correctly.
Changes:
- Always prefixes derived custom ACP and delegate agent IDs with
custom:. - Updates custom-entry reconstruction to use the same prefixed ID format.
- Preserves the existing display-name behavior for custom agents that share a built-in agent name.
This comment has been minimized.
This comment has been minimized.
When a user types a custom ACP command containing a quoted full path (e.g. "C:\Program Files\qwen\qwen.cmd" --acp), _DeriveId previously split on the first space — which fell inside the quotes — and produced a bogus id like "C:\Program. The derived id is what the settings page uses to build the saved custom:<id> value and the dropdown label, so a broken id silently broke save/round-trip for any path containing a space (very common: Program Files, AppData\Local\..., user profile paths with spaces, etc.). Changes to _DeriveId: - Trim leading whitespace before parsing. - If the command begins with ", take everything up to the next " as the executable token (proper quoted-path handling). - Otherwise split on the first run of whitespace (space OR tab). - Make the trailing .exe/.cmd/.bat strip case-insensitive via _stricmp so paths like qwen.EXE work the same as qwen.exe. Verified by tracing 10 inputs (unquoted, quoted-with-spaces, mixed slashes, tab separator, mixed extension case, leading whitespace, empty, single-quote pathological, very long) — all produce the expected basename or return an empty hstring safely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Extends PR #123 with coverage at two layers that were previously unguarded for the custom-agent save/load and GPO filter paths. Production refactor (no behavior change): - Extract AIAgentsViewModel::_DeriveId into header-only src/cascadia/inc/CustomAgentId.h so tests can call it without pulling in TerminalSettingsEditor.dll. - Add AgentPolicy::SetSnapshotForTest / ResetForTest seam + static GlobalAppSettings::_TestHookSetAgentPolicy forwarders so the injected snapshot lands in SettingsModel.dll (where EffectiveAcpAgent consults it). Tests: - ut_app/CustomAgentIdTests.cpp (22 cases): bare names, .exe/.cmd/.bat case-insensitive strip, quoted paths with spaces+args, forward/mixed slashes, leading whitespace, empty/quoted-empty, extension-only filename, unknown extensions (.ps1/.py/.sh) left intact, built-in collision. - UnitTests_SettingsModel/CustomAgentAndPolicyTests.cpp (19 cases): round-trip of acpAgent / delegateAgent custom: prefix (regression guard for #123), quoted Windows path round-trip, EffectiveAcpAgent and EffectiveDelegateAgent matrices across AllowedAgents (nullopt / allow / block / empty / case-insensitive) and AllowCustomAgents (NotConfigured / Allowed / Blocked), plus IsAgentPolicyLocked / IsCustomAgentPolicyLocked mirroring. Verified locally: 41/41 passing on x64 Debug via te.exe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…umers Address Copilot review feedback on PR #123: - SaveCustomAcpAgent / SaveCustomDelegateAgent now bail out when DeriveCustomAgentId returns an empty id (whitespace-only or quote-only commands), so the UI cannot persist a bare `custom:` entry that would leave a blank, unusable custom agent selected. - Drop the stale `telemetry` mention from the consumer list comment in SaveCustomAcpAgent; telemetry sanitizes any non-built-in id to the literal `custom` and does not key on the prefix. - Rename `qwen` to `mybot` in CustomAgentAndPolicyTests.cpp to avoid check-spelling alerts without adding to the expect list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Copilot please review the latest changes. |
Telemetry's sanitizeProviderId collapses every non-built-in id to literal `custom` without checking the `custom:` prefix. Update CustomAgentId.h to call out that the prefix matters for EffectiveAcpAgent, the resolver, and custom-edit/delete UI gates only — and that telemetry deliberately does not depend on it. Also rename the `qwen` example to `mybot` for consistency with the test fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Copilot please review the latest changes. |
Reviewed the latest changes. The telemetry wording is now aligned with the implementation, and I don’t see any additional issues in this update. |
Match the prior cleanup in CustomAgentId.h (24cb0d8) — the test file headers should not claim telemetry depends on the custom: prefix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reviewed the latest changes. The telemetry wording now matches |
This comment has been minimized.
This comment has been minimized.
The earlier 'mybot' placeholder was still flagged by check-spelling because it is not a real English word. Replace remaining 'qwen' occurrences in CustomAgentIdTests.cpp (which were missed in 24cb0d8) and the 'mybot' occurrences in CustomAgentId.h, CustomAgentIdTests.cpp, and CustomAgentAndPolicyTests.cpp with the real word 'helper'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Copilot please review the latest changes. |
Reviewed the latest changes. The placeholder renames and comment updates look consistent, and I don’t see any additional issues in this update. |
This comment has been minimized.
This comment has been minimized.
683fdd4 to
b851fc3
Compare
🎯 Prompt-robustness improvement (commit b851fc3)Added a Step 0 binary Chat-vs-Not-Chat gate before the existing Mode Decision tree in Root causeThe original Mode Decision tree treated Test methodologyBuilt a direct test harness against the same Azure OpenAI endpoint (
16 scenarios × 2 variants × 5 trials × 2 tracks = 320 total trials. Scenarios cover:
Results
Key observations:
Why a "binary gate before the mode tree" worksModels are much better at executing a mechanical first-pass classification than at correctly walking a 4-branch decision tree with subtle tie-breakers. Step 0 explicitly enumerates the signals that mean "Not-Chat" (failed buffer + bare follow-up, demonstratives, imperatives, environment binding) and requires Files changed in this commitOnly |
This comment has been minimized.
This comment has been minimized.
|
Replaced the prior 35-line Diff: The 3 edits
Empirical validation (3 tracks)5 trials × 16 scenarios per API track; 2 trials × 10 scenarios for the real Copilot CLI track:
All 8 Single residual failure on the Copilot API track: |
|
Gordon-Lam — you were right, the previous Pushed the actual autofix fix as 22a28a1. Two surgical edits to
A/B harness (12 scenarios × 3 trials × 2 model tracks):
|
22a28a1 to
447cfaa
Compare
|
|
||
| 2. **Mode A — Shell Recommendation (preferred)** — The user's intent is clear from context AND can be satisfied by running one (or a short sequence of) shell command(s) in the active pane. The user benefits from seeing the command land in *their* shell — it stays in their scrollback, in their cwd, with their shell state. | ||
| Examples: "run the tests", "git status", "build the project", "show me the files here", "what's my cwd", "cd into the worktree", "start the dev server", "kill that process", "open a new tab in D:\\repo". | ||
| Follow-up questions to a failed command shown in `buffer` ("why?", "explain", "help", "what should I do?", "any suggestion?") always land here — the buffer error makes the intent clear, and the user wants the fix command, not a prose explanation. |
There was a problem hiding this comment.
Why explain an error lands to a command?
Symptom
In the AI Agents settings page, adding a custom ACP agent (e.g.
qwen.cmd --acp) and clicking the inline Save next to the command field looks fine — but as soon as you click the page-level Save, the dropdown snaps back to the default (Copilot). The custom entry vanishes from the list.Root cause
SaveCustomAcpAgent/SaveCustomDelegateAgentinAIAgentsViewModel.cpponly added thecustom:prefix when the derived bare id collided with a built-in agent name. For anything else (the common case — most custom agents aren'tcopilot/gemini/claude/codex) the bare id was stored verbatim:The settings / runtime path uses the
custom:prefix as the discriminator for "this is a custom agent". The non-built-in branch silently drops it.Why the page reverts (immediate symptom)
When the settings page is rebuilt after the outer Save, the constructor calls
_MaybeAppendCustomEntry, which early-returns when!_StartsWithCustom(currentAgentId). So no custom entry is appended,CurrentAcpAgent()can't find"qwen"in the list, and the ComboBox falls back to the first non-AddNew item — Copilot.Downstream breakage (would still bite even if the UI stuck)
Even pretending the entry stayed visible, the saved value
AcpAgent="qwen"is broken end-to-end:GlobalAppSettings::EffectiveAcpAgent()AgentPolicy::IsAgentAllowed("qwen")→ not in allowlist → returns empty string. Consumers think no agent is selected.TerminalPage::_ResolveEffectiveAgentCliPath()_IsCustomAgentId(); bypassed → calls_BuildAgentCommandLine("qwen", …)which returns the bare"qwen"verbatim, never"qwen.cmd --acp". The launched conpty would try to execqwenwith no flags.IsAddingCustomAcpAgent/IsCustomAcpAgentSelected/EditCustomAcpAgent/CustomAcpCommandPreview_StartsWithCustom(). User can no longer edit or re-open the custom command.DeleteCustomAcpAgentif (idStr.starts_with("custom:"))is false → delete becomes a no-op;AcpCustomCommandis never cleared.AllowCustomAgentsGPOcustom:prefix. Policy bypass.Fix
Always prefix the saved id with
custom:regardless of whether the bare id matches a built-in. ThedisplayNamebranch is preserved so a custom override of a built-in still showscopilot (custom)._MaybeAppendCustomEntryis updated to compute the same id so list rebuilds round-trip correctly.As follow-up hardening,
SaveCustomAcpAgentandSaveCustomDelegateAgentnow also reject empty derived ids so whitespace-only / quote-only commands do not persist a blankcustom:entry.Backward compatibility
Users who already saved a bare id (e.g.
acpAgent: "qwen") from a previous build keep the same broken state until they re-add the agent —EffectiveAcpAgent()was already returning empty for them, so the UI already showed Copilot. No auto-migration needed; theiracpCustomCommandis preserved, so re-saving via Add New with the same command will produce a correctly-prefixed id.Verification
custom:-keyed settings, policy, resolver, and UI paths are now consistent with the new invariant.Repro (before this PR)
copilot/gemini/claude/codex(e.g.qwen.cmd --acp) and click the inline Save.With this fix the entry persists and shows
qwen(or<name> (custom)if the name collides with a built-in).