Three standalone MCP/customizations fixes: group-header casing, unclickable Show Output, duplicated server rows - #330890
Merged
Ulugbek Abdullaev (ulugbekna) merged 3 commits intoAug 17, 2026
Conversation
Copilot started reviewing on behalf of
Ulugbek Abdullaev (ulugbekna)
August 14, 2026 16:37
View session
Connor Peet (connor4312)
previously approved these changes
Aug 14, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes three AI customization/MCP UI regressions.
Changes:
- Preserves localized group-header casing.
- Avoids rebuilding unchanged MCP status actions.
- Deduplicates agent-host MCP server rows and adds tests.
Show a summary per file
| File | Description |
|---|---|
aiCustomizationManagement.css |
Removes automatic header capitalization. |
mcpListWidget.ts |
Stabilizes inline status actions. |
mcpListWidget.test.ts |
Tests status signatures. |
agentHostCustomizationService.ts |
Deduplicates presentable MCP servers. |
agentHostMcpServerCustomizations.test.ts |
Tests server deduplication. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 5/5 changed files
- Comments generated: 4
- Review effort level: Balanced
Ulugbek Abdullaev (ulugbekna)
marked this pull request as draft
August 14, 2026 16:44
Contributor
Screenshot ChangesBase: Changed (6) |
`text-transform: capitalize` on the shared group header re-cased strings that were already cased correctly, and got them wrong: "Included Based on Context" rendered as "Included Based On Context" and "Loaded on Demand" as "Loaded On Demand", against this repo's own rule that short prepositions stay lowercase, and "Built-in" rendered as "Built-In". Per-word capitalization is also not a transform that survives translation, so the rule was wrong for every locale rather than just awkward in English. The header is shared, so this reaches every customizations tab -- Agents, Skills, Instructions, Hooks, Prompts, Plugins and MCP Servers. Every group label on those tabs is already a correctly cased localized string, so the only rendered differences are the three the transform was getting wrong: four screenshot baselines change, on the Instructions and MCP Servers tabs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`updateStatus` began by clearing the row's action disposables and emptying its actions node, then rebuilt them. It runs from an autorun over the server's connection state, and an erroring server re-runs it about twice a second while producing byte-identical content: measured in a real Code OSS build, 9-10 rebuilds per 5 seconds, every one of them a no-op. A DOM node replaced between mousedown and mouseup never receives the click, so the inline `Show Output` button did nothing on precisely the rows that needed it -- the failing ones, which are the only rows that offer it at all. The row's actions are now rebuilt only when something about them changed. `getMcpStatusRenderSignature` reduces them to a comparable value covering both what they render and what they act on; leaving anything out would drop an update that matters, so it is a pure exported function whose test fails to compile if a field is added without being covered. The list re-splices on every customizations change, so `renderElement` would otherwise undo this by clearing the actions itself. It now keys on the row's content identity rather than the entry object, which is recreated on every refresh and therefore says nothing about whether this is the same row. Both guards are pinned by tests that drive the renderer itself, since that is the only place the failure is observable: an erroring server's update is fired repeatedly with unchanged content, and the button must be the same node afterwards, still attached, and still reach `showMcpServerLog` when clicked. A third test asserts a real status change does still rebuild, so the guard cannot be satisfied by never updating. Reverting either guard fails them. `McpServerItemRenderer` is exported for this. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A session can carry two customizations for a single MCP server. The agent host
publishes the declaration as a child of whatever declared it -- a plugin, or
the .mcp.json VS Code syncs into the agent -- and separately mints a top-level
customization for any server the SDK reports before that child can be resolved
by name. `McpCustomizationController._applyOne` never retires the minted entry
once the child becomes resolvable ("Once promoted to a top-level entry, stay
top-level for the session"), so both remain in state:
notion -> file:///.../vscode-synced-customization-.../.mcp.json#mcp=notion
state: stopped <- the declaration
notion -> mcp-top-level:copilotcli:<session>:notion
state: ready, channel: mcp:// <- the live one
Every consumer of getMcpServers saw both, so the servers list rendered the same
server twice with contradictory status. It was worse than a repeat: the list's
matcher only matches when exactly one candidate answers a key, so with two
copies the server's local row could not adopt either, and both fell through as
extra rows.
getMcpServers now drops a child that a top-level customization already speaks
for. The top-level copy wins because it is the one the host treats as live: it
carries the running state and channel, and its id is what the host resolves for
lifecycle and enablement. Position in the tree is the signal, not the shape of
the minted id, which is the host's own business -- and not the absence of an
owning plugin either, since a directory-declared child has none. That last one
is a live trap rather than a hypothetical, so it has its own test.
Nothing else is collapsed. Two plugins that each declare a server named
`search` stay two rows, because they are two servers. Only the presentation
path dedupes; log, diagnostics and id lookups still walk every customization,
so an id from either copy continues to resolve.
The host-side repair is tracked in #330893.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Ulugbek Abdullaev (ulugbekna)
force-pushed
the
ulugbekna/mcp-standalone-fixes
branch
from
August 17, 2026 09:08
947d5f8 to
de3185c
Compare
Ulugbek Abdullaev (ulugbekna)
enabled auto-merge (rebase)
August 17, 2026 09:16
Connor Peet (connor4312)
approved these changes
Aug 17, 2026
Ulugbek Abdullaev (ulugbekna)
deleted the
ulugbekna/mcp-standalone-fixes
branch
August 17, 2026 17:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three unrelated MCP/customizations bug fixes, each still present on
maintoday, kept as three separate commits so they stay individually revertable.They were extracted from #330207, which has been overtaken by upstream and needs a rework against the new
setCustomizationEnablementlayer. These three do not touch enablement, so there is no reason to hold them behind that.Important
6 screenshot baselines change, including two on the Instructions tab. That is expected, not scope creep: fix 1 is a one-line CSS change to the shared customizations group header, so it reaches every tab and both fixture families that render it. Each difference is only a text-casing correction with no layout shift. Full list in fix 1 below.
1.
text-transform: capitalizemangles localized group headerschat: fix: stop capitalizing localized customization group headers— 4bd2a2bSymptom. Group headers re-cased strings that were already cased correctly, and got them wrong:
Root cause.
.ai-customization-group-header .group-labelsettext-transform: capitalize. The labels reaching it are already correctly-cased localized strings, so the transform could only ever re-case them — and per-word capitalization is not a transform that survives translation, so the rule was wrong for every locale rather than just awkward in English. The "On" cases also violate this repo's own UI-label rule that prepositions of four letters or fewer stay lowercase.Why it is safe. The header is shared, so this reaches every customizations tab — Agents, Skills, Instructions, Hooks, Prompts, Plugins and MCP Servers — not just MCP. I checked every group label feeding it (
aiCustomizationListWidget.ts,pluginListWidget.ts,mcpListWidget.ts): all are already correctly-cased localized strings ("Workspace", "User", "Plugins", "Extensions", "Built-in", "Remote", "Enabled Locally", "Disabled Locally", "Agent Instructions", …). So the only rendered differences are the three the transform was getting wrong.Fixture impact — please accept these baselines. Rendering each affected fixture family on
main's CSS and on this branch, 6 images differ:aiCustomizationManagementEditor/InstructionsTabaiCustomizationManagementEditor/McpServersTabScrolledaiCustomizationListWidget/InstructionsTabWithItemsThis matches the screenshot-diff report on the PR exactly. I originally listed only the first four: I had rendered just the
aiCustomizationManagementEditorfamily, and the shared header is also used byaiCustomizationListWidget. Each of the six was then checked visually — only the casing changes, with no layout shift, no other text, and no other fixture affected.InstructionsTabmoving in an MCP-titled PR is the shared header fix doing its job.2.
Show Outputis unclickable on a failing MCP servermcp: fix: make Show Output clickable on a failing MCP server— 624a7c3Symptom. The inline
Show Outputbutton on an erroring MCP server row does nothing when clicked — on precisely the rows that need it, since a failing row is the only kind that offers the button at all.Root cause.
McpServerItemRenderer.updateStatusbegan with an unconditionaland then rebuilt the actions. It runs from an autorun over the server's connection state, and an erroring server re-runs it roughly twice a second while producing byte-identical content — measured in a real Code OSS build at 9–10 rebuilds per 5 seconds, every one a no-op. A DOM node replaced between
mousedownandmouseupnever receives the click, so the button was being destroyed out from under the user mid-press.Fix. Rebuild only when something about the actions actually changed.
getMcpStatusRenderSignaturereduces a row's actions to a comparable value;updateStatusearly-returns when it is unchanged.The signature has to cover everything the actions are built from — both what they render (status label, class, icon, and the server name that goes into the button titles and aria labels) and what they act on (the active-session twin's id and log channel, the local server it falls back to, and the session resource captured when the output handler is built). Leaving any of it out would silently drop an update that matters, so it is a pure exported function rather than a comment. Its test uses a
{ [K in keyof IMcpStatusRenderInput]-?: … }mapped type, so adding a field to the input fails to compile until it is given a differing value.The list re-splices on every customizations change, so
renderElementwould otherwise undo all of this by clearing the actions itself. It now keys on the row's content identity rather than the entry object — entries are recreated on every refresh, so object identity says nothing about whether this is still the same row.Both guards are pinned by renderer-level tests, since that is the only place the failure is observable: an erroring server's update is fired repeatedly with unchanged content, and the button must be the same node afterwards, still attached, and still reach
showMcpServerLogwhen clicked. A third test asserts a real status change does still rebuild, so the guard cannot be satisfied by never updating. Verified to bite by reverting each guard in turn — removing theupdateStatusearly return fails 2 of 3; defeating therenderElementrow guard fails the re-render test.Why it is safe.
actionDisposablesis only ever added to insideupdateStatus, always after its ownclear(), so nothing accumulates when a rebuild is skipped. The autoruns inrenderElementstill get re-created unconditionally, because they capture the freshelement. When the signature is unchanged, the retained buttons are bound to values the signature proves are equivalent.3. One agent MCP server renders as two rows
mcp: fix: show one row per agent MCP server, not one per customization— de3185cSymptom. A single MCP server shows up twice in the servers list, with contradictory status.
Root cause. A session can carry two customizations for one server. The agent host publishes the declaration as a child of whatever declared it (a plugin, or the
.mcp.jsonVS Code syncs into the agent), and separately mints a top-level customization for any server the SDK reports before that child resolves by name.McpCustomizationController._applyOnenever retires the minted entry once the child becomes resolvable — its own comment says "Once promoted to a top-level entry, stay top-level for the session" — so both persist. From a user's AHP logs:getMcpServersreturned both. It is worse than a repeat: the list'sActiveSessionMcpServerMatcher.take()only matches when exactly one candidate answers a key, so with two copies the server's local row cannot adopt either, and both fall through as extra rows.Fix.
getMcpServersdrops a child that a top-level customization already speaks for. The top-level copy wins because it is the one the host treats as live — it carries the running state and channel, and its id is what the host resolves for lifecycle and enablement.The signal is position in the tree, deliberately not two tempting alternatives:
flattenMcpServerCustomizationssetspluginonly forCustomizationType.Plugincontainers, so a Directory-declared child carriesplugin: undefinedtoo. Treating that as top-level would let a directory child both claim its name and exempt itself from shadowing — i.e. re-create the bug for exactly the.mcp.jsoncase above. There is a regression test for the directory container specifically.Why it is safe. Nothing else is collapsed: two plugins that each declare a server named
searchstay two rows, because they are two servers. And only the presentation path dedupes —showMcpServerLog,_findMcpServer, and the diagnostics/tracking paths still walk every customization viaflattenMcpServerCustomizations, so an id from either copy continues to resolve.Follow-up (out of scope). The true repair is upstream in
McpCustomizationController._applyOne(src/vs/platform/agentHost/node/shared/mcpCustomizationController.ts), which should retire the minted top-level entry once the declaring child becomes resolvable. This PR fixes the symptom in the workbench; the host-side repair is tracked in #330893.Validation
npm run transpile-client— cleannpx tsc --project ./src/tsconfig.json --noEmit --skipLibCheck— no errors from this PR (the 2 remaining are present on cleanmaintoo, inagentHost/node/copilotandagentHost/test/node)./scripts/test.sh --grep "aiCustomization|mcpListWidget|agent host|agentHost|MCP"— 591 passing, 0 failingnpm run eslint <touched files>— cleannpm run stylelint— no new findingsnpm run valid-layers-check— cleannpx component-explorer render …over theaiCustomizationManagementEditorandaiCustomizationListWidgetfamilies — 6 images differ, each verified as the fix 1 casing improvement, matching the screenshot-diff report