Repository navigation
Fixes the 3.8.1-31 known issue: while signed out (custom-provider config only, no stored login), picking any model from the /model list connected to nothing. Model switching now resolves credential-less official-slot references to the same provider's custom-env slot. The model pickers also drop the internal env- prefix, the signed-out list is restricted to custom.env models, and the custom-provider config file is renamed to custom.env with automatic legacy migration.
Fix: signed-out /model selection was unusable (3.8.1-31 known issue)
Why: In 3.8.1-31 the deduplication kept the official entry and dropped its env-slot twin. The official bigmodel slot is managed by /login and carries no credential while signed out, so setModel("bigmodel/glm-5.3") reached the runtime with no API key — while the credentialed env-bigmodel entry was exactly the one deduplication removed.
What changed:
src/identity.tsaddsresolveModelSlotRef(): an official slot ref (<provider>/<model>) is returned unchanged when the provider has a vault login or an official-slot key; with neither, it falls back to theenv-<provider>slot when that slot carries a key and declares the model. The fallback is judged per provider (signing in to zai does not affect picking a bigmodel model). Env-slot refs, models not declared in the env slot, and providers without a keyed env slot pass through unchanged.- All three model-switching entry points in the TUI —
switchTransientModel(shared by the/modellist, manually typed/model <provider>/<model>, and the quick cycle toggle) and the/settings → Model providerssession apply — pass throughresolveModelSlotRef()before sending to the bridge./settingspersistence writes the resolved slot ref, so a signed-out save points directly at the usable env slot (the post-loginswitchModelBlockToOfficialProvidermigration is unchanged). selectors.tscurrent marking now matches both forms (the internal slot idenv-<provider>/<model>and its prefix-free display form), fixing the lost "current" marker and/settingspreselection that 3.8.1-31 introduced.
Compatibility: The 3.8.1-31 dedup direction (each model listed once, official entry wins) is unchanged — the fix does not touch withoutEnvSlotTwins(); it extends the fact that the env slot is the only usable path while signed out from the display layer to the switching layer. Env-only entries and signed-in behavior are identical to 3.8.1-31; the 3.8.1-26 "the env file is the model-block authority while signed out" semantics are unaffected.
Verification: tsc --noEmit passes; 7 new identity unit tests (signed-out fallback, undeclared model passes through, no keyed env slot passes through, official-slot key passes through, vault token passes through, per-provider independence, env ref and no-slash alias pass through) and 2 new selector unit tests (current marker matches the env-slot form of an official twin in the flat picker and the provider cascade); full bun test green; all 5 TUI smoke tests pass.
Model pickers: no more env- prefix; signed-out list shows only custom.env models
Why: The internal env-<provider> slot ids leaked into the display layer — env-only entries showed up prefixed in the flat /model list, and /settings → Model providers split one provider into duplicate official + env groups. After the 3.8.1-31 incident the list layer is also tightened: while signed out, only entries that actually carry credentials are listed, because nothing else can connect.
What changed:
modelPicker()/providerModelPicker()gain a thirdsignedIn?: booleanparameter:undefinedkeeps the full list (backwards compatible),falsekeeps only env-slot entries,trueshows everything. Entry values, labels, and generated/modelcommands all go throughdisplayModelRef(); cascade grouping keys switch todisplayProviderId(), merging official and env slots of the same provider into one group with twin-deduplicated entries; group-name fallbacks drop the prefix; current marking and preselection match both forms. WithsignedIn === false, both the flat and cascade views filter out non-env-slot entries.- All three call sites (
showModelPicker,showModelProviderSettings, the quick cycleswitchModel) pass the sign-in state fromreadSignedInProvider(). An empty/modellist no longer opens a silent empty picker — it explains per sign-in state (signed out: "sign in (/login) or configure ~/.zcode/cli/custom.env"). - Selection still resolves through
resolveModelSlotRef()to a credentialed slot, so every listed entry is actually usable.
Verification: the acceptance group (11 cases, including selection-path guards R7/R8 reproducing the 3.8.1-31 incident and signed-out filtering R9/R10) is green; tsc --noEmit passes.
Config file rename: custom-provider.env → custom.env (automatic migration)
Why: Per the project naming convention, provider in custom-provider.env is a redundant modifier — the name simplifies to custom.env. Existing machines had the old file, so automatic migration was required.
What changed:
src/env-config.ts: the filename constant is renamed tocustomEnvFileNamewith valuecustom.env(single source of truth).migrateLegacyEnvFile()grows from one legacy layer (.env) to a two-layer chain —custom-provider.envfirst,.envas fallback; migration is skipped whencustom.envalready exists (coexisting old files are kept untouched) or whenZCODE_ENV_FILEoverrides; at most one rename per launch.- The migration notice in
src/launcher.tsreports the actual destination path instead of hardcoding.env; the signed-out hint insrc/identity.tsuses the new name. - The template is renamed
custom-provider.env.example→custom.env.example;docs/CONFIGURATION.mdand the install sections of all three READMEs are updated. - Tests: path assertions use the new name; the migration suite covers four cases (long-name migration, direct
.envmigration, coexistence prefers the long name and keeps.env, override skips).
Verification: the acceptance group (5 cases) is green; tsc --noEmit passes.
v3.8.1-31 marked as pre-release
The previous release carried the signed-out selection defect above; it was marked pre-release so the Latest pointer falls back to 3.8.1-30, and a known-issue warning was added at the top of its notes pointing to the fixed version. The release itself is kept for history. With this release published as Latest, latest/download links point at the fixed version again.
Install links are now tag-pinned
All three READMEs and docs/RELEASING.md switched from releases/latest/download/... to tag-pinned URLs — the latest pointer moves on every publish while the asset name carries the version, so historical latest/download links go 404. Tag-pinned URLs always resolve to that version's asset.
Tests & CI
- Two acceptance-case groups from Hopper (TestEngineerAgent) were synced, implemented against, and archived:
model-picker-env-prefix(11 cases) andcustom-env-rename(5 cases). - Regression-net gap filling: six new test files with 46 cases covering previously untested modules (
tool-group-view,command,protocol-part-view,renderable,plugin-protocol,clipboard-text); one known defect is locked withtest.failing(captureCommandthrowsERR_STREAM_PREMATURE_CLOSEinstead of returning{code: 1, stderr}when the target binary is missing — affects theupdatefallback for users withoutghand the browser-opening fallback in the OAuth flows). - CI gate slimmed: the
validatejob dropped the release build (90% of its runtime) in favor of a vendor-runtime cache keyed onzcode-runtime.lock.json+scripts/sync-runtime.ts, and no longer re-runs on pushes to main; timeout 45 → 20 minutes; cold-cache full run ~3.5 minutes. - Housekeeping: subproject paths in
.claude/CLAUDE.mdupdated from~/Documents/Projects/to~/Developer/after the project directory migration.
Install
npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-32/zcode-cli-3.8.1-32.tgz