feat(agent-setup): configure Zed and VS Code - #437
Draft
Menci wants to merge 151 commits into
Draft
Conversation
Zed reaches Floway through its `anthropic_compatible` provider rather than `openai_compatible`. That provider gets the full JSON Schema for tool definitions where the OpenAI one is pinned to a lossy OpenAPI subset that rejects `if`/`then`/`$ref` outright and silently drops `format` and `additionalProperties`; it maps Anthropic errors into typed retry semantics instead of flattening them at the transport boundary; its SSE reader tolerates a stray `[DONE]` sentinel that gateways emit; and it reports cache token counts, which the Chat Completions mapper hardcodes to zero. That last one matters because Zed has no token counting at all, so streamed usage is its only source of truth for the context meter and auto-compaction. Both installers write `global_settings.json`, a settings layer Zed reads below the user's own file and never creates or writes itself. Owning that file outright is what keeps this on plain JSON: the user's settings.json is JSONC, and no portable tool edits it safely — jq and its kin cannot parse comments, and PowerShell's ConvertFrom-Json errors on 5.1 while 7.0-7.5 silently turn a comment inside an array into a string element. The merge touches only the one provider key, matching the Claude and Codex installers. Zed is configured but never installed: it ships outside any package manager these scripts drive, so a missing configuration directory is a hard stop rather than a directory to create. Model entries are snapshotted from `/v1/models` because `anthropic_compatible` has no discovery path, and are selected by `kind` rather than by `endpoints` — the endpoint map is the upstream wire surface, and translation lets any chat model serve a Messages request. The credential is written to the OS store ahead of the settings document. Without a key `is_authenticated()` is false and every model disappears from the picker with no error shown, so the failure mode to avoid is a registered provider with no credential; an unreferenced credential is harmless by comparison. Refs: zed-industries/zed#30444 PowerShell/PowerShell#14553
Adds the Zed tab to Agent Setup: a provider-name field, the setup command, and a config snippet pair for operators who configure by hand. Zed takes only a name because everything else is derived. The catalog projection lives beside the Claude and Codex model helpers and mirrors the installer's jq program — both write the same `available_models` array, and the suite pins the parts that would silently break Zed: models are selected by `kind` rather than by `endpoints`, all three capability flags are always written because Zed reads no per-field default and a partial object fails the whole provider, and reasoning maps onto adaptive, budgeted thinking, or no mode at all. The snippet is a whole `global_settings.json` rather than a fragment to merge, since Zed owns nothing in that file. Its credential half is a separate block because Zed reads the key from the OS credential store, which has no settings representation: macOS and Secret Service take a one-liner, Windows needs a CredWriteW P/Invoke because the blob must be UTF-8 and cmdkey writes UTF-16LE. lobe-icons carries no Zed mark, so the tab icon comes from simple-icons under CC0, normalized to the sizing and currentColor attributes the other marks use.
Ten cases run both installers for real: the catalog projection, the managed-key merge against a document holding a sibling provider, the credential record, a provider name carrying a quote, and the three refusal paths — absent configuration directory, unparseable settings, and a catalog with no chat models. One case runs the same scenario through PowerShell and asserts the same provider document, which is the guard that matters most here: the jq program and the PowerShell projection are two implementations of one mapping, and nothing else would catch them drifting. Both fragments gain a credential hook, since neither a keychain nor a Secret Service is something a test host can be asked to mutate. The harness serves `/v1/models` with one model per branch of the projection — adaptive reasoning with images, a budget ceiling, no limits at all, and a non-chat kind that must be dropped. Collapse the three per-agent configuration builders onto a shared base so the fourth does not repeat the whole schema a fourth time.
Four defects, each of which would have shipped a broken or unsafe setup. **A thinking mode with no budget.** Zed serializes `Thinking::Enabled.budget_tokens` with no skip_serializing_if, unlike the `Adaptive` variant beside it, so a mode carrying no budget puts `"budget_tokens": null` on every Messages request and Anthropic rejects it. The projection emitted exactly that for any model stating reasoning without a ceiling — every Codex model (effort levels only) and every Claude Code model (a floor, no ceiling). Such a model now stays in Default mode, which the picker still offers, and where a budget exists the floor is preferred: Zed sends it verbatim on every request and Anthropic requires it below max_tokens. **XDG honored on macOS.** Zed consults `XDG_CONFIG_HOME` on Linux and FreeBSD only; macOS falls through to an unconditional `~/.config`. An operator exporting XDG on macOS — routine for shared dotfiles — had the file written where Zed never reads it, and the run reported success. FLATPAK_XDG_CONFIG_HOME is honored on Linux, matching upstream's order. **A hardcoded app bundle.** `security -T` fails the entire call when the path does not exist, so anyone running Zed Preview, Nightly, or a `~/Applications` install had setup abort with the reason discarded by a stderr redirect. Only bundles present on the host are named now, and naming none still writes the item. **A test hook that exfiltrated the key.** `AGENT_SETUP_TEST_CREDENTIAL_ RECORD` wrote the live credential in cleartext to an env-var-chosen path, replacing the hardened store. Every other hook in the family adjusts a timeout, a colour, or where code is fetched from, and the two that hand control to a chosen path strip `SETUP_API_KEY` first. Since these scripts are piped into a shell that inherits the caller's environment, one `.envrc` or profile line was enough. It is gone; the harness shims `secret-tool` and `security` on PATH instead, which also brings their real argument vectors under test for the first time. Also from the same pass: the catalog request carries its credential in a curl config file rather than argv; model projection moved ahead of both the credential write and the backup, so no failure can strand an orphan backup or an unreferenced keychain entry; PowerShell uses File.Replace on Windows rather than a delete-then-create Move-Item, guards its Add-Type against a second run in the same console, validates through the property bag so a provider named `Count` resolves, compares against $null so a catalog 0 survives, and surfaces the underlying fetch error. The `secret-tool clear` call and its rationale were both wrong — libsecret replaces a matching item — so both are gone, and the config-dir override is renamed to the `AGENT_SETUP_TEST_` convention.
The dashboard fetches `/api/models?include_unlisted=true` to populate the alias combobox, and nothing downstream dropped those rows. The installer snapshots `/v1/models`, which by definition excludes them, so an operator with `modelPrefix.addressable` wider than `listed` got a snippet naming both ids — visually identical in Zed's picker, since the unlisted row copies its display name — while the setup script wrote only one. The projection now drops them, and the id dedupe it carried is gone: the catalog collapses on a Map keyed by public id, so duplicates cannot arrive, and the guard only masked the divergence from the jq program, which never had one. Reasoning maps the way the installers now map it — a budget or no mode at all, floor preferred over ceiling — so the two implementations write the same document again. An empty projection no longer renders a copyable document. The installer refuses that catalog rather than register a provider with no models, and the panel offering one anyway would have handed the operator a config that fails silently. It shows the same refusal instead. The config hint said "Merge into", but the snippet is a whole document whose paste would drop any other provider in the file; it now says to save it and merge by hand when one exists. The credential snippet drops the `secret-tool clear` that the installer also dropped.
**The curl config file corrupted the key.** Moving the credential off argv looked like a hardening, but curl's quoted config value parses `\` escapes and terminates at `"`, and the unquoted form is discarded for containing whitespace. Verified against a listener: a key holding a quote arrives truncated, and one holding a backslash arrives with the escape expanded — the gateway 401s and the operator is told only that the catalog fetch failed. An API key is an arbitrary string, so this is reachable. Reverted to `-H`, which passes the key byte for byte, with the tradeoff stated where it is made. **PowerShell stored the key with a trailing newline.** `$key | & secret- tool` terminates the piped object with a newline and secret-tool stores every byte it reads, so Zed read back `sk-…\n` and sent a malformed Authorization header on every request — a 401 loop behind a settings file that looks correct, and a divergence from Bash on the same host. Now written through a redirected stdin. The shim could not have caught the second one: command substitution eats a trailing newline, so `key` and `key\n` recorded identically. It records the byte count instead, and the PowerShell test asserts it — the Bash test could not, because it takes the `security` branch on macOS while CI takes `secret-tool` on Linux. Confirmed by reintroducing the pipeline and watching the test fail. Also: the dashboard's macOS credential snippet still named a hardcoded `/Applications/Zed.app`, which fails the whole call on a Preview, Nightly, or `~/Applications` install — the installers were fixed for this in the previous commit but the pasted form was not. `max_output_tokens` moves after `capabilities` so the web projection matches the installers byte for byte rather than only semantically, and `== null` there drops an explicit JSON null the way both installers already do. `--agent` accepts every registered agent instead of the original two. A ceiling-only catalog row covers the budget fallback the installer harness never exercised. PowerShell reports its own message when a backup cannot be made, and an unverified claim about PSObject intrinsics is removed rather than left asserting something I did not confirm.
…licitly The redirected-stdin write leaked a Process handle on every run. Only stdin is redirected, so there is no output pipe to deadlock on, but the handle still needs releasing. The stdin assertion compared against a JS string length, which equals the UTF-8 byte count only because the sentinel is ASCII. It now says so and measures bytes, so a non-ASCII sentinel would not quietly turn the check into a tautology.
The strict schema parses stored rows as well as request bodies, so a configuration written before this branch — one with no `zed` key — throws on the next acquire. It throws permanently: `latestByUserId` has no `expires_at` predicate, so the stale row stays latest, and the parse happens in `restorableConfiguration` before the insert that would replace it, so the sweep trigger never fires and retrying re-enters the same 500. Verified by parsing a pre-branch blob against the current schema, then by running the migration against SQLite and re-parsing: the legacy row backfills and parses, an already-migrated row keeps its own name, and unrelated fields survive. Same shape as 0061 and 0068, which backfilled for the same reason. The pasted macOS credential snippet now runs its Darwin arm in a subshell. Unlike the installer's function, where `set --` is scoped, a pasted script sets positional parameters in the operator's own shell — leaving the API key in `$@` after the paste, where a later `echo "$@"` would surface it. The credential shim records the secret as hex rather than a byte count. The count proved a trailing newline was absent but would have accepted any same-length content; confirmed by substituting an equal-length wrong secret and watching the hex assertion fail where the count had passed.
The shim piped stdin through `od` and `tr`, neither of which is in the harness's hermetic tool list — the installer PATH is exactly binDir plus SHIM_BIN, with no system directories. Both stages die with "command not found", the substitution yields empty, and `case` swallows the status, so the shim still exits 0 and the assertion compares '' against the key. That is red on CI, which runs Linux and therefore takes the secret-tool branch; my macOS runs take `security` and never reach it. Reproduced under `env -i PATH=…` before fixing, and confirmed afterwards by forcing the secret-tool branch on this host. The secret now goes to its own file via `cat`, the one byte-preserving tool already on that PATH. Still byte-exact, still fails on the trailing newline a piping shell would add, and no longer licenses the installers to reach for tools the harness does not provide. PowerShell 6+ routes -Headers through HttpClient, which parses Authorization as a typed header and rejects a parameter containing `,` or `"` before the request leaves the host — verified: a key with a comma throws "The format of value is invalid" with no request sent, and succeeds with -SkipHeaderValidation. 5.1 has neither the validation nor the switch. The Bash installer already passes the same key to curl verbatim and says so in a comment; this makes the two agree. The projection is also handed to jq through --slurpfile rather than --argjson. It is already a file, and a single argument is capped at 128 KiB, which a few hundred chat models would reach.
It is the one free-text field in Agent Setup, so the only one whose draft the gateway can reject — every other control is a dropdown over a closed set. Typing a name and pausing after a space PUT a padded value, and a 400 is not retryable: the save was abandoned, the copy button stayed disabled, and the untranslated Zod issue landed in the page-level message bar with nothing pointing at the field. The name is now held locally while invalid and only patched into the draft once it is acceptable, with the reason shown on the field itself — the same shape as `d2cd73ab3`, which moved model validation to its fields. Confirmed load-bearing by removing the predicate and watching the test fail. The length bound is restated rather than imported, since `apps/web` may not runtime-import the gateway package; it only stops typing early, and the gateway still enforces the rule.
The local hold that keeps an invalid name out of the draft outlived the configuration it was typed against: once set it shadowed the incoming value permanently, so a lease arriving for another key rendered behind a half-typed name from the previous one. That is the invariant the two existing card tests protect — the form shows the lease and nothing else. It is now keyed on the configuration's api key id, so the hold applies only while that configuration is the one on screen. Confirmed by writing the test first, watching it fail, and re-running it against the unkeyed form afterwards. My first attempt at that test asserted the field reverts the moment another key is picked. It does not, and should not: the card deliberately keeps showing the current configuration until a lease answers, which the neighbouring test asserts. The test now stubs the arriving lease, which is the point at which the hold must yield. Also renames a lease-projection test that still said "both agents"; the same phrasing was corrected in its sibling when Zed landed.
The dashboard's Windows credential snippet emitted a bare Add-Type under the type name ZedCred, while the installer defines the same writer as FlowayZedCredential behind an existence guard. Two surfaces writing the same credential now agree on both. Add-Type returns its cached type for a byte-identical re-add and rejects a differing source under a name already in the AppDomain, so the guard makes a second paste — after rotating the key or renaming the provider — a no-op outright instead of resting on that cache. The emitted snippet was parsed with the PowerShell parser and run twice in one session to confirm the here-string survives the enclosing block. The installers' /v1/models fixture served the catalog to any caller, so removing the Authorization header from either installer left the suite green while every real install would have failed. It now answers 401 without the sentinel key; removing the header fails four Bash cases and the PowerShell parity case.
The comment asserted how VS Code keys its own provider group, which nothing on this branch implements and no reviewer here can check against code. The constraint it documents — an opaque label neither editor derives behavior from — is stated from the one consumer that exists.
The installer fetched /v1/models and projected it into Zed's available_models itself, which meant the same mapping existed three times: a jq program, a PowerShell loop, and the dashboard's preview builder. They had to agree byte for byte and repeatedly did not — a stated limit of 0 became a fallback under PowerShell truthiness, an empty effort list was dropped, and key order drifted. Each divergence was found by a test written specifically to compare the three. The gateway now projects the catalog once and embeds the result in the script it serves. The installers keep only the merge: write the embedded list to a file and hand it to jq, or decode it and hand it to the document writer. Nothing compares three implementations any more because there is one, in packages/agent-setup/src/models.ts, which the dashboard imports through a new /models subpath export so its preview is what a run will write. Two values stay client-side. The endpoint URL, because the gateway does not render its own public origin — the dashboard injects it into the executing shell. And the API key, because it already appears once in the script and copying it into every model entry would multiply the credential for no gain. Listing now happens while serving the script, so it can fail there. A listing failure renders a script that says so and exits non-zero: an opaque 404 would read as a dead setup link and a 500 as a gateway fault, and the upstream detail stays in the operator's log. The upstream scope is resolved through the same intersect rule the data plane uses, extracted so a setup script cannot advertise a model the key cannot reach. zed.sh drops from 307 to 248 lines and zed.ps1 from 344 to 278.
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.
`budget_tokens.min` is a lower bound an operator may legitimately record as 0, meaning "no lower bound stated", and the projection preferred the floor unconditionally. Zed sends the budget verbatim on every Messages request and Anthropic rejects anything under 1024, so such a model 400'd on every call with nothing in Zed's UI to explain it — and a stated ceiling that would have worked was discarded. A floor too small to use now falls through to the ceiling, and a model with no usable budget stays in Default mode, which the picker still offers. This is the same class as the zero-limits case: a stated 0 is a value, not an absent bound. Two failures around the new gateway seam: The PowerShell failure script ended in `exit 1`. Its documented invocation is `irm … | iex` in the operator's own console, which `exit` closes — taking the message the script exists to show with it. Reproduced; it now sets $global:LASTEXITCODE and returns, matching what the installers already do. The listing failure told the operator to check the gateway log, while the diagnostics helper deliberately drops the error message because a generic failure may carry a secret. Both secrets are in hand at that call site, so the reason is redacted and kept rather than discarded. And a transaction boundary: the PowerShell document mutation sat outside the try that owns rollback. A provider name PowerShell reserves on every object — PSObject, PSBase, PSTypeNames, all accepted by the schema because Zed treats the key as opaque text — makes Add-Member throw there and leaves the backup beside the operator's settings forever. Moving it inside restores the "no orphan" property the comment claimed; a test asserts the directory is clean after such a run, and fails when the block moves back out.
`PowerShell writes the same provider document as Bash` never read the Bash document — it re-asserted a hand-maintained copy of that test's expectations, which is the duplicated-expectations mechanism the server-side projection was built to remove: a restated expectation can drift from both implementations at once. It now runs both halves over the same catalog and prior document and compares the two results, with one anchored assertion so the comparison cannot pass by both halves being wrong the same way. Making the PowerShell half drop a model fails it. A single-model catalog had no coverage in either half, though it is where ConvertTo-Json unwraps a one-element array into an object — which fails Zed's Vec deserialization and takes the whole provider down — and where jq's --slurpfile/$models[0] is pinned. The catalog fixture gained the two rows whose projection branches nothing exercised: a model stating limits of 0, and an addressable-but-unlisted row that must not reach the settings document. The model server's /v1/models handler and its two catalog modes are gone. No installer requests that endpoint any more, so the handler was unreachable and its comment — that rejecting an unauthenticated request makes every success case assert the header — had become false.
# 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.
`umask 077` made every Zed settings write come back 0600, including a run that refused the document and reported leaving it untouched — the backup was created under the umask and moved back over the original. This file holds no credential (Zed reads the key from the keychain), so the operator's own mode is theirs to keep: the backup is copied with `-p` and the staged replacement inherits the mode of the document it replaces. A new file still takes the umask default. The pre-backup gate also passed on a truncated file, because jq runs a filter zero times on empty input and still exits 0. Such a file was backed up, staged, and only caught at the staged check — reporting a fault in our own list rather than naming the document the operator has to fix. `-e` answers that before any backup exists. The ESLint rule forbidding apps/web from runtime-importing @floway-dev/agent-setup matched only ImportDeclaration, so the branch's own `export … from` re-export of the /models subpath satisfied it by syntax rather than by intent — and a future re-export of the root would have too. It now matches exports as well and names /models as the one allowed surface, which is what makes the dashboard preview and the embedded projection the same code.
…alves Zed is the first agent to key a map by operator-chosen text, which reaches a case-insensitivity the shared PowerShell helper had never been exposed to: `-contains` matched `floway` for a chosen `Floway`, and the dotted assignment then wrote the new value under the OLD key. Bash added a second key instead. So one operator renaming only the case got a picker still showing the old name and an exit 0 on Windows, and a stale broken provider beside the new one elsewhere. The PowerShell property bag cannot hold both keys at once — adding `Floway` beside `floway` replaces it — so keeping both is not a behavior the two halves can share. Both now drop any key differing only by case before writing the chosen one, which is also the better outcome: a case-only rename stops leaving an entry whose stored credential no longer matches its name. ASCII case is what both fold, stated where they fold it. The Zed fragment does this itself rather than through Set-SetupProp, whose case-insensitive lookup is correct for the fixed key names Claude and Codex pass it.
Zed reads global_settings.json with serde_json_lenient, so a JSONC comment is
the operator's own content. jq refuses such a document and PowerShell 7 accepts
it and drops the comments on the way out — data loss reported as success, and a
third behavior on the 5.1 baseline, which errors. Both halves refuse it now. The
check walks strings rather than matching a pattern, so a `//` inside a URL or a
model id is not mistaken for a comment.
ConvertTo-Json emits a subtree deeper than its limit as the literal string
"@{k=}" with only a warning, and the staged check cannot see it because that
inspects the provider entry alone — an unrelated setting nested deeply enough
was replaced by a string under exit 0. The warning is promoted to an error.
# Conflicts: # packages/agent-setup/src/script-assets.generated.ts
The unknown-catalog guard sat above the Codex fall-through, so a failed model listing blanked the Codex snippet pane — a document built from the configuration alone, needing no catalog at all. It belongs inside the Zed branch, which is the only one that projects models. The case-variant guard covered `language_models` and not the key one level down. `anthropic_compatible` is reached by the same case-insensitive member access, so `Anthropic_Compatible` took the provider into a key Zed's deserializer never reads while the run reported success — and the staged check could not see it, because it reads back through that same access. And the overflow arm compared the exponent against 308, which misses every shape that overflows without one: `9e308` and `1.9e308` sit in the same decade as the limit, and a 310-digit integer has no exponent to compare. It measures the magnitude from the significant digits now, and decides the boundary decade on the leading four, because the double range ends at 1.797…e308 rather than at a power of ten. The scan enters at a token's first character only — every digit re-entered it before, and a suffix like `.7e308` measures larger than the number it came from.
The token-start guard treated `+` and `-` as interior characters, so the scan never entered a signed number: for `-1e400` the leading digit is preceded by the sign and every later digit by a digit or `e`, leaving the whole token invisible. jq rewrites it and both PowerShell hosts refuse it, so the Bash half configured a document the other stopped on — the split this arm exists to close, reintroduced by the rebuild that closed it for unsigned numbers. A sign is now transparent to the entry test. `1e-400` still stays out, because the digit after that sign is preceded by `e`, which is not.
… group VS Code stores the Thinking Effort an operator picks in the picker inside our own `customendpoint` group, keyed by model id, and reads it back on every resolve. Both halves rebuild that group from scratch, so a re-run — done to refresh the catalog or after rotating a key — reverted every choice to the schema default with nothing on screen. The asymmetry gave it away: a foreign gateway's `settings` survives, because its group is copied whole, and only ours was rebuilt without it. Carried across by exact name on both halves, in the position jq's object addition puts it, so the two still write the same document. A byte-order mark no longer reaches the scanner. It is not JSON content — jq reads a document carrying one and the PowerShell reader strips it — but some awks die on those bytes rather than classifying them, and the verdict travelled in the exit status, so a failure of the tool doing the examining was reported as a verdict on the operator's content. The verdict travels on stdout now and the status is left to mean "could not examine", which both installers name as such. The BOM strip and the tool-failure arm are unobserved: the macOS awk on this machine does not die on those bytes under any locale I could set, so the die is host-specific and the harness uses whichever awk the host has.
The overflow arm guarded one end. A literal below the smallest subnormal decodes to 0 on both PowerShell versions and is written back as `0.0`, silently changing a number inside an entry the run was not asked to touch, while jq preserves `1E-400` — the same harm the overflow arm exists to prevent, reached from underneath. Worse, the last round put `1e-400` into the document that asserts both halves *accept* a file unchanged, where its only value assertion was on a different key. It moves to the refusal table; `1e-320` is subnormal and representable, so it stays as the accepting case. A stated zero is unaffected, because its own token carries no significant digit.
The scanner blamed the operator's document for two failures that were not the document's. `awk` under a UTF-8 locale — the macOS default — dies with a multibyte conversion error on any non-ASCII byte outside a string, so a smart quote or a non-breaking space pasted out of a web page produced no verdict at all. The scan now runs under `LC_ALL=C`, which makes it byte-oriented and classifies those bytes the way the PowerShell half does. The BOM-stripping `sed` stage became redundant once the scan reads bytes and was removed. The two halves also disagreed on subnormal magnitudes: `1e-324` and a decimal-only tiny literal were accepted by Bash and refused by PowerShell. Both causes are fixed — the boundary decade at 1e-323, and the magnitude of a mantissa below one, which is the exponent less the leading zeros of the fraction rather than the length of an empty integer part. With the scan no longer dying on readable content, a missing verdict can only mean the scanner itself failed, so that arm names the scanner rather than the document. Tests cover the scan under both `C` and `en_US.UTF-8` — the harness builds the child environment from scratch, so a locale-sensitive behavior is unreachable unless a test asks for it — the underflow boundary on both sides, and a non-breaking space in the VS Code half.
…th halves Backing up a document was written out at nine sites across the two halves, and four of them left a partial copy behind when the copy or the mode restriction failed — a truncated file beside the operator's own, which a rollback would later hand back as their document. Both halves now reach one helper that removes the remains before the failure is reported, so a site cannot be written without the cleanup. `settings` inside our own VS Code group is the operator's whatever its value, and both halves read it by truth rather than by presence: jq's `//` dropped a stored `null` and `false`, and the PowerShell null test dropped `null`. Both ask for presence now. The PowerShell overflow refusal was inert on the only host that half runs on: Windows PowerShell 5.1 reports overflow from `[double]::TryParse` by returning False, not by parsing to Infinity — measured on 5.1.26100.8875, where `1e400` gives False and `1e-400` gives True with 0. `Resolve-SetupManagedPath` returns a canonical path for a plain file as well as for one behind a link, anchored on the session's working directory rather than the process directory PowerShell does not move. Also corrects three comments that described awk's exit status as the scanner's verdict — the verdict travels on stdout — and removes a stale paragraph in the Zed half that contradicted the one below it.
# 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 # packages/agent-setup/__tests__/routes_test.ts # packages/agent-setup/scripts/test-installers.ts # packages/agent-setup/src/script-assets.generated.ts
…le takes Four behaviors had no observer, so a change to any of them was silent. The API path an operator picks reached the projection only as the default, so a projection that hardcoded `messages` passed. It is now asserted against a path they chose. The user-directory derivation from `HOME` — the only path a real operator takes, since every other test names its directories through the override — never ran. Two builds are placed under `HOME` and both are configured. A file under `profiles/` is not a profile. Dropping the directory filter wrote the key to a path VS Code never reads; nothing said so. The profile loop's second arm, which names a failure this installer never predicted, was reached only through Stop-Setup — which reports itself. A profile directory that cannot be written now exercises it, and costs only that profile. `$keptSettings` is declared beside the list it belongs to rather than inside the branch that fills it: a document that does not exist leaves that branch unrun, and reading an unassigned variable as "absent" is a coincidence of PowerShell rather than a decision. Also corrects the token-plan summary, which claimed a bound the code does not apply to a reservation that comes out of no window, and names the dashboard's unknown-catalog suite for what it covers.
…pt for the C# A hand-merged `chatLanguageModels.json` can hold two groups under our name — which is what the snippet pane asks the operator to produce — and the two halves searched them differently. jq takes the first `settings` among them; PowerShell stopped at the first group under our name, so a document whose settings sat on the second one lost every Thinking Effort the operator had picked. Both halves now end the search where the settings are, and a fixture asserts they write the same document. The Zed credential writer was registered as a common section, so all four PowerShell scripts compiled a P/Invoke declaration into the operator's console and three of them opened with an unexplained block of C# — in a response that also carries their API key. The manifest now has a place for a fragment only one script needs, and the Zed script is byte-identical to before. Also corrects two comments: the VS Code verdict capture named `set -e`, which this installer never enables — the real reason is the one the Zed half gives, that the scanner answers with three statuses and `case` would clobber `$?` — and the VS Code migration pointed at the number an unrelated Ollama migration now holds.
…olves Both halves preserved the first group under our name that carried `settings`. VS Code resolves duplicates the other way: it walks the groups in array order and applies each matching group's settings into one map keyed by model, so the later group is the one whose Thinking Efforts were in effect. Keeping the first preserved the settings the editor was ignoring and dropped the ones the operator was using — and a hand-merged file, which is what the snippet pane asks them to produce, is exactly where two groups come from. The fixture now puts distinct settings on both duplicates, so a half that took the first can no longer pass by keeping the only one there was. The manifest's whole-file groups are typed as such: `standalone` and `agents` are embedded entire, and only `common` is tiled, so a cut point declared on either would have been accepted and silently ignored. Also corrects a comment served inside the Zed script, which still named the credential fragment's path from before it moved out of `common/`.
…not add beside it
The snippet pane's comment claimed a second group under one name
collapses onto the last, citing a change-detection map that decides
nothing about it. What VS Code actually does is split: a model is
identified as `${vendor}/${group}/${id}`, so two groups sharing a name
produce the same identifiers and the second registration is skipped with
a log line — its models, endpoint and key never take effect — while
per-model `settings` are read from every matching group in order, so the
later group's win.
An operator who already ran the installer and then followed the hint
("Merge this group into chatLanguageModels.json") appended a second
Floway group that did nothing, with the stale entry still in effect and
only a log line to say so. The hint now says to replace a group already
using the name, and the comment states both rules against the code that
implements each.
The installer's own merge is unaffected: it replaces every group under
its name with one.
Three references pointed at code that does not decide what the comment
beside them claims.
Both installer headers said groups are keyed by `${vendor}:${name}` so
ours replaces only its own, citing a map built to diff old configuration
against new for a change event. Nothing upstream keeps the list unique —
which the same files say fifteen lines down, where they handle two
groups under our name — so the sentence now states the rule as the
installer's own: it selects every group of our vendor and name and
replaces them with one.
The last-wins rule for `settings` cited the branch taken by vendors that
contribute no configuration schema. `customendpoint` contributes one
(extensions/copilot/package.json), so that branch never runs for the
vendor these files write; the collection that does run is the one in the
resolve path, which the Bash half already cited and the other two sites
did not.
The rule's other half — last among the groups that carry settings, not
last under our name — had no fixture, so a PowerShell loop that cleared
the kept value on a later group without settings would have dropped the
operator's choices with every test green.
…from Every Zed case named its directory through the test override, so the derivation an operator actually takes shipped unexecuted: the Darwin arm, the Flatpak and XDG arms, the `$HOME/.config/zed` fallback, and the PowerShell `%APPDATA%\Zed` arm. Each of them silently configures a directory Zed never reads if it is wrong, while the run still stores the key in the credential store — and the sibling VS Code installer had this closed for the same reason. Two cases cover it. The first runs both halves with no override and asserts the document lands under `~/.config/zed` and nowhere else, with `XDG_CONFIG_HOME` exported and ignored on macOS, honored elsewhere. The second answers the platform question as a Linux host would — a `uname` shim for the Bash half, and for PowerShell the same targeted rewrite the Windows-replacement guard uses, since its platform comes from an automatic variable — so the XDG branches and the fallback below them run on a macOS runner too. The child gets no `XDG_CONFIG_HOME` unless a test asks for one, for the same reason it gets no `LANG`.
…ay it does Zed does not read its two variables alike. `FLATPAK_XDG_CONFIG_HOME` it takes straight from the environment; `XDG_CONFIG_HOME` reaches it through `dirs::config_dir()`, which discards a relative value and falls back to `$HOME/.config`. Both halves honored a relative one, which configures a directory the editor never reads while the key still lands in the credential store. VS Code has no such filter and keeps its variable raw, so only the Zed half gained the gate. The VS Code half asked `$IsMacOS` directly rather than the one platform decider the rest of the installer uses — a second answer to the same question, and the one the harness cannot reach. Three derivation arms had no observer: Zed's Flatpak arm on both halves and VS Code's XDG base. All three now run, on a macOS runner too, through the platform-forcing the previous commit established. A failed backup is reported by the shared helper on the Bash side but was rethrown bare by the Claude and Codex PowerShell callers, so the operator read a raw .NET message naming Copy-Item rather than their own file. All four agents now give the sentence their Bash half gives, and both Codex documents are covered — the config is copied first, so a case that only has a config never reaches the token's own arm.
…ther copy The Codex config backup asked whether its failure was the already- reported sentinel, but only the mode restriction raises that, and this arm does not restrict a mode — the Zed backup, the same shape, correctly asks nothing. The guard stays where it can fire, with a line saying why. `renderScriptFailure` spelled the script-language union out by hand, so adding a language would have left it behind rather than failing to typecheck; it takes the exported alias its only caller already holds. The upstream-cap intersection is not stated once, as its comment claimed: the browser cannot import the gateway module, so the dashboard states it again in `components/models/reachability.ts` — and this branch made the two co-dependent by narrowing the Agent Setup preview through the browser copy while the served script is projected from the gateway's. Each copy now names the other, and the condition under which they should become one is written down where the decision would be made.
…onfigure The setup pane and the snippet pane each projected the catalog to decide whether there was anything to configure, and each rendered its own copy of the warning that says there is not — one rule with one sentence, written twice, so a change to either reached only one tab. Both now go through one projection and one component. Nothing observed the panes with a catalog that is not empty: every case rendered the empty one, which is the warning path, so a projection that answered with nothing for an editor would have blanked that tab with a green suite. Three cases render a chat model and assert each editor's document appears instead of the warning. Also corrects a test comment that credited the installer with an `apiKey` property. Neither half writes one — that property is VS Code's, and it is decoded as a secret-storage reference, which is why the key rides in `requestHeaders` on both sides and in the paste.
… its first digits The two halves disagreed over a real band of literals. The awk scanner compared four significant digits, so `1.7978e308` — which shares `1797` with the largest double and overflows — was called representable, and the accepted literal then reached jq, which rewrites it to `1.7976931348623157e+308`: a silent change to a number inside an entry the run was not asked to touch, which is what this arm exists to prevent. The same four digits refused `2.4705e-324` at the other end, which rounds to the smallest subnormal rather than to zero and which PowerShell keeps. Both boundaries are now compared as 17-digit strings — the mantissas do not fit in a double, which is the very thing being decided, and awk has nothing wider — against the values `[double]::TryParse` uses. Fixtures cover a literal on each side. `zed.ps1` restricted a new document's stage twice; the second call has been inert since the first was added for the mute-stat case, so nothing could observe it going wrong. The carry-over now reads as what it is: a step for a document that already existed. Also: a dashboard case named for offering the setup command observed only that the warning was absent, which a blank pane satisfies too, and three harness comments had drifted onto neighbouring declarations.
The previous round moved the comparison to the largest double, but the other half asks `[double]::TryParse`, which is correctly rounded: a literal overflows only past the halfway point beyond that value. So `1.7976931348623158e308` is finite and the Bash half refused a document the PowerShell half configures — the same class of split as before, in the other direction. The tie is decided by twelve digits past the seventeenth, which puts the split beyond any precision a settings file carries: `1.797693134862315808e308` overflows, everything below it does not. The underflow end gets the same treatment, where `2.4703282292062327e-324` rounds to zero and `2.470328229206232721e-324` does not. Thirteen literals either side of both boundaries were measured against real IEEE doubles and against `[double]::TryParse` on pwsh before and after; four of them are now fixtures. The PowerShell underflow guard asked whether the whole token held a non-zero digit, so `0e5` — a stated zero whose exponent carries the only one — was refused where the other half accepts it. The significand decides now, which is what its comment always claimed.
A 29-digit comparison still split the two halves at the digits past it, in both directions: `[double]::TryParse` reads every digit, so a literal sharing the prefix but running longer was ordered one way by awk and the other by PowerShell. Both boundaries are dyadic rationals — 2^1024 − 2^970 and 2^-1075 — so both decimal expansions terminate, and the scanner now carries them whole and orders a literal against them digit by digit. Equality refuses at both ends, which is where rounding takes the tie: to infinity above and to zero below. Fifteen literals across both boundaries were run through the real `_json_scan_verdict` and through `Get-SetupJsonVerdict` on pwsh 7; all fifteen now agree. Four are fixtures, including each turn written out by the same arithmetic that produced the installer's constants.
The PowerShell half already carved out `0e5` — a zero whose only non-zero digit is its exponent — but the awk scanner derived the magnitude from the exponent alone, so `0e400`, `0.0e-323` and `0.000e400` landed in the out-of-range decades and were refused where the other half configures the document. A significand of zero is zero whatever the exponent says, and no writer can change its value, so none of the range arms apply to it. Measured through both halves before and after: thirteen literals now agree. The macOS keychain comment claimed the key is unavoidably an argv element. It is not — `security -i` reads the whole command line, key included, from stdin — but that mode exits 0 whether its sub-command succeeded or failed, so a store that never happened would be reported as done and Zed would show no models with nothing to say why. Both halves now record the measurement and the decision it forces.
…her number takes `budget_tokens` was the one value this projection put on Zed's wire without asking whether Zed can hold it. It is an `Option<u32>` under Zed's fallible-options macro, so a fractional one is swallowed to `None` rather than reported: the model reaches the picker in Default mode with its thinking gone and nothing on screen to say why. The catalog can carry one, since a raw upstream's thinking bounds are copied across without a numeric check. Two comments claimed more, and less, than the source they cite. `max_output_tokens` is not a required `u64` — it is an `Option` under the same macro, so a bad value there is swallowed and Zed applies its own 4096, which is not the reservation the plan computed. And a failing `max_tokens` does not take the settings document down: the entry fails its provider, and `anthropic_compatible` is itself a fallible option, so every anthropic_compatible provider is dropped while the rest of the file loads. Both are now stated as measured, with the macro permalinked beside the struct.
The three paths were described as a preference that changes nothing but reachability. They do not carry the same request: VS Code emits the reasoning effort an operator picked only inside a thinking config, and that config is built from budget capabilities `customendpoint` never forwards — so on `messages`, the shipped default, the Thinking Effort picker records a choice the wire never sees, while `chat-completions` sends it whenever the model declares its levels, with no thinking gate. Measured at the pinned commit: messagesApi.ts builds `effort` inside `if (thinkingConfig)`; chatEndpoint.ts reads the budgets from `supports.min_thinking_budget`/`max_thinking_budget`; and byokProvider's `resolveModelInfo` sets streaming, tool_calls, vision, thinking, adaptive_thinking and reasoning_effort — never the two budgets. The comment beside the default, the type's own comment, and the operator's hint in both locales now say this. The element gate's justification was also half right: jq aborts on every scalar but `null`, which it indexes to `null` and would carry through the rewrite. Both halves say so now; both already refuse it.
… message The model-listing catch is the one place in this module that puts an upstream error message in the operator's log, and an upstream is not ours — whatever it says may quote the request it was given. Both secrets in hand are redacted there by hand, and nothing observed either: the served body carries neither, so asserting on the body alone left the log free to carry both. A case now rejects the listing with a message quoting the raw key and the lease token, and asserts the log holds the markers and neither secret. Verified by removing each redaction in turn.
…g they match The PowerShell case said it was byte-identical to the shell rendering but never looked at it: the expectation was rebuilt with `JSON.stringify`, the production serializer, which is exactly the technique the shell case beside it rejects for following the implementation through any change of key order. Neither test compared the halves, so the claim rested on nothing. The catalog is now spelled out as its sibling spells it out, and the two prefixes are rendered from one input and their embedded catalogs compared — which is what the comment always said. Verified by giving the PowerShell arm its own serialization and watching the case fail.
…ng three decoders Each half now has one execution shape — strip, merge, write — where it had a classifier in front of the merge that tried to predict what jq, `ConvertFrom-Json` on 5.1, and `ConvertFrom-Json` on 7.x would each do with the operator's document. Comments and trailing commas are the operator's own content, since both editors read their file with a lenient parser. Refusing them meant a document the editor loads was one this installer would not touch; they are stripped now, and the merge sees plain JSON on both halves. Nothing survives that a whole-file rewrite would have kept anyway. The number machinery is gone with it. It existed to keep jq and PowerShell from disagreeing about values at the edge of the double range — 300- and 750-digit boundary constants, a digit comparator, a magnitude walk — for files whose numbers are font sizes and token counts. `NaN` and `Infinity` stay refused on both halves, because jq rewrites those to values the operator never wrote and a person might paste one. What remains on the PowerShell side is a strict-JSON check, because neither decoder there is strict: both take a single-quoted string, an unquoted key, a raw control character and an unterminated container, and would write the document back as a different file where jq simply refuses it. Net: two shared modules and four call sites lose 393 lines and gain 263.
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 Zed and VS Code as Agent Setup targets, beside Claude Code and Codex. One
PR because the two share everything but their merge programs: one projection of
the model catalog on the gateway, one set of managed-file primitives per shell,
and one dashboard pane shape.
Neither editor can discover models — Zed's
anthropic_compatibleprovider hasno fetch path, and VS Code's
customendpointdrops every model it cannot type —so the gateway projects the catalog once, in
models.ts, and embeds the resultin the script it serves. The dashboard preview reads the same function through a
package export. That projection previously existed three times over, as a jq
program, a PowerShell loop, and a preview builder which had to agree byte for
byte and repeatedly did not.
Zed
Why
anthropic_compatibleZed reaches Floway through its
anthropic_compatibleprovider rather thanopenai_compatible. Protocol choice is not the reason —PublicModel.endpointsis the upstream wire surface, and any of chatCompletions/messages/responses
makes a model reachable from all four inbound chat routes, so every model works
on either provider. The reason is client-side fidelity:
openai_compatiblehardcodesJsonSchemaSubsetwith noescape hatch;
anthropic_compatiblehas no override and gets fullJsonSchema. The subset rejectsif/then/$refoutright and silentlydrops
format,additionalProperties, andexclusiveMinimum/Maximum.mode: Thinking { budget_tokens } | Adaptivemaps 1:1 ontochat.reasoning.budget_tokensandchat.reasoning.adaptive, which have nohome at all in the OpenAI dialect.
429/529,retry_after, and theerror.typetaxonomy into typed retry semantics; the OpenAI path flattenseverything at the transport boundary.
[DONE]sentinel,with a comment naming gateways as the reason.
0. This matters because Zed does no token counting anywhere — notiktoken, no
count_tokenson the trait, no/v1/messages/count_tokenscall— so streamed
usageis its only truth source for the context meter andauto-compaction.
parallel_tool_callsis not a loss: on the Chat Completions path afalseomits the field entirely, and Zed never emits
disable_parallel_tool_use, soboth surfaces land on the protocol default.
Why
global_settings.jsonBoth installers write
global_settings.json, a settings layer Zed reads belowthe user's own file and never creates or writes
itself — added upstream for
"enterprises with automation … without interfering with user's settings files".
Owning that file outright is what keeps this off the user's own
settings.json,which is theirs to shape and full of comments.
global_settings.jsonis read bythe same lenient parser, so an operator may have written comments there too —
both installers strip them and merge the plain document that remains, which is
what a rewrite of the whole file would cost them anyway.
The merge touches only the one provider key, matching the Claude and Codex
installers.
Shape
Zed is configured but never installed: it ships outside any package manager
these scripts drive, so a missing configuration directory is a hard stop rather
than a directory to create. Model entries are snapshotted from
/v1/modelsbecause
anthropic_compatiblehas no discovery path, and are selected bykindrather than by
endpoints.The credential is written to the OS store ahead of the settings document.
Without a key
is_authenticated()is false and every model disappears from thepicker with no error shown, so a registered provider with no credential is the
failure mode worth avoiding; an unreferenced credential is harmless.
VS Code
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.Comments here are even less durable than Zed's: VS Code rewrites this whole file
itself whenever Manage Models changes anything. They are stripped on the way in
just the same.
Persisted configuration
The strict schema parses stored rows as well as request bodies, so adding a
required
vscodekey needs migration0082to 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
0082fails it namingvscode; deleting0081fails it naming
zed.Shared installer behavior
Both installers treat the operator's document as theirs. There is one execution
shape on each side — strip the comments and trailing commas the editor accepts,
merge, write — and the only things refused are the ones no merge can carry: an
unreadable file, a shape neither half can merge into, and a document that is not
strict JSON, which each half would otherwise repair into a different file than
the other.
NaNandInfinityare refused for the same reason, since jqrewrites them to values the operator never wrote. Every mutation runs through a
backup that is removed on any failure, staged owner-only when the document
carries the key, and written through a symlink rather than over it.
Verification
The installer harness runs the real scripts under both shells. Several VS Code
cases 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.
Since then the harness has grown to 224 installer tests, and the
round that followed found the divergences a single-host suite cannot:
awkunder a UTF-8 locale dies on a smart quote pasted from a web page rather than
classifying it, and
ConvertFrom-Jsonon Windows PowerShell 5.1 does notenumerate a top-level array — which made both editors configure nothing on
every Windows run while every test stayed green. Those were found by running the
served script on a real 5.1 host (5.1.26100.8875), which is also where the
credential-store and ACL paths were measured.
Full repo: 5683 tests, typecheck, lint, and the web build all green.
Notes
user file, so a Floway entry in
global_settings.jsonsurvives it.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.