MCP server management: working Edit button, correct scope handling, Windows spawn quoting, CLI-scope servers - #226
Open
leonidasbarkas98-cpu wants to merge 34 commits into
Conversation
…only)
displayMCPServers() built configDisplay (server command, args, url and
type) by interpolating config.command/config.args.join(' ')/config.url/
serverType straight into serverItem.innerHTML with no escaping. Any of
those four values can come from external ~/.claude.json / .mcp.json
data, so a malicious config.type like <img src=x onerror=alert(1)>
rendered as a live element.
Wrap all four interpolations in escapeHtml(), matching the display-
hardening half of upstream commit f759a955 (fork-issue-39, "harden MCP
configuration handling"). That commit also contains a Windows
spawn-quoting fix and a "show CLI local-scope servers" feature; both
are unrelated config-path changes that belong in the MCP-cluster
bundle, not in this XSS-hardening branch, so only the escaping lines
are picked here. Placed first so every later commit in this branch
builds on top of an already-escaped configDisplay.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (fork-issue-40) parseSimpleMarkdown escaped fenced code blocks but passed prose and inline-code content through unescaped into innerHTML, so a literal HTML tag in a response — for example a <select> dropdown Claude wrote outside a code fence — was parsed as real markup, and an unbalanced tag broke the rendering of that message and everything after it. Escape the remaining text right after code blocks are pulled out into placeholders and before inline markup is applied, so raw < > & become entities while markdown syntax and the __CODEBLOCK__ placeholders stay intact. This also closes the same gap in inline code, whose content was unescaped too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The conversation list interpolated firstUserMessage/lastUserMessage raw into innerHTML, so raw HTML typed in chat rendered as live elements in the history. Escape after truncation so entities are never cut apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k-issue-48) Fenced code blocks over a configurable line threshold (default 20) render as native <details>/<summary> with a caret, language and line count in the header; shorter blocks keep byte-identical markup. New claudeCodeChat.ui.collapseLongCodeBlocks / .collapseCodeBlockLines settings, with a catch-up pass in the webview because settingsData arrives after history replay. Every user/claude/error message also gets a manual fold button in its header, independent of the auto-collapse. Threshold/line-count logic lives in the self-contained, .toString()-spliced src/collapse-rules.ts, covered by a new test:collapse-rules suite (18 cases, including a splice-sandbox test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bview (fork-issue-49) Follow-up to fork-issue-40: the permission bubble, the tool-call input view, the prompt-snippet list and the file picker all interpolated model- or user-controlled text straight into innerHTML, so a Bash command like echo "<b>hi</b>" tore the permission bubble apart and a command containing a double quote broke out of the always-allow title attribute. escapeHtml() serialises through textContent/innerHTML and therefore leaves " and ' untouched, so attribute sinks need their own escaper: add escapeAttr in a self-contained html-escape.ts, spliced into the webview via .toString() like the collapse helper, and cover it with unit tests under plain mocha. Also drop the manual "/&andrepimenta#39; decoding in toggleExpand -- getAttribute already returns decoded values, so that second pass destroyed any value containing entity text -- and render the TodoWrite summary as textContent, which never needed markup in the first place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
script.ts declared function enableYoloMode twice in the same scope; the
later declaration won and dereferenced permissionMenu-undefined on the
argument-less inline chat button, so the button died with a TypeError.
Consolidated into one function: with a permissionId it keeps the
permission-menu behavior (now with a null guard on the menu element),
without one it posts the lightweight {type:'enableYoloMode'} message --
the extension host writes the single permissions.yoloMode key and the
settingsData round-trip updates checkbox and warning banner. The
formerly dead early variant's updateSettings() path was deliberately
not revived: it would push 21 unrelated keys to global settings.
No automated test asserts the declaration appears exactly once in this
branch; verified manually that the button no longer throws. Known edge
(workspace-less window, pre-existing on the menu path) tracked as
fork-issue-59.
The earlier build of this commit's conflict resolution left two unrelated
functions, isOutputTokenLimitError() and openMaxOutputTokensSettings(), in
place of the deleted duplicate -- foreign context from elsewhere with no
caller in src/ and no matching 'openMaxOutputTokensSettings' case in
extension.ts. That dead code is dropped here; the duplicate early
enableYoloMode() is simply removed, per the description above.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onclick (fork-issue-58) The always-allow list in the settings modal interpolated toolName and command into an inline onclick handler through escapeHtml, which leaves ', " and \ untouched. Any pattern containing quotes or backslashes -- practically every Windows path pattern like python "C:\Users\..." * -- broke the attribute or the JS string literal, so clicking Remove did nothing. Move the values into data-tool/data-command via escapeAttr and read them back through this.dataset (same pattern as the snippet delete button), which also hands the extension the exact raw string its removal filter compares against. Escape the tool name and command shown in the list while at it. No dedicated automated regression test covers renderPermissions/ removePermission in this branch yet; verified manually that the always-allow Remove button works again for tool names and commands containing quotes and backslashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
escapeHtml only escapes & < >, so every attribute built with it stayed open for
a " breakout. formatToolInputUI({file_path: 'a" onmouseover="alert(1)" zz="'})
produced a real onmouseover attribute on the emitted span, and the webview CSP
(default-src * 'unsafe-inline' 'unsafe-eval') would have fired it -- file_path
is model-generated tool input.
All attribute sinks in script.ts, skills-script.ts and plugins-script.ts now use
escapeAttr. Inline handlers that interpolated a value (openFileInEditor,
sendPlanAction) move to the data-* + this.dataset.* pattern established in fork-issue-58.
That also fixes two older bugs at those call sites: a path containing an
apostrophe broke the handler syntactically, and backslashes were never escaped,
so C:\tmp\a.txt reached the handler with \t expanded to a tab.
Covered by six new tests that run the emitted getScript() output in a vm sandbox
and parse the result with parse5 (src/test/webview-attr-escape.test.ts). parse5
was only available transitively via @vscode/vsce -> cheerio and is now an
explicit devDependency, because the test lives under src/ and tsc would fail
the build if it ever disappeared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…issue-59) _enableYoloMode wrote permissions.yoloMode with ConfigurationTarget.Workspace only, without the global fallback _updateSettings has for the same key. In a window with no workspace folder open, config.update() threw, the bare catch swallowed it and _sendCurrentSettings() never ran -- the setting was silently not persisted. The chat said "YOLO Mode enabled!" anyway, because that confirmation fired client-side the moment the button was clicked and never depended on a response from the extension host. That was the actual defect: the fallback alone would still have left the message unconditional. The workspace-then-global decision now lives in one pure, vscode-free function (settings-batch.ts), shared with _updateSettings' handling of the same key, and _enableYoloMode reports success only after the write went through -- otherwise _reportYoloModeEnableFailure logs the error, shows a VS Code error message and sends a yoloModeEnableFailed response. An outer try/catch makes that hold even for failures outside the update itself, e.g. a mismatched deploy where settings-batch.js is stale: previously that would have been an unhandled rejection with no feedback at all. Also dropped the module comment's "same pattern as shell-utils/perm-log-redact" comparison -- neither module exists in this branch (shell-utils.ts belongs to the separate MCP-cluster bundle; perm-log-redact.ts was removed here as a fork-issue-51 fix with no caller in this branch, see that commit's absence in this history). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
displayMCPServers interpolated the server name straight into a JS string
literal inside an HTML attribute -- onclick="editMCPServer('${name}', ...)" --
with no escaping at all, and passed the whole config object alongside it as
JSON.stringify(...).replace(/"/g,'"'). A single quote in the name broke
out of the call; a double quote broke out of the attribute entirely. Server
names can come from a cloned workspace's own .mcp.json (extension.ts:2740,
read in at extension.ts:2770), and the webview CSP is default-src *
'unsafe-inline' 'unsafe-eval'.
The config object can never be escapeAttr'd as an attribute value, so it no
longer touches an attribute: displayMCPServers records it in a per-render map
and editMCPServer(name) looks it up. Name and scope travel through
data-server-name / data-server-scope with escapeAttr plus this.dataset.*, and
serverType is escapeHtml'd before it reaches innerHTML.
Nine tests (src/test/webview-attr-escape.test.ts) cover both payload classes
-- x'); alert(1); (' does not break out of the attribute but is valid
multi-statement JS in the handler, while x" onmouseover="alert(1)" y="
injects a real attribute -- all verified to fail against the previous code.
extractFunction now skips comments: an apostrophe in an existing comment
inside editMCPServer desynchronised its brace matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rk-issue-61) Three sinks handled data from outside the extension with no escaping at all. renderDropdown, renderAllModels and renderOpenCreditsModelCards wrote model.id, model.name and model.owned_by/provider straight into data attributes and into innerHTML. The data comes from fetch(OPENCREDITS_API_URL + '/v1/models'): model-updater's resolveLatestModels() overwrites the bundled names with the API values, the extension posts them as updateRecommendedModels, and the webview replaces its model list and re-renders the cards without any user interaction. A name of <img src=x onerror=...> produced a live element in the DOM. href and src from the open MCP registries were escaped after fork-issue-57 but their scheme was never checked, so a registry entry with "url": "javascript:..." was passed through verbatim. The new safeHttpUrl() only lets http:/https: through, strips tab/CR/LF and leading C0 control characters first and compares the scheme case-insensitively; anything it cannot make sense of is dropped rather than rendered, and callers omit the attribute instead of emitting a dead one. addEnvVariableRow built value="..." unescaped, which truncated any value containing a double quote. Covered by 21 tests (src/test/webview-attr-escape.test.ts), each one verified to fail against the previous code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…issue-62) copyCodeBlock read data-raw-code with getAttribute -- which the HTML parser has already decoded -- and then decoded "/</>/& a second time by hand. For ordinary code that was a no-op, but any code containing those entity texts literally came out of the clipboard mangled: "literal " entity" was pasted as 'literal " entity'. There is exactly one place that writes the attribute (parseSimpleMarkdown, via escapeAttr) and all four callers pass raw text, so getAttribute already returns the original. Verified over 19 inputs -- CRLF, Unicode, NUL, 200k characters, the $& replacement specials from fork-issue-55, placeholder lookalikes -- that the clipboard text now equals getAttribute exactly in every case. createExpandableInput was dead code from upstream 73c4a38: no caller anywhere, no dynamic access, and formatToolInputUI already provides the same expand/collapse markup with proper escapeAttr escaping, whereas the dead copy carried a hand-written escaper that missed &. Removed. A future upstream merge will conflict at that spot. Covered by five tests (src/test/webview-attr-escape.test.ts) driving the real extracted parseSimpleMarkdown and copyCodeBlock through parse5. No automated needle test guards against createExpandableInput reappearing in this branch; a future upstream merge conflicting at that spot is the practical safeguard. The removal comment left in script.ts pointed at build/check-webview-syntax.js, a file that is not part of this branch. Reworded to drop the dangling reference; the comment's actual point (don't spell out the old identifier in the emitted <script> text) still stands on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fork-issue-55) Replacement strings containing $&, $`, $' or $$ were interpreted as special replacement patterns by String.replace, corrupting restored code blocks. Extract the restore loop into markdown-restore.ts using a function replacement (same pattern as escapeAttr's build-time splice), add test:markdown-restore (9 tests). Cherry-picked from fork-issue-55's original branch onto this branch's own history (this branch never had fork-issue-47/KaTeX, so build/check-webview-syntax.js and its fork-issue-55 assertion additions, plus the now-irrelevant test:restore-commit-utils/test:perm-log-redact/ test:webview-syntax package.json lines and the renderMathEnabled restore guard, are intentionally dropped here -- foreign context from fork-issue-55's original branch that doesn't apply to this one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-63)
User messages went through parseSimpleMarkdown, so asterisks, underscores and
backticks in a prompt were rendered away: a path like src/_test_.ts came back
italicised, and what you sent was no longer what you read in the transcript.
They now render as raw text -- escapeHtml only, with white-space: pre-wrap so
line breaks survive. Fenced code blocks are the one intentional exception:
pasting code should keep its language label, copy button and fork-issue-48 collapse. To
avoid a second implementation, the fence extraction moved out of
parseSimpleMarkdown into a shared extractCodeBlocks(), which both paths call;
user text then goes through escapeHtml and the placeholders are put back with
restoreCodeBlockPlaceholders (fork-issue-55), so data-raw-code keeps its fork-issue-62 behaviour.
Claude and thinking messages are untouched. The refactor was verified to be a
pure move: 28 targeted plus 4000 random inputs produce byte-identical
parseSimpleMarkdown output before and after, and 6000 adversarial payloads
through the user path produce no tag or attribute outside the fixed code-block
vocabulary.
.message.user .message-content also sets line-height 1.6, which the text
inherited from .message p before it stopped being wrapped in paragraphs.
Also dropped the sandbox helper's stub list 'function isOutputTokenLimitError()
{ return false; }' line for a function addMessage() never actually calls in
this branch (see fork-issue-52's dead-code note) -- along with the comment
referencing it.
fork-issue-55 had not actually been cherry-picked into this branch when
fork-issue-63 first landed here, so renderUserMessageContent's placeholder
restore was rebuilt inline as a plain html.replace(placeholder, str) --
reintroducing the exact "$&"/"$`"/"$'"/"$$" substitution-pattern bug
fork-issue-55 exists to fix, in a second, newly-added call site. fork-issue-55
is now cherry-picked directly before this commit; renderUserMessageContent
calls the real restoreCodeBlockPlaceholders() like the message above always
claimed it did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-64) extractFunction found a function's source by counting braces. The scanner knew about strings and comments but not about regex literals, so escapeAttr's own .replace(/'/g, '&andrepimenta#39;') made it read the apostrophe as a string start and lose the count: 362 characters of escapeAttr came back as 2727, dragging safeHttpUrl, openFileInEditor, formatFilePath, toggleDiffExpansion and toggleResultExpansion along and loading formatFilePath into the vm sandbox twice. That was harmless only by accident -- escapeAttr sits at the front of the chunk and the later, correctly extracted copy won. Editing or moving toggleResultExpansion would have ended the extraction somewhere else or thrown "unbalanced braces" and taken every test in the file with it. TypeScript is already a devDependency, so the helper now asks its parser for the declaration's span instead of re-implementing a tokenizer. 24 of the 25 extracted functions come back byte-identical; only escapeAttr changes, which is the fix. The five helpers that had been hand-copied into two test files now live in src/test/webview-dom-helpers.ts. No production code changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ers (fork-issue-70)
Issue fork-issue-70 claimed parseSimpleMarkdown/renderUserMessageContent had reintroduced
the fork-issue-55 html.replace(placeholder, string) bug. At this branch's current tip
both call sites already used restoreCodeBlockPlaceholders (fork-issue-55's
function-replacer, already part of this branch's history), cherry-picked before fork-issue-63
landed -- fork-issue-63's own commit message documents that this had already been fixed up
within the same commit. No production code change was needed for the fork-issue-70 finding
itself.
Added the missing full-pipeline regression coverage fork-issue-70 asked for: a code
block containing "$&"/"$`"/"$'"/"$$" must not duplicate the surrounding
prose, exercised through the real parseSimpleMarkdown AND the real
renderUserMessageContent (not just a direct unit call to
restoreCodeBlockPlaceholders, which markdown-restore.test.ts already covered).
While wiring that up, found and fixed an unrelated, pre-existing gap in two
vm-sandbox test loaders: loadCodeBlockSandbox (webview-attr-escape.test.ts)
and loadUserInputPipelineSandbox (user-message-rawtext.test.ts) extracted
parseSimpleMarkdown/renderUserMessageContent but never extracted
restoreCodeBlockPlaceholders itself (script.ts splices it in via
`${restoreCodeBlockPlaceholders.toString()}`, a separate statement, not part
of either function's body) -- every test calling those two functions through
either sandbox threw "ReferenceError: restoreCodeBlockPlaceholders is not
defined" (14 failures, confirmed present before this commit via git stash).
Both loaders now include it alongside their other extracted dependencies.
141 passing, 0 failing (was 127 passing / 14 failing). eslint: 0 errors (25
pre-existing warnings, none in touched files). git grep -il agentdeck: empty.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tch handling Translates German-language code comments to English and removes internal process/tracker references (review-tool name, tracker name, personal name) so the branch reads cleanly as a standalone PR. Also fixes a real bug found while correcting a stale comment in _updateSettings: the loop over settings keys had a single outer try/catch, so one failing key silently discarded all subsequent keys and skipped resending settings/balance to the webview. Each key now gets its own try/catch instead.
The previous "translate internal review comments" pass removed jargon and personal names but left every bare #NN comment reference untouched, so GitHub would still auto-link them to unrelated issues on this repo. Each bare reference (140 across 9 source and 6 test files) is requalified as fork-issue-NN. Genuine "upstream #NNN" references (8x "upstream andrepimenta#151", 1x "upstream andrepimenta#63" in script.ts's extractCodeBlocks/parseSimpleMarkdown comment) are left untouched since they point at the upstream project's own issue tracker, not ours.
Every other new test file (collapse-rules, html-escape, webview-attr-escape, markdown-restore, user-message-rawtext) has its own npm run test:<name> script; settings-batch.test.ts was missing one. No test-coverage change -- the wildcard mocha run in this repo's verification already picks it up.
Several comments in this branch pointed at modules/functions that only exist on sibling branches (getMathScript()/math-script.ts/math-segments.ts/ findMathSegments/restoreMathSegments/diff-utils/shell-utils/ auto-model-switch) -- none of that exists here; the analogies now point at collapse-rules.ts/html-escape.ts/markdown-restore.ts/settings-batch.ts, the sibling pure-logic modules that actually exist in this tree. script.ts's sendOnEnter/renderMathEnabled comment referenced identifiers that never existed anywhere else in this file; pointed it at the real collapseLongCodeBlocks/collapseCodeBlockLines slots instead. Also dropped the two leftover renderMathEnabled: false stub properties (and the comments explaining them) from the webview-attr-escape/user-message-rawtext test sandboxes -- dead, the code under test never reads that variable. Removed leftover internal process-tracking markers from collapse-script.ts and extension.ts comments; reworded without changing behaviour. No production logic changed, comments/test-sandbox-setup only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
script.ts's escapeAttr/restoreCodeBlockPlaceholders splice comments still compared against "math-script", a module that does not exist on this branch (only collapse-script.ts does); collapse-script.ts's per-message fold comment still carried an internal "Phase 2:" planning-round marker. Neither had a production effect, comments only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several code comments pointed at lines that no longer match after earlier edits/rebases; the referenced behaviour itself is unchanged. - settings-batch.ts:16 dropped a dead "extension.ts's _permLog" reference (git grep finds no such symbol anywhere else). - script.ts's enableYoloMode comment now points at the real permission-menu item (~4060, was ~4604), the real inline chat button sites (~219/~476, was ~254/~639), and the real settingsData handler / updateYoloWarning() locations (~5360/~5392, was ~6147/~6150 -- past EOF, the file has 5569 lines). - script.ts:181's ui-styles.ts reference for .copy-btn's margin-left:auto now points at 1129 (was 1152, which is .message-collapse-btn's font-size). - ui-styles.ts:1210's reference for .message p's line-height now points at 3855 (was ~4045, past that rule's actual location). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent MCP fixes plus a display hardening. Windows spawn quoting (upstream andrepimenta#125): without a custom executable the Claude process is spawned with shell:true, where Node joins the argv array into one command line without quoting. cmd.exe then splits any argument containing a space — for example an --mcp-config path under a user profile like "Vincent K. Bae" — into two, producing an "MCP config file not found" error. A small quoteWinShellArgs helper (in its own vscode-free module so it is unit-testable under plain mocha) wraps space-containing args in quotes only when a shell is used; a no-op otherwise, so the command line is byte-identical for paths without spaces. Show CLI local-scope servers (upstream andrepimenta#100): MCP servers configured via the CLI (~/.claude.json -> projects[cwd].mcpServers) were used but never listed, so the panel said "none configured". They are now merged in read-only, matched case-insensitively with normalized slashes since the CLI stores project keys with forward slashes and inconsistent drive casing while uri.fsPath uses backslashes. Rendered with a "via CLI" badge and no edit/delete, because that scope has no config path of its own and must never be written by the extension. Also escape the MCP server name/command/args/url when rendering the list, since this now surfaces external ~/.claude.json data. upstream andrepimenta#106 and upstream andrepimenta#140 were verified as already fixed in 2.2.0 and are not touched here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
editMCPServer never ran: its first statement dereferenced
getElementById('addServerBtn'), and no element in the emitted HTML carries that
id (upstream deca7de). Clicking Edit threw a TypeError, the form never appeared.
Guarded, so the rest of the function -- and this fix -- can run at all.
It also never set the scope select, so editing a server while the select still
showed a different scope wrote the update to the other config file and left the
original behind as a duplicate. The select is now set from the server's own
_scope and disabled while editing: changing it would be a move between two
config files, which is implemented nowhere, so it only ever produced duplicates.
installMarketplaceServer clears the lock again, like it already did for the name
field.
Two robustness fixes in displayMCPServers: the config map is Object.create(null)
so a server named toString or constructor cannot resolve through the prototype
chain past the "not found" guard, and serverType goes through String() -- a
non-string type in a .mcp.json threw mid-loop and swallowed every remaining
server plus the buttons after the list.
The test harness handed out an element for any id asked of it, which is why the
null dereference above stayed green. FakeDocument now returns null for ids that
do not exist in the real getHtml() output, parsed from it rather than
hand-maintained; removing the guard turns six tests red on that one cause. A
sweep of all ids used by editMCPServer, hideAddServerForm and showAddServerForm
against the 189 real ones found no further mismatch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…issue-68) .marketplace-detail-header is a flex row without flex-wrap, and the name box used flex: 1 -- which means flex-basis: 0%. Flexbox decides where to wrap using each item's hypothetical main size, so the name counted as zero no matter how long it was: the row never wrapped, the name box was left with whatever space the icon and the ~208px select+install block did not take, and since .marketplace-detail-name sets neither white-space nor overflow, the text spilled out of its own box and under the actions, which paint later. flex-wrap on the row plus flex: 1 1 auto on the name box makes the name enter that calculation with its real content width, so a narrow panel wraps the actions onto their own line instead. Wide panels are unaffected -- the only growing item fills the remaining space either way. Pre-existing since the upstream import deca7de; fork-issue-61 and fork-issue-65 did not touch this layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
executeSlashCommand called addMessage('user', `Executing /...`, 'assistant') --
arguments in the wrong order for addMessage(content, type, timestamp, rawText).
'user' became the content, the whole sentence became the type and landed as
class tokens on the bubble, and 'assistant' became a timestamp nobody reads.
Since the type matched none of user/claude/error, the bubble got no header:
no icon, no copy button, no collapse button, no timestamp -- and it displayed
the literal word "user".
Repairing the call would have produced two identical notices, because the host
already posts the same text back as terminalOpened; the broken one just never
looked like a duplicate. So the client-side call is gone instead. Verified that
the host covers every case it covered: _executeSlashCommand returns early only
for /compact, which the client branch skipped too, and nothing between that and
the postMessage can throw or bail out. The notice now arrives one round trip
later, which is the only difference.
Of the roughly 31 addMessage call sites, this was the only one with swapped or
unsupported arguments.
The new 'webview executeSlashCommand terminal notice (fork-issue-66)' mocha
suite (src/test/user-message-rawtext.test.ts) asserts executeSlashCommand no
longer appends a message of its own for any command while still dispatching
it to the host, and that the host's terminalOpened response renders the
correct notice -- so the duplicate cannot come back unnoticed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ork-issue-67) #serverScope had no <option> for 'extension'-scope servers, so the field editMCPServer() locks to the server's own scope (fork-issue-65) read back as '' for them -- saveMCPServer() posted that empty scope, which the host dispatch turns into 'project', writing a second copy of the server into the workspace's .mcp.json while the original stayed in the extension config. saveMCPServer() now takes the scope from the edited server's own config instead of the locked, display-only select. showAddServerForm() and hideAddServerForm() share one resetAddServerFormFields() helper, so a fresh "Add manually" after an abandoned edit no longer inherits the previous server's name, scope or fields. #serverScope gains a disabled 'extension' option so the locked field is readable, and the reset puts its selection back to 'project' -- a disabled <option> still stays selected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…'s own config (fork-issue-69) _getMCPConfigPathForScope used to end in a catch-all that returned the extension's own config path for anything that wasn't 'local', 'global' or 'project'. An unknown or empty scope therefore wrote silently into a file the caller never asked for. It now resolves to undefined, which the existing guards in _saveMCPServer/_deleteMCPServer already turn into an mcpServerError. The decision itself moves into src/mcp-config-path.ts: a pure function with no vscode import, taking the home dir, workspace folder and extension storage path as arguments. Sitting in a private method of the vscode-dependent provider class, the hardening was untestable -- it could be reverted without a single test going red. It now has 13 unit tests behind `npm run test:mcp-config-path`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d sandbox Adapting the fork-issue-66 fix from another branch in this stack added an executeSlashCommand test sandbox that also extracted and loaded formatMessageTimestamp, mirroring a variant of addMessage that already carries a timestamp param. That function does not exist on this branch (the message-timestamps feature was never part of this stack) and addMessage here still takes only (content, type), same as the existing fork-issue-63 suite in this file already documents. Without this, the three new fork-issue-66 tests failed at runtime with "function formatMessageTimestamp not found in emitted script" even though tsc and every other suite was green. No production code changed.
The "via CLI" badge for read-only, CLI-configured MCP servers (fork-issue-39) had no CSS rule, so it rendered as plain unstyled text, and its data-tooltip attribute did nothing without a :hover::after rule like the existing .beta-badge/.mcp-auth-btn ones. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
added 4 commits
July 29, 2026 05:47
editMCPServer() only ever conditionally populated #serverArgs/#serverEnv/
etc. ("if (config.args...)") and never cleared them first, so editing
server A (e.g. stdio with args/env) and then, without Cancel or Save in
between, editing server B (no args/env of its own) left A's values
sitting in the form -- "Update Server" for B would silently write A's
args/env into B's config. Only became reachable through the addServerBtn
null-guard fix (fork-issue-65): before that, editMCPServer() threw before
ever reaching the field population.
Fix: call resetAddServerFormFields() at the top of editMCPServer(), before
populating the target server's own values, same full reset the "Add
Server" button already uses. Also hide #mcpServersList while the edit
form is open, matching showAddServerForm() (previously only
#popularServers was hidden, leaving the list visible behind the form).
…ments Five lines this branch added left the German word in otherwise-English comments/test titles (src/script.ts:1686 already correctly said "Part A"); one of them is a mocha test title, so it showed up in every test run's output. Translate all five to match.
…ates Three comments cited files that do not exist on this branch, likely copy-pasted from a template shared with other fix branches. - src/mcp-config-path.ts and src/test/mcp-config-path.test.ts named restore-commit-utils and perm-log-redact as "same pattern" siblings; neither module exists here. Point at settings-batch/shell-utils and settings-batch/quote-win-shell-args instead, which do. - src/test/webview-attr-escape.test.ts credited a check-webview-syntax.js "PASS proof" that does not exist on this branch (or upstream); reworded to reference the getHtml(...) call this suite already makes itself. - src/test/user-message-rawtext.test.ts's messageRawText stub cited "fork-issue-46", an unrelated issue from a different branch in this stack; reworded without the stale issue number. Also drops the internal "(2nd round)" review-round markers from two webview-attr-escape.test.ts comments -- an artifact of this stack's own review process, not part of the bug description. No production behaviour changes; comments/test descriptions only.
…bers in comments - src/script.ts and src/test/user-message-rawtext.test.ts described /compact as running via _startCompact() through a "summarize-and-restart flow"; neither exists on this branch (extension.ts's _executeSlashCommand hands /compact to _sendMessageToClaude() as a plain chat message instead). Comments now describe that actual path. - src/mcp-config-path.ts and src/test/mcp-config-path.test.ts credited the unknown/empty-scope-resolves-to-undefined hardening to fork-issue-67, which only touched script.ts/ui.ts on this branch; the hardening and its extraction into this module are both fork-issue-69. Also stopped claiming the extraction is "unchanged" behaviour -- the catch-all branch used to fall through to the extension's own config path and now resolves to undefined instead, which is the whole point of fork-issue-69. - Dropped five leftover "review"/"review click path"/"review PoC" markers from mocha suite/test titles in webview-attr-escape.test.ts; those are printed on every test run and referred to this stack's own review process, not the bug being tested. No production behaviour changes; comments and test titles only.
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.
Stacked on top of #224 — please review/merge that one first; only the last 12 commits (from
2fe4259onwards) are new here. Everything below2fe4259in the diff belongs to that PR, whose escaping helpers this branch builds on.Fixes for the MCP server panel, where several paths were broken outright rather than merely rough:
shell: true, where Node joins the argv array into one command line without quoting, andcmd.exethen splits any argument containing a space — an--mcp-configpath under a profile likeC:\Users\Vincent K. Bae\...becomes two arguments and the CLI reports "MCP config file not found". AquoteWinShellArgshelper (its own vscode-free module, so it is unit-testable under plain mocha) quotes space-containing args only when a shell is actually used; for paths without spaces the command line is byte-identical. CLI local-scope servers (upstream MCP Servers configured in Claude Code, don't seem to be available when running Chat #100): servers configured through the CLI (~/.claude.json→projects[cwd].mcpServers) were used but never listed, so the panel claimed "none configured". They are merged in read-only — matched case-insensitively with normalized slashes, since the CLI stores project keys with forward slashes and inconsistent drive casing whileuri.fsPathuses backslashes — rendered with a "via CLI" badge and no edit/delete, because that scope has no config path of its own and must never be written by the extension. Plus the display escaping for server name/command/args/url, now that this surfaces external~/.claude.jsondata. (upstream The VS Code extension Claude Code Chat throws a runtime error on startup due to an invalid, hard-coded MCP configuration path. #106 and upstream Error with MCP server: Storage path not available #140 were checked and are already fixed in 2.2.0; nothing here touches them.)editMCPServerdereferencedgetElementById('addServerBtn')as its first statement, and no element in the emitted HTML carries that id — every click threw aTypeErrorand the form never appeared. With that guarded, the rest of the function becomes reachable, which exposes the next problem: it never set the scope select either, so editing a server while the select still showed a different scope wrote the update into the other config file and left the original behind as a duplicate. The select is now set from the server's own scope and disabled while editing (changing it would be a move between two config files, which is implemented nowhere and only ever produced duplicates);installMarketplaceServerclears the lock again, as it already did for the name field. Two robustness fixes in the same loop: the per-render config map isObject.create(null)so a server namedtoStringorconstructorcannot resolve through the prototype chain past the "not found" guard, and the server type goes throughString()— a non-string type in a.mcp.jsonthrew mid-loop and swallowed every remaining server plus the buttons after the list.<option>forextension, so the field the edit path locks to the server's own scope read back as'', and the host dispatch turned that empty value intoproject— writing a second copy into the workspace's.mcp.jsonwhile the original stayed in the extension config. Saving now takes the scope from the edited server's own config rather than from the display-only select; a disabledextensionoption makes the locked field readable; and the add/hide form paths share oneresetAddServerFormFields()helper, so "Add manually" after an abandoned edit no longer inherits the previous server's name, scope or fields.editMCPServer()only ever populated the args/env fields conditionally and never cleared them first, so editing server A (stdio, with args and env) and then server B (with none of its own) without Cancel or Save in between left A's values in the form — "Update Server" for B would have written A's args and env into B's config. It now calls the same full reset before populating, and hides the server list behind the edit form, matching the add path. Only reachable at all once the null-guard above landed, which is why it had gone unnoticed.local,globalorproject, so an unknown or empty scope wrote silently into a file the caller never asked for. It now returnsundefined, which the existing guards in the save/delete paths already turn into a proper error. The decision moved into a pure, vscode-freesrc/mcp-config-path.tstaking home dir, workspace folder and extension storage path as arguments: as a private method of the vscode-dependent provider class it was untestable and could have been reverted without a single test going red. 13 unit tests behindnpm run test:mcp-config-path.flex-wrap, and the name box usedflex: 1— that is,flex-basis: 0%. Flexbox decides where to wrap from each item's hypothetical main size, so the name counted as zero width no matter how long it was: the row never wrapped, the name got whatever the icon and the ~208px select+install block left over, and since the name rule sets neitherwhite-spacenoroverflow, the text spilled out under the action buttons, which paint later.flex-wrapon the row plusflex: 1 1 autoon the name box lets the name enter that calculation at its real content width, so a narrow panel wraps the actions onto their own line; wide panels are unchanged. Pre-existing since the upstream import.executeSlashCommandcalledaddMessage('user', 'Executing /...', 'assistant')— arguments in the wrong order foraddMessage(content, type, timestamp, rawText).'user'became the content, the whole sentence became the type and landed as class tokens on the bubble, and since that type matched none of user/claude/error the bubble got no header at all: no icon, no copy button, no collapse, no timestamp, and it displayed the literal word "user". Repairing the call would have produced two identical notices, because the host already posts the same text back asterminalOpened; the broken one just never looked like a duplicate. So the client-side call is gone instead, after confirming the host covers every case it covered (the host path returns early only for/compact, which the client branch skipped too, and nothing between that and the post can bail out). The notice now arrives one round trip later — the only behavioural difference. A sweep of the ~31addMessagecall sites found no other with swapped or unsupported arguments.addServerBtncrash above stayed green for so long. It now returnsnullfor ids that do not exist in the real emitted HTML, parsed from that output rather than hand-maintained; removing the guard turns six tests red on that single cause. A sweep of every id used by the edit/show/hide form paths against the 189 real ones found no further mismatch.The remaining commits are small follow-ups: the "via CLI" badge had no CSS rule and no hover rule for its tooltip attribute, so it rendered as unstyled text; and comments carrying stale cross-references, a remaining non-English word and mis-attributed internal issue numbers were corrected. Internal tracker references are written as
fork-issue-NNso they do not auto-link to unrelated issues on this repository; genuine upstream references are kept as such.This is one of several PRs from our fork submitted together as part of a broader, thematically grouped series (permission/session handling, settings/UI, security hardening, rendering, etc. split across separate branches/PRs). This is the MCP branch of that series and it stacks on the security-hardening branch, since the escaping the MCP panel relies on lives there.
tsc --noEmitclean, no new dependencies; full unit run at this tip is 179 passing, 0 failing.🤖 Generated with Claude Code