feat(agent-setup): configure VS Code - #438
Draft
Menci wants to merge 50 commits into
Draft
Conversation
Menci
force-pushed
the
vscode-agent-setup
branch
2 times, most recently
from
August 8, 2026 11:21
deb32e6 to
6c82bae
Compare
VS Code reaches Floway through the bundled Copilot extension's `customendpoint`
vendor, the only non-deprecated arbitrary-URL provider that ships in stable.
Its group takes one of three API paths, and Floway serves all three for every
model, so that is a group-wide preference rather than a per-model derivation.
Models are enumerated rather than discovered, for the same reason Zed's are:
`customendpoint` reads only `id` off a `/models` response and drops every model
it cannot type, and a group-level `url` is worse than useless — it is not even a
declared property, yet it short-circuits into that discovery branch and
suppresses the explicit list, leaving the provider empty. So the gateway
projects the catalog and embeds it, and the installer keeps only the merge.
The projection lives beside Zed's in one module, which is what keeps the two
halves honest: the same entries reach jq and PowerShell, and the dashboard
preview imports them too. Only the endpoint and the credential are attached
client-side — the gateway does not render its own origin, and the key already
appears once in the script. A pasted snippet has no merge to attach them, so
the preview builder does it instead.
The key rides in `requestHeaders` rather than the group's `apiKey`: that
property is declared `secret`, so VS Code runs its `${input:...}` decoder over a
literal and lands on a secret-storage miss. `requestHeaders` survives the header
sanitizer because this vendor un-reserves `authorization` for endpoints behind
gateways. The trade is that the document carries the credential, so it is
written owner-only.
Every installed build and every profile within it is configured in one pass,
because the active profile is not discoverable from outside the editor. Each
profile is its own transaction and a failure does not stop the others, so one
hand-edited file cannot leave the rest unconfigured; the run still exits
non-zero. Migration 0079 backfills the new configuration key, without which the
strict schema would lock every existing owner out of Agent Setup.
Menci
force-pushed
the
vscode-agent-setup
branch
from
August 8, 2026 16:29
ae56137 to
d6d7bff
Compare
# Conflicts: # packages/agent-setup/scripts/test-installers.ts
The projection omits the per-model endpoint and the credential because a setup run's merge attaches them — and a pasted snippet has no merge, so the dashboard attached them separately. Two implementations of one rule is the shape of divergence the server-side projection was built to remove, and nothing measured them against each other: changing the snippet to emit a wrong URL and a capital-A `Authorization` — which `customendpoint` does not un-reserve, so the sanitizer drops it and every request 401s — left all 602 web tests and all 26 VS Code installer tests green. `addressVSCodeModels` now states it once, beside the projection. The dashboard builds its snippet from it, and both installers are asserted against it for all three API paths: what a run writes must equal what the operator pastes. The capital-A mutation fails that assertion.
# Conflicts: # packages/agent-setup/src/script-assets.generated.ts
ConvertTo-Json emits a subtree deeper than its limit as the literal string
"@{k=}" with only a warning, and the staged check inspects our own group alone
— so a sibling gateway's group nested deeply enough was replaced by a string
under exit 0. Same promotion the Zed half just took.
The other half of that pass does not carry over. This document is rewritten
wholesale by VS Code itself whenever Manage Models changes anything, so its
comments are already volatile and there is nothing to refuse; and it carries
the API key, so it is owner-only by design rather than inheriting whatever mode
the operator had.
# Conflicts: # apps/web/src/components/api-keys/agent-setup-card.tsx # apps/web/src/i18n/locales/en.ts # apps/web/src/i18n/locales/zh-Hans.ts
… damage VS Code reads chatLanguageModels.json with its JSONC-tolerant scanner, so a comment is content the editor accepts. jq refuses such a document, PowerShell 7 accepted it and dropped the comment on the way out — data loss reported as success — and 5.1 errors, three outcomes for one file. Both halves refuse it now, through the same shared helpers the Zed half uses, and the Bash message names the comment instead of blaming the list shape. The Bash gate also accepted a stream of JSON documents, so `[…][…]` passed and was merged once per document. Slurping asks for exactly one. `grep` was missing from the harness's hermetic tool list, so the Bash comment check silently never ran — the same gap `stat` had. The suite now links it, and reverting either half's refusal fails the new case.
# Conflicts: # apps/web/src/components/api-keys/agent-setup-card.tsx # packages/agent-setup/src/script-assets.generated.ts
The Bash half matched a line-leading `//`, so a block comment or a trailing one fell through to the jq gate and was refused as a malformed provider list — the wrong stated cause, which is exactly what naming the comment was meant to fix. It now uses the shared string-aware scan the Zed half took, so both editors and both languages answer this the same way, and the test carries a block comment and a trailing one rather than only the form the old pattern happened to catch.
… the run `Set-SetupAgent` treated only a `setup-handled` throw as one profile's failure and re-threw anything else, so a backup that could not be written — a read-only directory, a locked file, a serializer that refused — killed the whole run with a raw .NET message and no summary. Reproduced: with an unwritable default profile beside a writable named one, Bash reported the backup failure and still configured the second profile; PowerShell configured neither. That contradicts the comment directly above it, and the Bash half has never behaved that way — there, every write failure is a counted profile. Any exception is now this profile's failure, named where Stop-Setup has not already named it, which is what makes the contract structural rather than a list of anticipated throws. The backup failure also says what it was, matching Bash's wording. A test asserts both halves keep going, and gating on 'setup-handled' again fails it. Also aligned two smaller divergences: the Bash half now prints the user directory it is configuring, as both Zed halves do, and the two refusal messages for the same document read the same. The JSONC comments in both VS Code halves stated the wrong reason. VS Code rewrites this file whole through `model.setValue(JSON.stringify(…))` on its own next edit, so a comment here — unlike one in Zed's document, which Zed never writes — is not the operator's to keep. The refusal exists because jq has no JSONC mode and the two halves must give one answer, not because anything is being preserved.
The previous round moved the per-profile write inside a transaction but left the enumeration outside it: `Get-SetupVSCodeProfileDirs` ran in the `foreach` header, so an unreadable `profiles/` threw past both loops. Reproduced — Bash configured the default profile and carried on, PowerShell wrote nothing at all and printed a raw access-denied, which for a stale root-owned `profiles` under Code also skipped Insiders and VSCodium. The enumeration now reports and returns the default profile, which is what the Bash glob does when it cannot read that directory: the named profiles are lost, not the build. Three `Test-Path` calls accepted a file where Bash asks `[ -d ]`, so a `User` path or a `profiles` entry that is a regular file was walked into rather than skipped. `zed.ps1` already asked for a container. Three instrumentation gaps behind them. The harness's Windows-replacement rewrite had reverted to a `.replace` chain covering only Claude and Codex when this branch was rebuilt, so neither editor's `File.Replace` ran off-Windows — replacing that line with a throw left the suite green. It is keyed per agent again and throws when a guard stops matching. `PowerShell writes the same provider group as Bash` never ran Bash; it now runs both halves and diffs the documents, as its Zed twin already did. And a comment paragraph was separated from the test it explains for the second round running, this time by the VS Code catalog test. Two smaller ones: the gateway's `listModels` returned an empty catalog when the key or its owner vanished mid-request, reporting "advertises no chat models" for what is a deleted key — it throws now, so the listing-failure script says something went wrong. And the README still listed three agents.
# Conflicts: # packages/agent-setup/scripts/test-installers.ts
`contextWindow` is the whole window and `maxInputTokens` the prompt budget — two numbers VS Code reconciles itself, deriving the second from the first when it is absent. The projection filled the window from whichever limit it found and never emitted the prompt one, so a model stating both had its prompt budget computed as window minus output instead of taken from what it announced. Both are emitted now, and a model announcing only one keeps what it announced. Two divergences from the Bash half, and a shared cause behind them: `-ceq` against an array is a filter, and a non-empty result is truthy, so a group whose `vendor` is `["customendpoint"]` matched ours and was deleted. jq keeps it, because a non-string is not equal to a string. The same question was asked a second time when validating the staged document — with the type check missing there too, which is how a first repair of the filter alone turned a silent deletion into a staging failure. It is one predicate now, asked in both places. A `profiles/` directory the run cannot enter yields nothing from the Bash glob, which read as "no named profiles" and reported success; PowerShell already warned. Both say so now. Instrumentation: the VS Code PowerShell script route had no test at all, the lease test named for configuring "every agent" stopped at Zed, and the directory-vs-file test asserted only a non-zero exit — which the broken version also produces, via a raw .NET exception instead of the reason Bash gives.
… to prompt VS Code reserves the output budget out of the window and gives the prompt whatever remains, so an output fallback larger than the window leaves a prompt budget of zero — the model shows up in the picker and every request is over budget before it starts. Ollama states a context length and no output limit at all, so an 8k model hit exactly that: `contextWindow 8192, maxOutputTokens 8192` resolved to `maxInputTokens: 0`. The fallback is bounded by a quarter of the window now; a stated 0 still survives verbatim, since only the fallback arm changed. Zed does not degenerate on the same row, so this was VS Code-only. The `maxInputTokens` emission added last round was observed by nothing — deleting it left the package, installer and web suites all green, because no fixture stated both a window and a prompt limit and the projection had no VS Code tests at all. There is now a VS Code block mirroring the Zed one, written against what VS Code computes rather than what we emit, plus a catalog row stating all three limits so the installer round-trip carries them. Removing either fix fails these.
# Conflicts: # apps/web/src/components/api-keys/agent-setup-models.ts # packages/agent-setup/scripts/test-installers.ts
The shared `[1m]` reconstruction reached only the Zed projection: the edit that was meant to apply it to `projectVSCodeModels` was written on a branch where that function does not exist, so it silently matched nothing — the same "rewrite that quietly stops matching" this harness now guards against elsewhere. Verified by projecting a merged 1M row and reading the id back. VS Code emits the addressed id now, with a unit test on both projections and the installer expectations updated to the id a run actually writes. Two comments stated capabilities the pinned upstream contradicts. The `messages` default was justified as "the only one carrying thinking budgets", but the Messages path builds a thinking config only from capabilities `customendpoint` never forwards, so no apiType can carry one — the real reason is the experiment flag, which is now what it says. And `maxOutputTokens` was described as merely clamped to the window; on the Messages path it is also sent as the wire `max_tokens`, making it a hard output cap.
# Conflicts: # apps/web/src/components/api-keys/agent-setup-models.ts # packages/agent-setup/scripts/test-installers.ts
The revert reached the Zed half through the merge but left `projectVSCodeModels` calling a function that no longer exists, so the package suite went red on the VS Code block. The id it sends is the id the gateway can resolve, and its unit test now says so rather than asserting the suffix.
A mutation sweep over `models.ts` found four guards no test could see: the Zed context-window fallback and its `max_output_tokens` presence check, VS Code's `maxInputTokens` presence check, and `toolCalling`. Swapping `??` for `||`, `!== undefined` for truthiness, or `true` for `false` left the whole package suite green — including two guards added specifically by the rounds that introduced them. The stated-zero test could not fail on its own subject either: it paired a zero output limit with a zero window, so the fallback arm produced 0 as well. Its window is non-zero now. Each is written against the property rather than a value — a stated 0 and an absent limit must reach the editor differently — and each of the four mutations now goes red.
# Conflicts: # packages/agent-setup/src/script-assets.generated.ts
# Conflicts: # packages/agent-setup/scripts/test-installers.ts # packages/agent-setup/src/script-assets.generated.ts
…replacing it The VS Code half carried the same defect the Zed half just lost: chezmoi and stow place a symlink where the editor expects `chatLanguageModels .json`, and renaming the staged document onto that path replaced the link, so the operator's dotfile stopped being what VS Code reads. Resolve the managed path through the shared helper, as Zed now does. Restricting the file is now stated once per half, before the key is written rather than after. The PowerShell half re-restricted the renamed document, which cannot fail on its own — the rename carries the stage's mode — and only served to hide a stage that had not been restricted; the Bash half restricted the stage after jq had already written the key into it, leaving a window the umask, not this code, was closing. Verified by mutation: removing the path resolution on either half, or the stage's restriction on the PowerShell half, turns the suite red. The Bash half's `chmod 600` stays unobservable because `umask 077` produces the same mode independently — two guarantees of one property, which the comment now says rather than implying test coverage it cannot have.
# Conflicts: # packages/agent-setup/__tests__/routes_test.ts # packages/agent-setup/src/script-assets.generated.ts
`contextWindow` was filled from `max_prompt_tokens` whenever no window was stated, and VS Code derives the prompt budget as `contextWindow - maxOutputTokens` — so a catalog stating a prompt limit and no window reached the editor short by the whole reservation. The shape every Codex model has, 120k stated, resolves to 111 808. Reachable in production from provider-custom and from any Copilot row reporting a prompt limit without a window. This is the defect the Zed projection already fixed by adding the reservation back; the VS Code half kept the raw substitution, against this file's own note that the two fields are different numbers. The window and the reservation are now decided together, because VS Code reconciles them against each other and neither can be chosen alone. The quarter-window cap on the output fallback now applies to a stated window only. That cap exists because a reservation larger than the window leaves no room to prompt, which can only happen when both come out of one fixed total; where the window is rebuilt from a prompt limit the reservation sits on top of it and crowds out nothing, so scaling it down there would invent a ceiling the catalog never implied. Verified by mutation: dropping the reservation from the reconstructed window, or the cap from the stated-window branch, each turns the suite red.
… observed
Four guards shipped with nothing watching them:
- `-WarningAction Stop` on ConvertTo-Json. Past `-Depth` the serializer
emits what it could not reach as the literal "@{k=}" and only warns, so
a sibling gateway's deeply nested group would be flattened into that
string and renamed into place — someone else's provider destroyed
quietly. A 120-level foreign group now reaches it.
- The `name -is [string]` half of the own-group test. `-ceq` against an
array is true when any element matches, so without it PowerShell would
delete a group jq keeps; only the `vendor` half had a case.
- The backup's mode on the PowerShell half.
- The empty-catalog refusal on the PowerShell half, which was Bash-only.
The staged-list checks stay unobserved because nothing reachable fails
them: an empty catalog is refused long before, and every malformed
document is refused before the backup. They are assertions on the merge,
not gates on input, and both halves now say so rather than implying a
coverage they cannot have. The Bash backup is restricted explicitly to
match the PowerShell half, though `umask 077` reaches the same mode
independently and no test can separate the two.
`an unreadable profiles directory does not stop the other builds` had one
user directory, never checked the exit, and duplicated the warning test's
setup; its comment described a per-profile enclosure neither half has. It
is folded into the warning test, which now runs a blocked build ahead of
a healthy one and observes what the name claims. The test override takes
several directories to allow that — the real enumeration yields several,
so an override that could name only one could not stand in for it.
Also drops a comment asserting that an empty effort list makes a picker
in VS Code — it returns early on a zero-length list — and tells the
operator following the pasted-snippet path that the file holds the key
in plain text, which only the script path was restricting.
`-ceq` is case-sensitive but still culture-aware, so it reported a name carrying a soft hyphen, a zero-width space, or an NFD accent as equal to ours. jq compares code points and says otherwise — and the disagreement is one half deleting a `customendpoint` group the other half keeps. A name pasted with a stray invisible, or typed where the keyboard composes accents, is the way in; the staged assertion cannot catch it, because the foreign group is removed rather than duplicated. `[string]::Equals` with Ordinal answers what jq answers. The `-is [string]` tests stay: overload resolution converts a single-element array to its element's string form before comparing, so `["customendpoint"]` would match without them. Also fixes three tests that promised more than they observed. The empty-catalog case now asserts the early gate's own message — the staged assertion downstream refuses the same input and rolls back, so exit code and byte-identity alone could not tell which one fired, and only the early one names a cause. The unreadable-`profiles` case asserts the exit is still 0, which is what "costs that build alone" means. And the suffix-free case says why its row has a 1M window: that is what a suffix rule keys on, so it is what keeps one from coming back. Verified by mutation: reverting to `-ceq`, dropping either type test, or removing the early empty-catalog gate each turns the suite red.
# Conflicts: # packages/agent-setup/src/script-assets.generated.ts
…t too VS Code parses this document with `allowTrailingComma`, so a comma before a closing bracket is syntax the editor accepts — and jq refuses it while ConvertFrom-Json takes it and writes the file back without it. One operator blocked, another silently rewritten, from the same document. The comment gate already existed here for exactly this reason; the comma case reaches it through the shared scanner now, and the refusal names JSONC syntax rather than comments alone. Verified by mutation: disabling the gate on either half turns the suite red.
# Conflicts: # packages/agent-setup/src/script-assets.generated.ts
# Conflicts: # packages/agent-setup/src/script-assets.generated.ts
… versions
Same root cause as the Zed half, in three places: the embedded model
list, the operator's existing list, and the staged document were each
read with `@(… | ConvertFrom-Json)`, which is a flat array on pwsh 7 and
a one-element array holding the real one on Windows PowerShell 5.1.
ConvertTo-Json then writes the inner collection as
`{"value":[…],"Count":n}`, so VS Code reads an object where its schema
requires a list and the whole group fails to load.
The staged check compares the model count instead of asking whether the
list is non-empty, for the same reason it does on the Zed half: a nested
list has a count of 1 and passed an emptiness check, which is how this
could reach a written document.
`vscodeTokenPlan` bounded the reservation only where the catalog stated none. A stated one went out whole, and VS Code gives the prompt whatever the window leaves after it — so a row stating `max_output_tokens: 128_000` and no window took our 128k fallback as the window, reserved all of it, and registered with a prompt budget of zero. Present in the picker, over budget before the first request. Every limit is independently optional from an upstream catalog and from operator model config, so a row stating only an output limit is ordinary. This is the failure the quarter-window cap was added to remove, closed for one arm of the same function. A stated reservation is now kept to half the window: past that the prompt gets the smaller share of a budget it is supposed to dominate, and a model that truly answers with 128k tokens would come with a window that says so rather than with our fallback. The prompt-limit branch keeps neither bound — there the reservation sits on top of the window and crowds out nothing. The small-context test now walks every projected row asserting a non-zero prompt budget, rather than checking the two Ollama shapes alone. Separately, the empty-list shortcut no longer spells whitespace as `\s`, which in .NET matches U+00A0 and the rest of Unicode's. VS Code's own scanner does treat those as trivia — so `[ ]` really is an empty list to the editor and to ConvertFrom-Json while jq refuses it, and the halves answer differently. Both outcomes are safe and closing the gap would mean rewriting the operator's whitespace before jq sees it; the comment says so rather than implying the shortcut settles it.
# Conflicts: # packages/agent-setup/installers/powershell/common/json-document.ps1 # packages/agent-setup/src/script-assets.generated.ts
`ConvertTo-Json -InputObject @(…)` keeps the brackets on a one-element array on both Windows PowerShell 5.1 and pwsh 7 — it is the pipeline form that unwraps a lone object. The hand-restored brackets after it could never run, and the test named for the round-trip could not have failed on either version. The serialization goes through `-InputObject` for exactly this reason, and the comment now says that rather than describing a 5.1 unwrap that does not happen there. `-WarningAction Stop` promotes nothing on 5.1, which emits no warning for an over-deep object at all; what refuses such a document there is ConvertFrom-Json's own recursion limit, before the merge runs. jq has no ceiling, so the Bash half keeps a deeply nested foreign group whole. The halves differ in how deep one may be, not in whether it survives, and the comment says so. The staged check's comment no longer claims the Bash half asserts the same two properties: it asserts the same shape, and the count is exact here because a list nested one level deep is a PowerShell failure mode jq has no equivalent of. The sentinel comparisons use the ordinal helper the group identity already moved to, rather than the culture-aware `-ceq`/`-cne` beside it. The owner-only test runs through both halves, which is what observes the PowerShell half's explicit restriction — the Bash half's `umask 077` reaches that mode on its own and no assertion there can separate them.
…d its prompt branch `vscode.ps1` was the one half `Get-Content -Raw` was never removed from, so on a stock Windows box it decoded the operator's provider list through the system ANSI code page and wrote it back as UTF-8 — permanent mojibake in a sibling gateway's group name or any localized string. Worse at the staged read: group identity is compared ordinally, so a non-ASCII provider name would not match itself after that decode and every profile would fail validation. Both sites go through the shared reader now, and the JSONC gate became the shared verdict, so a single-quoted string or an unquoted key is refused here rather than accepted by Newtonsoft and written back canonicalized. `vscodeTokenPlan`'s prompt branch left the reservation unbounded on the strength of it sitting on top of the window — true only when the catalog states no window. When all three are stated the reservation comes out of the stated window, and an output limit at or over it left VS Code with a prompt budget of zero. It is bounded against that window now, exactly as the branch below it already was. Also drops the empty-list shortcut, which was dead once the array reader started telling `[]` from `[null]` — measured 0 and 1 on both versions — and corrects the staged assertion's message, which reported an empty list for what is a count mismatch. The claim that every Codex model states a prompt limit and no window is false: Codex states a window. That shape comes from an OpenAI-compatible upstream or an operator override.
`ConvertFrom-Json` on PowerShell 7 flattens a one-element nested array
during the decode, so `[[{…}]]` arrived as `[{…}]`, passed the
element-type check, and the run rewrote the operator's document with the
nesting silently gone — where Windows PowerShell 5.1 and jq both refuse
it. `-NoEnumerate`, which exists on 6+, makes the two versions return the
same structure for every shape measured: a nested array, an empty-array
element, a two-object array, a one-object array, `[null]`, `[]`,
`[[null]]` and `[1,2]`.
The element check is indexed rather than piped as well. That is not what
fixes it — the reader is — but asking the same question without a second
dependence on how a pipeline treats a collection element is what keeps
the next edit from reintroducing it.
Both projections now read their limits through one filter. A zero,
negative or fractional limit is no bound at either editor's wire: VS
Code's fields are required numbers too, a zero window registers a model
that can never be prompted, and on the Messages path the output limit is
sent as the wire `max_tokens`, which the upstream rejects at zero. Two
tests asserted the old behavior and now assert what the editor can act
on. The prompt branch's unstated-output arm is bounded against the window
when the catalog states one, which is where the reservation comes from.
The refusal table carries the sentence each case must produce, rather
than accepting either — the point of naming a cause is lost if any name
will do.
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.
Adds VS Code as a fourth Agent Setup target.
Why
customendpointVS Code reaches Floway through the bundled Copilot extension's
customendpointvendor — the only non-deprecated arbitrary-URL provider that ships in stable.
customoaiis deprecated and carrieswhen: productQualityType != 'stable',so it is invisible in a release build;
ollamais deprecated in favour of amarketplace extension.
Its group takes an API path —
chat-completions,responses, ormessages—and Floway serves all three for every model, so that is one group-wide
preference rather than a per-model derivation.
Why models are enumerated, not discovered
customendpointreads onlyidoff a/modelsresponse and drops every modelit cannot type: it passes no known-models table and overrides no capability
resolver, so the base class's
if (!modelCapabilities) { continue; }discardsall of them.
Worse, setting a group-level
urlis actively harmful — it short-circuits intothat discovery branch and suppresses the explicit
models[]list, leaving theprovider empty. The installer never writes one.
Why the key rides in
requestHeadersThe group's own
apiKeyproperty is declaredsecret, so VS Code runs its${input:...}decoder over whatever it finds there. A literal has no:, sodecodeSecretKeyslices it into a secret-storage miss and the provider sends anempty
Authorizationheader.requestHeaderspasses through verbatim, and thisvendor deliberately un-reserves
authorization"for endpoints behind APIM,gateways, vanity domains".
The trade is that the document carries the credential rather than the keychain,
so it is written owner-only.
Shape
Every installed build (Code, Insiders, VSCodium) and every profile within it is
configured in one pass — the active profile is not discoverable from outside the
editor, and a named profile's directory is an opaque hash rather than its display
name.
Two shell notes. The Bash loop reads its profile list from a file rather than a
pipeline, since a
whilereading from a pipe runs in a subshell where a failurecould neither stop the loop nor reach the caller. The PowerShell writer restores
the array brackets by hand when
ConvertTo-Jsonunwraps a lone group, because-AsArraydoes not exist on the Windows PowerShell 5.1 baseline.Unlike Zed, no JSONC problem arises: VS Code rewrites this whole file itself
whenever Manage Models changes anything, so comments in it are already volatile.
Persisted configuration
The strict schema parses stored rows as well as request bodies, so adding a
required
vscodekey needs migration0079to backfill it — without one, thenext acquire 500s for every existing owner, and permanently: the parse happens
before any replacement row can be written and the latest-record lookup ignores
expiry. This class has now occurred twice, so a guard seeds the configuration
shape
0060stored when it created the table, runs every migration over it, andparses the result. Deleting
0079fails it namingvscode; deleting0078fails it naming
zed.Verification
134 passed, 0 failedin the real installer harness, seventeen of them VS Code.Several run through PowerShell and assert the same group as Bash — the jq
program and the PowerShell projection are two implementations of one mapping.
Writing them caught two defects: an override path that skipped the existence
check and crashed against a missing directory instead of reporting it, and a
catalog fixture that never exercised discrete effort levels.
Review then caught three more that would have broken real installs, all in the
PowerShell half and all confirmed by reverting the fix and watching a test fail:
File.Replacewas passed$null, which PowerShell binds asString.Emptyandthe API rejects, so every Windows re-run failed; the root shape was read from
the decoded value, which cannot tell
[]from an object; and limits usedPowerShell truthiness, so a catalog value of
0silently became a fallback.The first of those shipped unexecuted because the atomic-replacement branch is
Windows-only and no test dropped its platform conjunct. That rewrite is now
keyed per agent and throws when a guard stops matching, and both editors have a
test through it.
Full repo: 5489 tests, typecheck, and lint all green.
Notes
carries a Microsoft-branded one, and the tab configures all three builds, so
the unbranded shape is the more accurate label as well as the licensed one.