mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 19:13:28 -10:00
fix/termux-android-platform
421
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fbfc2a07fa |
fix: add android platform support for Termux npm install
`npm install -g @gitlawb/zero` failed on Android/Termux with EBADPLATFORM because `package.json` `os` array lacked `"android"`. Termux provides a Linux userspace — the existing `linux-arm64` binary works natively. - Add `"android"` to the `os` array in package.json. - Map `android` platform to `linux` in postinstall.mjs resolvePlatform(). Fixes #449 |
||
|
|
b63fe696fd |
modify readme.md and add readme_zh.md (#436)
Co-authored-by: zhangzetao <[email protected]> |
||
|
|
3fbbd28e4c |
fix(provider-wizard): allow multiple custom OpenAI-compatible providers (#403)
* fix(provider-wizard): allow multiple custom OpenAI-compatible providers Generic/custom providers (custom-openai-compatible, custom-anthropic-compatible) all share the same CatalogID. wizardProviderStoredKey matched by CatalogID, which blocked creating more than one custom provider. Fix: in advanceProviderWizard, skip the ManageKey diversion for custom providers and route directly to the endpoint step instead. The user can overwrite an existing instance by re-entering the same name, or create a new one with a different name. Named providers (OpenAI, Anthropic, etc.) are unaffected — ManageKey is still shown for them. Fixes #375 * refactor(provider-wizard): deduplicate advance/discovery branch, add anthropic-compatible test --------- Co-authored-by: Baran3575 <[email protected]> |
||
|
|
b9ddd6f421 |
feat(sandbox): unelevated Windows fallback tier instead of prompts-only degrade (#427)
Before the one-time elevated `zero sandbox setup` has run, Windows dropped straight from the native sandbox to the in-process policy gate: commands ran with no OS enforcement at all. But only the WFP network filters actually need Administrator rights - the write-restricted token and the workspace ACL grants (DACL edits on user-owned roots) work unelevated. Add an `unelevated` enforcement tier between native and degraded: when the elevated setup marker is missing and the profile restricts the filesystem, the command still wraps through the command runner at a new `unelevated` sandbox level. The runner applies the workspace ACL plan itself (recording applied plans by hash in windows-unelevated-setup.json so repeat commands skip the apply) and launches under the same write-restricted token, so the OS-level write-jail holds. Network enforcement stays with the per-command approval gate at this tier, and the downgrade reason says so; profiles that need the sandbox only for network isolation keep the old degrade behavior, and `--sandbox require` still errors with the setup hint. Verified end to end on Windows 11 without elevation: a write inside the workspace succeeds under the restricted token, a write outside every granted root is denied by the OS (new ZERO_SANDBOX_REAL_SMOKE integration test, using cmd.exe because managed PowerShell cannot initialize its crypto provider under a write-restricted token on some hosts). Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
aaad4d2712 |
feat(providers): split minimax zai into intl cn (#398)
* feat(providers): split minimax zai into intl cn - Add minimaxi-cn (api.minimaxi.com) sharing model catalog with minimax - Add zai-cn (open.bigmodel.cn) splitting zai from china to intl - Decouple env vars: ZAI_API_KEY vs ZHIPU_API_KEY for zai/zai-cn, MINIMAX_API_KEY vs MINIMAXI_API_KEY for minimax/minimaxi-cn - Map zai-cn and minimaxi-cn to models.dev providers "zai" and "minimax" so the model catalog hydrates for both endpoints * chore(providers): address review feedback on intl/cn split - Run gofmt on internal/providermodelcatalog/catalog.go (map-value alignment on the \"zai\" entry was off by one space, fails CI lint) - Update stale comment in TestResolveAppliesZaiInternationalCatalogDefaults to say glm-4.5 default (live discovery upgrades to glm-5.2) - Add missing ProviderKind, APIKeyEnv, and APIKey assertions to TestResolveAppliesZaiInternationalCatalogDefaults so it would catch a silent regression in ZAI_API_KEY routing |
||
|
|
b4275e3504 |
feat(providers): add Meituan LongCat catalog preset (#424)
Add LongCat-2.0 as a first-class OpenAI-compatible provider preset so it can be selected by name in the setup wizard / `zero providers setup longcat` instead of hand-entering a custom endpoint. LongCat-2.0 is Meituan's open, OpenAI- and Anthropic-compatible model. This wires the OpenAI-compatible transport: base URL: https://api.longcat.chat/openai (Zero appends /chat/completions) model: LongCat-2.0 auth: Bearer via LONGCAT_API_KEY Purely additive — one descriptor plus its catalog/onboarding tests and a README env-var line; no shared plumbing is touched, so existing providers are unaffected. Placed with the other OpenAI-compatible third parties (after moonshot), below the first-party providers. Verified: go build / vet / test green (catalog + cli onboarding), gofmt clean, and `zero providers catalog` lists the descriptor with the correct fields. Endpoint is wired by-spec from LongCat's published docs; a live completion still needs a real LONGCAT_API_KEY to confirm. |
||
|
|
5b2b4dea1a |
fix(tui): compose help overlay through the viewport overlay pipeline (#421)
* fix(tui): compose help overlay through the viewport overlay pipeline exactly like how other popups works. * fix for the CI/CD of formatting * Added regression tests for the help overlay * formatted code |
||
|
|
b5a78dca6f |
docs(install): document Bun install and improve missing-binary error (#418)
Bun does not run dependency lifecycle scripts by default, so the npm wrapper's postinstall (which fetches the native Zero binary from GitHub Releases) is silently skipped. The first run then fails with an unhelpful "No native binary found" message that points at a source-only build command, leaving Bun users stuck. Add a Bun section to README.md and docs/INSTALL.md with the two working install paths (run postinstall.mjs manually, or add the package to the project's trustedDependencies). Rewrite the wrapper's missing-binary error to give an actionable fix for everyone and a Bun-specific hint when run under Bun, instead of only suggesting a source checkout. |
||
|
|
952788f72d |
fix(tui): resolve every permission request so the agent can't deadlock (#397)
A goroutine dump of a run "stuck for 33 minutes" showed the agent parked forever in executeToolCall -> requestPermission, blocked on the TUI's decision channel. Root cause: a shell command whose sandbox decision was Allow but which requested additional sandbox permissions (sandbox_permissions: with_additional_permissions) made shouldRequestPermission block on OnPermissionRequest, yet buildPermissionEvent stamped the event Action=allow. The TUI's permissionRequestMsg handler only rendered/resolved Action=prompt requests and silently dropped everything else — so the decide callback was never called, the agent waited on a channel nothing would signal (no ctx cancellation on the active run), and the UI showed "Working" with no prompt and zero network activity. Intermittent and model-dependent: gpt-5.5's folder-creation command set the additional-permissions flag, glm's did not, which is exactly why one hung and the other ran clean. Two-part fix: - agent (buildPermissionEvent): an additional-permissions request is an ELEVATION the user must consent to, even over a sandbox Allow, so mark it Action=prompt (matching shouldRequestPermission's own condition). It now surfaces as a real prompt instead of a dropped auto-allow. - tui (permissionRequestMsg handler): enforce the contract that a blocked agent ALWAYS gets a decision. A non-prompt action resolves immediately and FAILS CLOSED (deny; cancel stays cancel) so the UI can never silently grant an unsurfaced permission; a superseded-run (runID mismatch) resolves as cancel so its parked goroutine unblocks without depending on ctx-cancel ordering. Matches the ACP handler's always-resolve, fail-closed contract. Red-green: reverting the tui branch reproduces the deadlock (decide never called); reverting the agent override reproduces Action=allow. End-to-end against a mock backend emitting the exact triggering tool call, the real TUI now renders the permission prompt and the run completes on approval, where before it parked forever. |
||
|
|
f915f70e5a |
fix(mcp): block cross-origin credential redirects (#396)
Co-authored-by: Gautam Manchandani <[email protected]> |
||
|
|
cdf9d839ae |
fix(tui): title /model rows by model name, not the catalog description (#395)
The model picker rendered each row's title from the model's description via modelPickerDisplayName, which returned the description whenever it was non-empty and not a known-generic placeholder. Catalog/discovery descriptions come from models.dev as full sentences (e.g. "DeepSeek chat model for instruction following, coding, and analysis"), and several models share the same sentence — so the picker showed sentence-long titles and rows that looked like exact duplicates, with the real model id surviving only in the meta line. Prefer the (prettified) model id as the row title; fall back to a non-generic description only when there is no id to name the row. Distinct ids that share a catalog description now render as distinct rows. |
||
|
|
960db9660e |
fix(tools): require permission before web_search requests (#382)
* fix(tools): gate web_search behind network permission * chore(tools): clarify web_search permission reason --------- Co-authored-by: Gautam Manchandani <[email protected]> |
||
|
|
b1ccb6d9c1 |
feat(providers): add KiloCode and OpenCode provider support (#388)
* feat(providers): add KiloCode and OpenCode provider support Add KiloCode (Kilo Gateway), OpenCode Zen, and OpenCode Go as catalog entries using the existing openAICompat transport. No new runtime code needed — both gateways expose standard OpenAI-compatible /chat/completions. Fixes #383 * fix(providers): swap OpenCode Zen default model to deepseek-v4-flash gpt-5.5 is served via /responses on Zen, not /chat/completions. deepseek-v4-flash works with openAICompat transport. Co-Authored-By: Vasanthdev2004 |
||
|
|
0bc8074c97 |
feat(providers): add zero providers models to discover a provider's models (#386)
`zero providers` had no way to list the models a provider actually serves, so a custom OpenAI-/Anthropic-compatible endpoint (e.g. a self-hosted proxy serving a dozen models) forced users to hand-write a config object per model. Live model discovery already existed for the TUI picker but had no headless CLI surface. Add `zero providers models [name] [--json]`: it probes the provider's live model-listing endpoint (an OpenAI-compatible /v1/models or the Anthropic /models) via providermodeldiscovery.Discover, using the profile's base URL and resolved credential (inline -> stored -> env). This uses the raw discovery primitive with no curated-catalog merge or coding-model filtering, so a custom provider's full model list is returned. Configure the provider once, run `zero providers models` to see what it serves, and use any id with `zero exec --model <id>` (Zero passes unknown model ids through to the provider) -- no per-model config needed. Wired through a new appDeps.discoverProviderModels hook (default: providermodeldiscovery.Discover) so the command is unit-tested hermetically. Fixes #381. |
||
|
|
72eed06b4f |
fix(config): unbrick first-run setup — default google/anthropic models, enter setup on fixable config errors (#385)
* fix(config): default missing google/anthropic models from the catalog
A hand-written provider entry like {name: "google", provider_kind:
"google", apiKey: ...} with no model bricked Zero: config resolution
failed with "provider google requires model", and since zero config and
bare zero setup both resolve up front, the only commands that could have
fixed the config died with the same error (reported in the wild — the
one working escape, zero setup google --model <m>, is undiscoverable
from that message).
The openai kind has always defaulted a missing model
(modelregistry.DefaultModelID); google and anthropic were missed. Fall
back to their provider-catalog default models (gemini-2.5-pro /
claude-sonnet-4.5) the same way, behind the same gate: provider-command
configs (normalizeProvidersWithoutModelDefaults) still surface exactly
what the external command returned. An explicitly configured model
always wins, and profiles with a catalogID already got this via
applyCatalogDescriptor — this covers the catalog-less hand-written case.
The residual requires-model error (openai-compatible customs, which
have no catalog default) now carries an actionable hint: add "model" in
config.json or re-run zero setup with --model.
Tests: google/anthropic defaulting (red-green: reverting the google
fallback reproduces the exact reported error), explicit model wins,
provider-command gate still errors with the hint, and validation treats
official-kind model-less profiles as clean while still flagging customs.
* fix(cli): enter setup on a requires-model config and hint zero auth login
Two residual first-run gaps after the catalog model-defaulting fix:
- An active provider whose model has NO catalog default to fall back on
(custom openai-/anthropic-compatible endpoints) still bricked bare
`zero` and `zero setup` — the exact commands that could have repaired
the config. Tag that resolve failure with ErrProviderRequiresModel and
extend the interactive TUI's fixable-error fallback (previously only
ErrNoActiveProvider) to drop into the setup wizard for it. The sentinel
is attached via a message-preserving wrapper (multi-target Unwrap), so
headless commands (zero config / zero exec) print the exact actionable
text unchanged and every other resolve error stays fatal.
- `zero login` / `zero logout` don't exist (it's `zero auth login`);
first-run users try them and got only "unknown command" (reported in
the wild). The unknown-command error now answers: did you mean
"zero auth login"? Unrelated unknown commands get no suggestion.
Tests: the sentinel comes from a REAL Resolve over a real config file
(message-prefix asserted, no key leak); the TUI fallback is red-green
(reverting the condition reproduces the exact brick with the old error);
the existing no-active-provider fallback and other-errors-stay-fatal
tests still pass; login/logout hint with a no-false-positive check.
|
||
|
|
33dc7ae2cc |
fix(tools): CRLF line ending mismatch in edit_file tool on Windows (#378)
* fix(tools): CRLF line ending mismatch in edit_file tool on Windows * fix: resolve gofmt error, add robust CRLF guard, and fix Windows sandbox and OAuth test flakiness |
||
|
|
292e1edea5 |
security(config): project config should not override sandbox.network to allow (#379)
* security(config): project config should not override sandbox.network to allow * fix(config): use NormalizeNetworkMode in project config merge, per coderabbit review |
||
|
|
39e7100674 |
fix(gemini): strip unsupported JSON Schema fields from tool declarations (#374)
Every exec against the Google/Gemini provider failed: Zero emits tool parameter schemas in OpenAI's JSON Schema dialect, which always includes `additionalProperties` (schemaToRuntimeMap writes it unconditionally). Google's functionDeclarations schema doesn't recognize that keyword and 400s the whole request — "Unknown name \"additionalProperties\" … Cannot find field" — so no tool-using call ever reached the model. Sanitize tool parameters in the Gemini mapper (only there — the field is valid and wanted for OpenAI strict mode and harmless for Anthropic): - sanitizeGeminiSchema keeps only the keywords Gemini's Schema type supports (type, description, enum, properties, required, items, anyOf, default, nullable, format, and the min/max/length constraints) via an allowlist, so any future OpenAI-ism or MCP-supplied field can't leak. - Recurses through properties, items, and anyOf so nested schemas are cleaned at every depth, not just the top level. Tests: a unit test on the sanitizer (nesting, legit fields preserved, nil-safe) and an end-to-end test asserting the outgoing request body's functionDeclarations[].parameters carry no additionalProperties while the real schema survives. Bypassing the sanitizer reproduces the reported payload, so the coverage is red-green. Fixes #373 |
||
|
|
027a8f2768 |
fix(config): use ~/.config on macOS and enter setup when no provider (#371) (#372)
On macOS, DefaultUserConfigPath used Go's os.UserConfigDir which returns ~/Library/Application Support, while users (and Claude Code) place config at ~/.config/zero/config.json. The user config was never read, leaving activeProvider empty and the launch failing with `active provider "" not found` on a fresh `go run cmd/zero setup`. - Add config.UserConfigDir(): on darwin, resolve to ~/.config (honoring XDG_CONFIG_HOME), matching Linux and Claude Code. DefaultUserConfigPath, the TUI user-commands dir, and specialist manifests all route through it. - Add config.ErrNoActiveProvider sentinel; the interactive TUI now drops into the setup wizard when resolve fails solely due to a missing/ unresolvable active provider, instead of exiting fatally. Other resolve errors (malformed JSON, directory conflicts) remain fatal. Note: `zero setup`/`zero auth` already MkdirAll the config dir before writing, so the directory is auto-created on first write. Co-authored-by: smallnest <smallnest@gmail> |
||
|
|
bd89a1f451 |
feat: require manual approval before npm publish + drop release-as pin (#369)
The publish-npm job now targets the npm-publish environment, whose required-reviewer rule pauses the job for maintainer approval in the Actions UI before anything reaches the registry. Also removes the release-as 0.1.0 pin from release-please-config.json now that v0.1.0 has shipped — leaving it would stamp every future release 0.1.0. |
||
|
|
c807f92b72 |
chore(main): release 0.1.0 (#368)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>v0.1.0 |
||
|
|
8eccc26698 |
feat: publish zero to npm via release-please (#367)
* feat: publish zero to npm via release-please Adds release-please to automate versioning and releases from conventional commits on main: - release-please-config.json / .release-please-manifest.json: node release type (package.json is the version source of truth), plain v-prefixed tags, bootstrap-sha at current HEAD, and release-as 0.1.0 to pin the first release (remove after v0.1.0 ships). - .github/workflows/release-please.yml: maintains the release PR; on merge it tags + creates the GitHub Release, then dispatches release-artifacts.yml at the tag (GITHUB_TOKEN-created tags cannot fire on-push-tags workflows). - .github/workflows/release-artifacts.yml: tag-gated steps now match the ref instead of the push event so the dispatched-at-tag run publishes assets, and a new publish-npm job publishes @gitlawb/zero after verifying every postinstall asset URL is publicly downloadable (also blocks publishing while the repo is private). * Update .github/workflows/release-please.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: publish to npm via OIDC trusted publishing instead of NPM_TOKEN npm's trusted publishing exchanges the workflow's OIDC token for publish auth, so no long-lived NPM_TOKEN secret exists to leak, and provenance is generated automatically. Drops registry-url from setup-node (its .npmrc line hard-fails on the unset NODE_AUTH_TOKEN) and updates npm first (OIDC needs npm >= 11.5.1). One-time caveat: npm only lets you configure a trusted publisher on an existing package, so the first @gitlawb/zero publish is a manual bootstrap. * fix: drop npm@latest from the OIDC publish path Rely on the npm bundled with the pinned Node 24 toolchain (>= 11.5.1, the trusted-publishing minimum) instead of pulling an unpinned npm@latest from the registry inside the publish job, and fail fast with a clear error if a future toolchain change regresses it. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
6e0a665118 |
fix(auth): propagate credentials to every provider-build surface and pin children to the live provider (#366)
* fix(auth): propagate credentials to every provider-build surface and pin children to the live provider Sub-agents, swarm members, ACP sessions, and mid-run escalation switches could build providers without the credentials the parent was running with. This consolidates credential resolution so every surface gets the same auth: - Wrap deps.newProvider once in fillAppDeps to fold the stored API key in centrally; ACP, exec escalation, and buildProvider no longer patch it per call site (an apiKeyStored profile previously went out keyless on those paths and 401'd). - Export ZERO_PROVIDER at startup (buildProvider) and keep exporting on every runtime switch (wizard, onboarding, /model, /provider), so spawned children resolve the parent's live provider instead of whatever another process last persisted to config.json. Injected via appDeps so tests never mutate the process environment. - Share one OAuth login selection end to end: ProviderProfile. OAuthLoginCandidates (name, then catalog-ID fallback; nothing for a profile with its own configured credential) + oauth.FirstStored resolve the login ONCE, and the bound store key is threaded through providers.Options.OAuthLoginKey so the Codex account-id header always reads the same login that issued the bearer token. A renamed profile (name != catalog ID) now finds its login; an explicit API key is never silently overridden by a same-name or catalog-shared OAuth login. - Export ProviderProfile.HasConfiguredCredential as the single "profile is key-authed" predicate (cli/tui/config previously had three copies). - applyProviderWizard commits live state (provider, profile, name, env export) only after BOTH the build and the config persist succeed, so a persist failure no longer leaves the chat running on a provider the status line and children disagree with. Covered by red-green tests for each path: stored-key wrap, escalation switch, catalog-ID fallback (incl. case-variant names), explicit-key protection, same-login account selection, wizard persist-failure rollback, onboarding/startup env export. * test: isolate OAuth/config test storage and fix the flaky MCP response-id test Review + CI fixes, all test-only: - factory/cli OAuth tests: pin ZERO_OAUTH_STORAGE=file next to the temp ZERO_OAUTH_TOKENS_PATH — an inherited "keyring" value sends NewStore to the OS keychain and ignores the temp path entirely, so the tests could read (or clobber) the developer's real logins. newOAuthStoreForTest now owns both pins so a future call site can't forget them. - TestApplyProviderWizardExportsActiveProviderEnv: point m.userConfigPath at a temp config and stub newProvider, so the SUCCESS path exercises the full build+persist+commit sequence in isolation (the default empty path silently skipped persist; a changed default must never reach the real user config). - TestClientRequestWaitsForMatchingResponseID (ubuntu smoke flake): the responses were pre-loaded into a bytes.Buffer, so the client's reader goroutine could consume the matching response — and hit EOF — before request() registered its pending id, failing with "request() error = EOF" on fast runners. The fake server now sits on pipes and responds only after reading the outgoing request, matching production ordering; survives -race -count=50. |
||
|
|
142c548c89 |
feat(tui): FILES sidebar panel with click-to-select and file drill-in (#365)
* feat(tui): FILES sidebar panel with click-to-select and file drill-in
Add a FILES section to the right context sidebar showing the files the
session has touched — newest first, with A/M status badges, aggregated
+/− diffstats, and a pulsing row for the file being written right now.
Clicking a row selects the file (accent marker, edit cards tint in the
chat, transcript scrolls to the latest edit); a second click opens a
drill-in view: the chat column swaps to the file's stacked edit cards,
`f` toggles to the syntax-highlighted on-disk file with markers on
session-added lines, `d` back to diff, Esc returns to the chat.
The roster is fed from two sources: changedFiles carried by
write_file/edit_file/apply_patch results (persisted per session, so it
survives /resume), and a git sweep — baseline snapshot at startup,
re-check after command tools and at turn end — so files created through
bash/exec_command scaffolding or subagents show up too.
* fix(tui): address FILES sidebar review findings
Review fixes for the FILES panel / drill-in / git sweep:
- touchedFiles: split multi-file diffs per file (perFileDiffStats) so an
apply_patch spanning several files no longer charges the whole patch's
+/- totals to every file it touched; order by lastRowIndex descending
so a re-touched file lists first (reversing first-seen order kept it in
its original slot).
- sidebarFileLines: filter the live-writing row out before computing the
"+N more" trailer so the skipped entry no longer inflates the count.
- git sweep: run `git diff --numstat -z` and parse the NUL-terminated
rename records (counts + preimage + postimage) — plain --numstat
brace-mangles renamed paths ("src/{old => new}"), so their diffstat
never matched a porcelain path and renamed files stayed at 0/0;
gitTouchedFiles now yields newest detection first to match the
roster's ordering.
- file view: stream the full-mode read and stop at fileViewMaxLines
(os.ReadFile loaded a multi-GB file wholesale before truncating — the
exact freeze the cap exists to prevent); re-opening the file already
being viewed is a no-op so a stray re-click can't bounce full mode
back to diff or reset scroll; skip sub-4-char lines in the changed-line
markers so common braces/returns don't mark unrelated occurrences.
- keys: gate the drill-in's d/f and Esc handling on noBlockingModal so a
permission prompt / ask-user / wizard keeps its own key handling.
- sidebar: a live in-flight write counts as content, so the FILES pulse
shows for the session's first mutation.
- tests: numstat test now feeds real -z record shapes and asserts the
rename postimage key; new coverage for per-file stats, re-touch
ordering, live-row overflow, same-file reopen, truncation, modal
gating, live-write content, and a real-repo rename sweep.
---------
Co-authored-by: Gnanam <[email protected]>
|
||
|
|
02f0c0929b |
fix: cut the content-stall dead-wait and auto-retry a mid-tool-call stall (#362)
* fix: cut the content-stall dead-wait and auto-retry a mid-tool-call stall Root-caused (via a verified multi-agent audit) the chatgpt/gpt-5.5 "stuck" reports: the ChatGPT Codex backend pauses real generation mid-stream on large single tool-call outputs (a ~1000-line write_file) while continuing SSE keep-alive heartbeats. That is an upstream limitation, not a Zero bug or a connection-pooling issue (the darwin DisableKeepAlives fix is applied and confirmed not the cause). Zero already detected it, but two UX gaps made it feel like a hang: 1. Faster abort. The content-stall watchdog fired at idle*2 = 10 minutes. A heartbeat-pause stall on gpt-5.x rarely recovers, so 10 minutes of a frozen token counter was a terrible wait for a doomed turn. Replaced the StreamContentStallFactor=2 const with ContentStallTimeout(idle)=idle*1.2 (6 minutes at the default). It still only ever fires on a TRUE zero-output freeze — any real data line resets it — so healthy slow-reasoning runs are unaffected. Scales with ZERO_STREAM_IDLE_TIMEOUT; returns 0 (disabled) when idle is disabled. 2. Auto-retry the actual stall. The existing stall-retry only fired when NOTHING had been forwarded — so the common case (model streamed transient reasoning + began the big write_file, then froze mid-arguments) was NOT retried, because the incomplete tool call is collected and transient output was forwarded. Relaxed the gate to retry when no visible PROSE was shown (forwardedVisibleText, marked only by OnText) and no final text was collected — dropping the len(ToolCalls)==0 exclusion, since an incomplete tool call from a timed-out stream is never executed (the turn returns on the error before dispatch) and never appended to messages, so the retry re-sends clean context with no duplication. A visible "[no output — model stalled; retrying N/max]" notice fires per attempt. Forwarded prose still blocks the retry (it would duplicate answer text). Verified: full -race suite green; a 4-dimension root-cause audit and a 3-dimension adversarial review of this diff both passed with no confirmed defects. New tests: TestRunRetriesStallAfterIncompleteToolCall (the exact gpt-5.x case), TestRunRetriesStallAfterOnlyReasoning; preserved TestRunDoesNotRetryStallAfterPartialOutput (prose still blocks retry). * fix(providerio): make ContentStallTimeout overflow-safe Address PR #362 review: idleTimeout*6/5 could overflow int64 for a pathologically large ZERO_STREAM_IDLE_TIMEOUT and wrap to a NEGATIVE duration, which would arm the content-stall timer to fire immediately and abort every stream. Compute 1.2× as idle + idle/5 with a clamp to the max duration on overflow (which just means "effectively no content watchdog", the sane result for an absurd idle timeout). The normal 1.2× values are unchanged (5m->6m, 30s->36s, 100ms->120ms). New test: TestContentStallTimeout covers the scaled values, the zero/disabled case, and the MaxInt64 overflow guard. |
||
|
|
3fde03a900 |
fix(tui): show the queued-message preview above the composer, not below (#361)
A message typed while a run is active is queued for the next turn. Its "[queued] …" preview rendered on the line BELOW the input box; move it to directly ABOVE the box so it reads as "waiting to send," sitting on top of what you're currently typing, and doesn't get visually detached from the composer by the status line beneath it. Footer height is unchanged (the preview line just moves from below to above the box). New test: TestQueuedPromptPreviewSitsAboveComposer asserts the preview line index is above the composer box top border. |
||
|
|
85273ea72d |
fix(providerio): disable HTTP keep-alive reuse on macOS to defeat degraded-connection stalls (#360)
This codebase already has a documented, previously-diagnosed macOS-only bug: "sharedHTTPClient... defeats the stale-pooled-connection hang... reproduced on BOTH chatgpt and ollama... surfaces on macOS far more than Linux because macOS keeps dead pooled connections around longer." The existing mitigation (ResponseHeaderTimeout=120s, IdleConnTimeout=30s) catches a reused connection that never responds at all, or one that sat idle too long. Traced a live report that doesn't fit either case: a session accumulated 301 real completion tokens over ~12.5 minutes (~0.4 tok/s) on chatgpt/gpt-5.5 before being manually cancelled - not silent, just catastrophically slow, on a retry shortly after a prior attempt on the same host. That's a third failure mode: a reused connection that's alive-but-severely-degraded, still delivering real bytes (enough to keep resetting Zero's stream idle/content-stall watchdogs, which only fire on true silence) but never at a usable rate. Basic network diagnostics on the affected machine (DNS, TCP connect, TLS handshake) were all healthy and routed directly with no VPN/proxy involved, pointing at the same connection-reuse class of bug rather than a local network misconfig. DisableKeepAlives removes pooling from the equation entirely rather than just tightening the timeout window, since a degraded-not-dead connection isn't reliably distinguishable from genuine backend slowness once inside the stream. Scoped to darwin only via runtime.GOOS: a fresh TCP+TLS handshake per request costs low tens of milliseconds (measured ~92ms against a real host on the affected machine) against potential minutes-long stalls, and Linux/Windows don't exhibit this OS-level pooled-connection behavior, so they keep full connection reuse. New test: TestHTTPClientReturnsStallHardenedSharedClient extended to assert DisableKeepAlives matches runtime.GOOS == "darwin". |
||
|
|
0914ced9e1 |
test: widen timing margins on two Windows-flaky CI tests (#359)
Both failed on windows-latest today, on merges of otherwise-unrelated PRs (#349, #356) - neither PR touched internal/specialist or internal/agenteval. Windows GitHub Actions runners have coarser default timer granularity and more scheduling contention than Linux/macOS, so tight timing margins that hold on the other two platforms aren't reliable there. - TestOutputToolBlocksUntilTaskCompletes: a 100ms poll timeout against a 5ms background write left only 20x margin. Widened the timeout to 2000ms (a ceiling, not a target - PollInterval keeps the test fast in the success case; it only changes how much CI jitter it tolerates before failing). - TestHarnessTimeoutCancelsBlockedAgent: confirmed there's no actual data race (harness.Agent.Run is a direct synchronous call, not a spawned goroutine racing the assertions) - the failure was workspace materialization alone consuming the full 10s budget on a contended Windows runner, so the agent was never reached. Widened to 30s. This test's own design blocks for the full timeout to observe cancellation, so unlike the other fix this directly costs ~20s more wall-clock time, but that's inherent to what it's testing. |
||
|
|
68669073c2 |
fix(tui): explicitly request the initial window size at startup (#358)
Zero is always in alt-screen mode (useAltScreen() is unconditionally true), and transcriptView's `if m.altScreen && m.height > 0` gate is what pads the rendered frame out to fill the full terminal height (scrollableTranscriptItemsView pads bodyWindow to window.height). If m.height is ever 0 - Bubble Tea documents an initial WindowSizeMsg as delivered automatically on program start, but that's the terminal proactively pushing a size, and nothing else ever asks again if that push is missed (a slow/unusual terminal, a multiplexer, a startup race) - Zero falls back to the unpadded, non-fullscreen render path for the rest of the session: content only occupies as many rows as it needs, leaving the remaining terminal height as plain unfilled background instead of the alt-screen's painted panel color. Fixed by explicitly firing tea.RequestWindowSize in Init(), the same defensive pattern already used for tea.RequestBackgroundColor right below it - Zero doesn't rely solely on an unprompted push for either. |
||
|
|
33410d8730 |
fix(tui): make the stall watchdog visible before it fires, not just after (#357)
Zero already has a content-stall watchdog built specifically for this exact case (providerio.ErrStreamStalled's own doc comment: "observed on chatgpt/gpt-5.x and ollama reasoning models") - a heartbeating-but-silent upstream is aborted after idleTimeout * StreamContentStallFactor (5min * 2 = 10min by default). It works. The gap is that until it fires, the "still generating... Xs" cue is the ONLY signal the user has, and it looks completely identical whether real (if slow) content is still coming or the stream is already dead and just hasn't been caught yet - confirmed against a live report where the token count sat frozen for 7+ minutes with no way to tell from the UI alone. Past half the effective idle timeout, quietGenerationHint() now escalates to name what's actually happening and when Zero will act on its own: "still generating... Xs - unusually quiet, Zero will auto-recover by ~Ys if it doesn't resume". Exported StreamContentStallFactor from providerio (was unexported) so the TUI can compute the same ceiling the watchdog itself uses, instead of duplicating the constant. New test: TestQuietGenerationHintEscalatesPastHalfIdleTimeout, using ZERO_STREAM_IDLE_TIMEOUT to shrink the timeout so the test runs fast while exercising the exact same math the real watchdog uses. |
||
|
|
c0254e3266 |
fix(tui): context-fill gauge for custom/local Ollama models + related fixes (#356)
* fix(tui): show the context-fill gauge for custom/local Ollama models
The colored context gauge (used/window · N%) only renders when Zero knows
a model's context window. It resolves that two ways: a curated remote
catalog (how Grok/hosted models are known), or live /v1/models discovery —
but parseModelsResponse only ever extracts id/description from that
response; OpenAI-compatible model listings don't carry context-window
metadata at all. A custom/local Ollama model tag (e.g. a locally-pulled
":cloud"-tagged model) has no curated-catalog entry to borrow one from
either, so it always resolved to an unknown window and the gauge silently
fell back to a bare token count with no color grading.
Ollama does expose this, just via a different, Ollama-native endpoint
(/api/show, not /v1/models) whose model_info map reports it under an
architecture-prefixed key like "qwen2.context_length". Added
DiscoverOllamaContextWindow (providermodeldiscovery) to probe it, wired
through an async command scoped to the local "ollama" catalog ID only
(not "ollama-cloud", a different hosted service this endpoint isn't
assumed to exist on) — fired from Init() for the provider active at
launch, and from switchProviderModel for a provider switched to
mid-session (which previously warmed no discovery at all for the new
provider, a separate gap this also fixes).
New tests: DiscoverOllamaContextWindow's endpoint construction and
model_info parsing (including a mixed-type model_info and a missing
*.context_length key), the discovery command's scoping to local Ollama
only, the Update() handler populating modelContextWindow's fallback, and
switchProviderModel warming discovery for the newly active provider.
Local only — not pushed. I can't run a live Ollama daemon in this
sandbox, so the actual /api/show request/response shape (verified against
Ollama's documented API, not a live instance) needs confirming against a
real daemon before this goes out.
* fix(tui): stop showing the token count twice next to the context gauge
contextWindowSegment (the gauge) and usageStatusSegment (the plain "N tok"
figure) both read the exact same value (latestUsageTokens) — the code
already suppressed the plain figure when the sidebar shows it ("the token
figure shows on both sides"), but never extended that same dedup to the
gauge. So as soon as a model's context window becomes known (e.g. after
the previous commit's Ollama fix), the footer showed the identical number
twice: once inside the gauge, once again right next to it.
Fixed by falling back to cost-only whenever the gauge is shown, same as
the sidebar-active branch already does.
* fix: check collect()'s deadline even while output keeps draining
The wait deadline in execSession.collect() was only checked in the branch
reached when drainString() returns empty. A background process producing
output continuously enough to keep the buffer perpetually non-empty could
theoretically starve that check forever, turning the caller's requested
timeout into an unbounded block regardless of yield_time_ms.
Investigated as part of a cross-provider "stuck" report (multiple sessions
across different models/providers all hanging on the same prompt, each
with a background terminal running and no forward progress). Could not
empirically reproduce a hang via a synthetic continuous-writer test under
Go's real scheduler — the reader won the race often enough in practice
that this doesn't appear to be the actual cause of those reports. Keeping
the fix regardless since it closes a real, if hard-to-trigger, gap: the
deadline is now checked in both branches, so continuous output can never
prevent collect() from returning within its requested window.
|
||
|
|
c4c14d62d4 |
fix(oauth): make xAI / Hugging Face OAuth actually launch from the wizard (#355)
The provider wizard offered "Sign in with OAuth" for xAI and Hugging Face, but clicking did nothing: the login managers were built without an env, so the preset opt-in (ZERO_OAUTH_ALLOW_PRESETS) read as unset, ResolveConfig errored before any browser step, and the error was never rendered on the provider list. Only ChatGPT worked, because its login path force-enables presets. Add ManagerOptions.AllowPresets, which layers the opt-in onto the manager env (snapshotting the process environment so ZERO_OAUTH_<NAME>_* overrides survive envValue's hermetic-map rule) without the operator exporting the flag. Set it on every user-initiated OAuth manager: the wizard loopback login, the device-code prepare/complete, the post-login token refresh, the runtime resolver that authenticates model calls, and `zero auth login`. This fixes xAI end to end (it ships a public client_id, so the browser now opens). Also surface a failed OAuth attempt on the provider list (renderProviderStep) so the click is no longer a silent no-op, with a Hugging Face hint pointing at app registration + ZERO_OAUTH_HUGGINGFACE_CLIENT_ID (HF ships no public client_id, so it still requires that one env var by design). |
||
|
|
49bec0274f |
feat(tui): interactive theme picker + color theme catalog (#354)
* feat(tui): interactive theme picker + color theme catalog
Bare `/theme` now opens a popup that live-previews each palette as the cursor
moves and applies on select (Esc reverts), matching /model and /effort. Adds ten
built-in color themes alongside the dark/light built-ins — dracula, nord, gruvbox,
tokyo-night, catppuccin, one-dark, solarized-dark, rose-pine, everforest, and
solarized-light — plus a reworked light palette. Every palette is contrast-audited
to WCAG AA; the contrast tests now loop the whole registry.
Palettes and an ordered registry live in a new internal/tui/theme_palettes.go (now
the sole home of hex literals); selection, the picker, /theme <name>, --theme, and
ZERO_THEME all resolve through it. The whole frame is painted with the active
theme's surface (tea.View.BackgroundColor) so every theme is legible on any
terminal — a light theme's dark text no longer lands invisibly on a dark terminal,
and color themes show their real surface.
The chosen theme persists to user config (preferences.theme) and is reapplied at
startup, below the --theme flag and ZERO_THEME.
* fix(tui): address CodeRabbit review on the theme catalog
- CHANGELOG: reconcile the --theme bullet with the named themes (was still
documenting {auto|dark|light}).
- /theme usage string advertises the list and auto forms (/theme list is still
accepted) so /help matches the command surface.
- Theme-persistence test parses the written config and asserts preferences.theme
directly instead of a raw substring, so a wrong-key regression fails.
|
||
|
|
bcc65da2a0 |
fix(tui): require a second Esc press to cancel a running turn (#352)
* fix(tui): require a second Esc press to cancel a running turn A single stray Esc during an active run cancelled it instantly with no chance to reconsider - e.g. a press meant to dismiss an overlay that had already closed. Ctrl+C already guards app-exit with a double-press confirmation (exitConfirmActive/exitConfirmSeq); Esc now gets its own independent confirmation (cancelConfirmActive/cancelConfirmSeq) guarding run-cancellation specifically, since the two are different actions with different consequences armed by different keys. Mirrors the existing seq-gated tea.Tick pattern: first Esc arms a 3s window and shows "Press Esc again to cancel" in the footer; a second Esc within the window cancels the run; letting it expire, or the run finishing on its own, disarms without cancelling. Updated the three existing tests that assumed single-press cancellation (TestEscCancelsPendingRun -> TestEscRequiresSecondPressToCancelPendingRun, TestEscCancellationLeavesVisibleMarker, TestEscCancelRecordsSessionError, TestCancelledRunFlushesCheckpointSessionEvents) and added TestEscCancelConfirmationExpires mirroring the Ctrl+C expiry test. * fix: pre-push review findings on the Esc cancel-confirmation gate Ran an adversarial review before opening the PR. Three independent reviewers converged on the same root defect: unlike exitConfirmActive (disarmed by any key other than Ctrl+C), cancelConfirmActive had no "anything else disarms it" safety net, so it behaved as a plain 3s timer rather than a true immediate-second-press gate. Concretely: - Typing, Ctrl+O, or any other non-Esc key left it armed, so a later, unrelated Esc press could silently cancel the run. - A mid-turn ask_user or permission prompt lands asynchronously and is checked earlier in the Esc handler than the cancel-confirm block, so it silently "stole" the confirming press (denying the prompt instead) while leaving cancelConfirmActive armed for a subsequent, unrelated Esc to act on. - Mouse clicks resolving a permission prompt or entering a subchat had the same gap. Fixed by mirroring the Ctrl+C pattern: any non-Esc key disarms it (model.go, next to the existing exitConfirmAction reset), and any left/right mouse click disarms it too (mouse.go — scoped to clicks, not hover/motion, since motion fires continuously and would otherwise defeat the window whenever the mouse sits over the terminal). Within the Esc handler itself, restructured to capture whether this press was really a confirm *before* any of the earlier branches (subchat, MCP wizard, ask-user, permission, picker, suggestions, queued-message, detailed-transcript) can run, then disarm unconditionally — so an Esc consumed by any of those no longer leaves a stale confirmation for a later press to trip. Also updated two now-stale help strings the review flagged: the `?` overlay's Esc row and pickerBusyText()'s "press Esc to cancel" hint, both of which still described the old single-press behavior. Added TestEscCancelConfirmationDisarmsOnInterveningKey (mirrors the existing Ctrl+C equivalent) and TestEscCancelConfirmationDisarmsWhenStolenByAskUser covering the two confirmed gaps. * fix: address CodeRabbit review comments on PR #352 - model.go: the first Esc (arming the cancel confirmation) was clearing the composer draft immediately, even though nothing had actually been cancelled yet — a leftover from the old single-press behavior, where clearing the draft and cancelling the run happened atomically as one action. Now that cancellation is two presses, only the confirming second Esc (or a plain Esc with no run pending) clears the composer; arming preserves whatever the user was typing, mirroring how Ctrl+C already preserves a draft (TestCtrlCClearsComposerBeforeExitConfirmation). - clipboard.go: routePaste (bracketed paste and right-click paste) never disarmed a stale cancelConfirmActive — it isn't a tea.KeyPressMsg, so it bypassed the generic "any non-Esc key disarms it" hook added in the previous commit. A paste is a deliberate action like typing or clicking, so it should disarm the confirmation too; otherwise a later, unrelated Esc could silently cancel the run. New tests: TestEscArmingCancelConfirmationPreservesComposerDraft, TestPasteDisarmsCancelConfirmation. |
||
|
|
2a8802e39c |
fix: cap unbounded exec_command output buffer to prevent OOM kill (#353)
* fix: cap unbounded exec_command output buffer to prevent OOM kill
execOutputBuffer.data accumulated every byte a backgrounded exec_command
session wrote, with no size cap, unlike buffer.recent which is correctly
bounded to recentExecOutputBytes for the /ps preview. drainString() only
clears data when the agent actually polls that session again — so a
long-lived background session nobody polls (e.g. a dev server left running
after the run that started it was cancelled) grows this buffer forever as
long as the process keeps writing, with no ceiling.
Traced this directly to a real crash: a session that had been running for
~5.5 hours (an earlier turn invoking swarm agents got cancelled mid-flight,
per that session's own event log) accumulated to ~42.7GB of compressed
memory, at which point macOS's memorystatus killed the process outright
("killing zero due to low-swap").
Fixed by capping data at maxExecOutputBufferBytes (2MiB), trimming from the
front like recent already does, and surfacing a one-time truncation notice
on the next drain so data loss is visible rather than silent.
* fix: pre-push review findings on the exec_command buffer-cap fix
Ran an adversarial review before opening the PR. It found the truncation
notice added alongside the buffer cap didn't actually work: prefixing
execOutputTruncatedMessage into drainString()'s text meant the notice
always sat ~2MiB before the end of the combined output, but the tool's
own downstream truncateExecOutput() keeps at most a 400KB tail (even at
the maximum allowed max_output_tokens) — so the notice was reliably
swallowed, or chopped into an unreadable fragment at smaller budgets.
Verified with direct reproductions against the real functions.
Fixed by moving the signal out of the text stream entirely:
- execOutputBuffer gains consumeTruncated() (read+reset, called once per
collect()) and peekTruncated() (read-only, for status views) instead of
injecting a marker into the drained bytes.
- collect() now returns (string, bool); both call sites thread the bool
through to execToolResult, which sets meta["output_buffer_truncated"]
and appends the notice to the response body *after* truncateExecOutput
runs, so it can never be re-truncated or land in a discarded region.
- Also flagged: a session torn down without ever being polled again after
truncation loses the signal entirely, since nothing drains/logs it on
teardown. Full audit-trail logging felt like overkill for a soft
transparency signal on top of a hard memory-safety fix, but exposed
peekTruncated() on ExecSessionSnapshot and surfaced it in the /ps row
(background_terminals.go) so it's visible any time a still-running
session is checked — covers the common case (the review's own scenario:
a long-lived dev server nobody's polling but that's still alive) even
though a session reaped before ever being looked at is still a gap.
New tests: reworked TestExecOutputBufferCapsUndrainedData for the
consume/peek API, and TestExecToolResultSurfacesBufferTruncationOutsideByteBudget
reproducing the review's exact failure case (max_output_tokens=1) and
proving the notice now survives it.
|
||
|
|
d53052071a |
revert(local): restore the original bright-lime accent color (#351)
Restores accent to #caff3f (the pre-PR#350 value), undoing the three-round brightness reduction (#ade619 -> #8db620 -> #759523) merged in PR #350. Only the accent token changes - the hover-highlight color (a separate amber token) and every other PR #350 fix (subchat selection, drag-to-edge auto-scroll, the dragging-flag fix) are untouched. Full gate green. Local only - not pushed. |
||
|
|
1c65f59be6 |
fix(tui): subchat mouse selection, hover highlighting, and drag-to-edge auto-scroll (#350)
* fix(local): subchat mouse selection + hidden plan panel + scroll-extend selection Three bugs in the subagent/swarm drill-in (subchat) view and mouse text selection, all from the same root: newer features (subchat's row-source swap, wheel-scroll) were never integrated into the older selection/footer code. 1) Can't copy anything in the subchat view. transcriptLineAtMouse (and selectedTranscriptText, the copy-time text extraction) always hit-tested against m.transcript (the parent), never m.subchat.childRows, even while the screen was showing the child session. New transcriptHitTestSource() mirrors transcriptView's own branching (nav bar + child rows + full width vs pinned title bar + parent rows + sidebar-aware width) and is now the single source both functions use. 2) Plan panel shown above the composer inside the subchat view. footerView unconditionally rendered the PARENT run's pinned plan panel; only pendingAskUser was gated. Added a !m.subchat.active guard (m.plan belongs to the parent run, not whatever subagent/swarm session is being viewed). 3) Selection didn't extend across a scroll (reported as "stopped at the end of the screen"). transcriptSelection.cursor was only ever updated by a mouseMotion event; a wheel event is a distinct message type that never reached that code, so scrolling while selecting left the selection frozen at whatever line the mouse last physically moved over. New scrollChatExtendingSelection scrolls AND re-evaluates the cursor at the mouse's current position against the post-scroll viewport. Uses a new nearestTranscriptLineAtMouse (falls back to the closest visible selectable line) rather than an exact match, since the recomputed position can land exactly on a blank spacer row between messages after an odd-length scroll shifts text/spacer parity - an exact-match miss there must not silently no-op the extension. Tests: subchat selection resolves the CHILD session's text, not the parent's; the plan panel is absent from the footer during subchat; a selection's cursor bodyY moves (extends) after a wheel-scroll instead of staying pinned. Full gate green (73 pkg, -race). * feat(local): highlight clickable rows on mouse hover Moving the cursor over an interactive row now highlights it (accent underline) before you click, across every clickable surface: - specialist cards and collapse/expand toggle headers in the transcript - AGENTS sidebar rows (swarm members) - PLAN sidebar step rows - permission-prompt options (reuses the EXISTING keyboard-cursor highlight by moving pendingPermission.cursor on hover, instead of new styling) Requires switching the mouse-tracking mode from CellMotion (only reports motion while a button is held) to AllMotion (reports idle cursor movement too) - the existing 15ms mouse-event throttle already covers the added motion events. hover.go: hoverTarget + mouseHover predicate + updateHoverTarget, which reuses the SAME hit-testers and priority order as the existing click handlers so hover and click never disagree about what's clickable. Sidebar hover is resolved by STABLE IDENTITY (sessionID / stepIndex), not a cached line offset: hoveredSidebarLineOffset re-matches the identity against a freshly computed hit list on every render, so a row that's disappeared (a swarm member's linger elapsed, a plan step completed) simply doesn't highlight rather than a coincidentally-matching unrelated row lighting up. Transcript hover stays bodyY-keyed (matching the existing selection-highlight convention) with explicit clearHover() calls wherever bodyY numbering can shift without an intervening mouse motion: wheel-scroll, PgUp/PgDn, window resize, and transcript rebuilds (/clear, /resume, /rewind, /compact via resetFlushFrontier). Adversarial review caught two real bugs before ship, both fixed: - BLOCKER: plan-step hover stored the step's own index (0,1,2...) but the sidebar render treated it as a raw line offset into the rendered rows - every plan-step hover highlighted the wrong row (the AGENTS header/body). Fixed by resolving fresh at render time via the stable-identity redesign above, which also closes the related sidebar-drift issue. - MAJOR: hover wasn't cleared on keyboard PgUp/PgDn scroll. Tests: predicate correctness; hover resolves the right target across all 4 surfaces (specialist card, toggle row, sidebar agent, plan step, permission option) with the correct priority order; hover changes the actual render output (transcript + sidebar); hover clears on wheel-scroll and subchat exit; a regression test proving hoveredSidebarLineOffset self-heals (no highlight) when a hovered row's identity no longer resolves - the exact scenario the blocker fix targets. Full gate green (73 pkg, -race). * fix(local): soften hover color + auto-scroll a drag past the transcript edge 1) Hover highlight was brand-lime (bright, glaring against the near-black surface) and re-paints on every mouse movement. Switched to amber (an existing semantic token, "light orange") and dropped the underline per feedback. 2) Dragging a text selection past the top/bottom edge of the visible transcript now auto-scrolls toward that side and extends the selection to follow, matching the browser/editor "drag past the viewport edge" pattern. transcriptEdgeScrollDelta detects the drag is vertically outside the body rect (but still within its horizontal span, so it isn't just "off to the side" over the sidebar); dragToEdgeScroll scrolls one step and anchors the cursor to whichever line now sits at that edge of the NEW viewport - deliberately not a re-resolve of the drag's physical mouse position, which stays outside the body rect after scrolling (scrolling changes what content occupies a screen row, not the mouse's screen position) and would keep finding nothing. Extracted nearestTranscriptSelectableAt as the shared "closest selectable line to a target bodyY" core, now used by both nearestTranscriptLineAtMouse (target derived from a mouse position that may have landed on a spacer row) and dragToEdgeScroll (target derived directly from the viewport bound, no mouse position involved at all). Tests: dragging past the top edge scrolls toward older content and extends the cursor upward; the symmetric bottom-edge case scrolls toward newer content and extends downward. Full gate green (73 pkg, -race). * fix(local): smooth-glide the drag-to-edge auto-scroll instead of jumping The initial edge-scroll implementation (previous commit) applied a single chatWheelScrollLines (5-line) jump per raw mouse-motion event, which reads as sudden/jerky since motion events during a real drag arrive in bursts, and stops dead the instant the physical mouse holds still even while still parked past the edge (a terminal only reports motion on actual cursor movement). Replaced with a self-scheduling tea.Tick chain (dragEdgeScrollTickMsg, gated by edgeScrollSeq mirroring the existing exitConfirmSeq/copyStatusSeq idiom) that steps a small, fixed increment (dragEdgeScrollStep=1 line) on a short cadence (dragEdgeScrollInterval=70ms) for as long as the drag holds past the edge, independent of whether new raw motion events keep arriving. reducedMotion still gets the original single-bigger-step-per-event behavior (no animated glide). Adversarial review of the new self-perpetuating tick chain (the risk being an orphaned/stuck chain) found one real bug: a keypress mid-drag clears m.transcriptSelection.active directly (the KeyPressMsg handler in model.go) without going through stopEdgeScroll, leaving edgeScrollDelta stale. The in-flight tick itself terminates safely via its own liveness check - the bug was that the STALE nonzero edgeScrollDelta then fooled startEdgeScroll's "already running" fast path on the NEXT same-direction drag into silently doing nothing (no immediate step, no scheduled tick) for that whole hold. Fixed at the press site (transcript_selection.go's mouseLeftPress case): every fresh press now resets glide state via stopEdgeScroll, so a new drag always starts clean regardless of how the previous one ended - a single targeted fix rather than chasing every place transcriptSelection can be cleared. Tests: top/bottom edge glide starts and extends the cursor over several ticks (a single small step can land on a blank spacer row between messages, same as real usage); the chain stops when the drag returns to the body and a stale in-flight tick after that is a safe no-op; the keypress-interrupt regression (reproduces the adversarial review's finding, confirms the fix). Full gate green (73 pkg, -race). * fix(local): soften the brand-lime accent color across the whole UI The accent color (#caff3f) was fully saturated at fairly high lightness (HSL: L 62%, S 100%) - the kind of value that reads as neon/glaring everywhere it filled a block or bold-text surface, not just the one spot originally flagged: the composer's "typed" prompt glyph, the permission popup's focused-option badge, and the text-selection highlight all use it, plus the spinner, the brand chip, tool-card prompts, and more. Rather than patch each of those consumers individually, softened the single palette source (dark theme only; the light theme's accent is a separate, already-darkened value for its own contrast needs, untouched) to L 50% / S 80% (#ade619) - still clearly on-brand lime, and contrast against both black (onAccent text on accent fills) and the near-black panel stays far above WCAG AA (~14:1 and ~13:1, both comfortably over the 4.5:1 floor). Updated TestStreamingFadePaletteMonotonic's pinned RGB assertion (it checked the fade ramp's first bucket equals the literal old accent bytes) to the new values. Full gate green (73 pkg, -race; make lint/go vet clean except an unrelated stray file that isn't part of this branch or git history at all). * fix(local): darken the brand-lime accent further per live feedback Two prior passes (#ade619 at L50/S80, #8db620 at L42/S70) were each tested live against the original #caff3f via raw truecolor escape codes and confirmed too subtle in turn. Settled at L36/S62 (#759523) - a noticeably darker, more olive lime, confirmed against the original side by side. Contrast vs black (onAccent) and the near-black panel stays comfortably above WCAG AA (~6.1:1 and ~5.6:1). Updated the streaming-fade test's pinned RGB assertion to match. * fix: address PR #350 review comments and CI smoke test failures Smoke tests (macOS + Ubuntu): the 4 new edge-glide tests implicitly relied on m.reducedMotion being false, but mouseTestModel() inherits it from defaultReducedMotion(), which forces true whenever colorprofile.NoTTY is detected - true in CI's non-TTY test runners, false in a local interactive dev shell. Set reducedMotion = false explicitly in each of the 4 tests so they're deterministic regardless of environment. Verified by re-running locally under ZERO_REDUCED_MOTION=1 (reproduces the CI condition) - passes. CodeRabbit actionable comment: handleTranscriptSelectionMouse's mouseMotion case gated only on transcriptSelection.active, which stays true through the async copy-command grace window after release (a non-empty selection isn't reset until transcriptCopiedMsg lands) - so a genuine hover motion arriving in that window would be misrouted as a drag update. The suggested fix (gate on msg's own Button field) turned out to be wrong in a way only a full test run caught: TestTranscriptSelectionUpdatesOnGenericMotion is a pre-existing, deliberate test - some terminals don't restate the button on every motion event during a real drag, so trusting the per-event Button field breaks those terminals. Fixed properly instead: added a `dragging` field to transcriptSelectionState, tracking the app's own press/release bracket (true at press, false at release) independent of active and independent of any single event's Button field. mouseMotion now gates on dragging. CodeRabbit nitpicks: routed the two remaining raw `m.hover = hoverTarget{}` subchat-exit assignments through the existing clearHover() helper, so hover invalidation stays in one place. Left the third nitpick (hit-test layout recomputed per hover-motion tick) as a documented follow-up per the reviewer's own "trivial/poor tradeoff" framing - a proper per-frame memoized cache needs correct invalidation on scroll/resize/content-change to avoid reintroducing the exact staleness class of bug already fixed twice in this branch, and isn't worth rushing. New tests: TestTranscriptHoverAfterReleaseDoesNotMoveSelection (the actual bug CodeRabbit found - a post-release hover must not alter the just-copied selection). Full gate green (73 pkg, -race); TestTranscriptSelectionUpdatesOnGenericMotion (the test the first fix attempt broke) verified still passing. |
||
|
|
7fb0092de7 |
fix: agent runtime resilience — stream stalls, weak-model tool args, and sub-agent reliability (#349)
* fix(local): defeat the macOS stale-pooled-connection hang (transport + stall-retry)
Root cause (you confirmed it: both chatgpt AND ollama hung, only on macOS): providers
fell back to http.DefaultClient, whose pooled keep-alive connections get reused on a
later turn after the server/NAT silently dropped them. The model call is a POST
(non-idempotent), so Go won't auto-retry it on a fresh conn — it blocks forever.
macOS keeps dead pooled conns around longer than Linux, so it reproduced on macOS only.
1. providerio.HTTPClient now returns a shared, tuned transport (ResponseHeaderTimeout
60s bounds a hung POST that gets no response; IdleConnTimeout 30s stops dead conns
from lingering to be reused). Routed openai/codex through it too (anthropic/gemini
already used HTTPClient). Slow first tokens / long reasoning are unaffected — the
header timeout only bounds time-to-first-response-header, not the stream body.
2. Agent loop now retries a stream idle/stall timeout that produced NO output yet, on
a fresh connection (capped + backoff). A partial-output turn is NOT retried (would
duplicate). Recovers any stall that slips past the transport timeout.
Local only — not pushed.
* fix(local): tolerate concatenated tool-arg JSON + widen response-header timeout
1) Weak models (minimax-m3, glm) pack MULTIPLE concatenated top-level JSON objects
into one tool-call arguments slot ({A}{B}), which json.Unmarshal rejects with
'invalid character {' after top-level value' (~9 failed tool calls/session).
New recoverableToolArguments returns the first JSON value ONLY when the payload is
one value optionally followed by more WHOLE JSON values; it rejects a bad/truncated
first value OR trailing NON-JSON garbage ({"x":1}xyz) so genuine corruption still
errors (adversarial-review fix). decodeToolArguments uses it at the central dispatch
(executeToolCall) so {A}{B} runs the first object; historySafeToolCalls uses the
SAME helper so the replayed transcript records the recovered first object (not {}),
keeping dispatch and history consistent. Truly-malformed JSON still hits the
existing sanitize-to-{} + retry path. Trailing objects are dropped (the model
re-issues on a later turn).
2) Raise the shared transport ResponseHeaderTimeout 60s -> 120s so a slow cloud proxy
(ollama *:cloud) that withholds its 200 header until first token isn't wrongly
aborted; still bounds a truly dead reused connection.
Local only — not pushed.
* feat(local): add /turns command + raise default maxTurns 30->50
Agentic multi-step runs (build + delegate + swarm + MCP + self-verify) routinely hit
the 30-turn ceiling mid-task and never reached the later steps; removing /mode earlier
also left NO interactive way to raise the budget.
- /turns [n]: per-session tool-turn budget setter (commandGroupSession), mirroring
/selfcorrect. Empty/status shows the current budget; a positive int sets
agentOptions.MaxTurns (read by the run at model.go); clamped to maxTurnsCeiling=500
to guard against typos; invalid/non-positive shows usage without changing it.
- defaultMaxTurns 30 -> 50 (matches the old 'deep' preset). Raise per-run with /turns
for long tasks (e.g. /turns 150 for a full acceptance run).
Local only — not pushed.
* fix(local): swarm reports failed members as failed + sub-agents inherit /turns budget
Issue B — a swarm member whose child exited non-zero (e.g. exit 4 / max-turns) was
recorded [done] (swarm_status '0 failed'), so the orchestrator — which can't see the
AGENTS panel — trusted incomplete work. The specialist launcher ignored
res.Result.Status; it now surfaces a StatusError result as a member FAILURE
(errors.New(report)) so the swarm marks it [failed]. Added Coordinator.FailWithSession
and switched the member watch path to it so a failed member keeps its session id
(still drillable) and its report rides along as the failure message.
Issue A — a member hit its OWN turn limit because /turns raised only the parent's
budget; children re-resolved config.json's default. New config.SetMaxTurnsEnv exports
ZERO_MAX_TURNS, read in applyEnv -> cfg.MaxTurns; /turns now exports it so spawned
sub-agents / swarm members (which inherit the env) run with the same budget. A
non-numeric/zero env value never clobbers the configured budget.
Tests: launcher non-zero-exit -> failed (+ session kept); SetMaxTurnsEnv round-trip
+ garbage-env guard; /turns exports + clamps the env. Full gate green (73 pkg).
Local only — not pushed.
* fix(local): corrective error for unknown specialist name + clamp ZERO_MAX_TURNS
1) A model that invents a specialist name (validator-runner, file-creator,
file-writer — none registered) got an opaque 'specialist not found' and kept
spawning doomed sub-agents. loadManifest now lists the available specialists and
notes that 'worker' has shell/edit while explorer/code-review are read-only, so the
model self-corrects to a capable specialist. New availableSpecialistList helper.
2) Defense-in-depth (review nit): applyEnv now clamps an inherited ZERO_MAX_TURNS to
MaxTurnsCeiling, so a raw shell 'export ZERO_MAX_TURNS=999999' can't bypass the
/turns ceiling. Ceiling promoted to config.MaxTurnsCeiling (single source of truth,
referenced by the TUI /turns clamp).
Tests: unknown-name error lists worker/explorer/code-review; applyEnv clamps an
over-ceiling env value. Full gate green (73 pkg).
Local only — not pushed.
* fix(local): surface sub-agent kill signal instead of opaque exit -1
When a sub-agent child is terminated by a signal, command.Wait returns an ExitError
whose ExitCode() is -1, losing the cause. Capture ProcessState.String() (portable,
e.g. "signal: killed") into ChildRunResult.Signal and have BuildFinalResult render
it — "Subagent terminated by a signal (signal: killed) — killed before it finished.
Likely causes: the OS out-of-memory killer ... a timeout, or cancellation." — instead
of an opaque "Subagent failed (exit -1)". The message lists causes rather than
asserting OOM, since timeout/cancellation also SIGKILL the child. A captured signal
always surfaces even if a late run_end reported a clean exit. The background task
path (which can't carry the signal name) emits the same hint for a negative exit code.
* address CodeRabbit review on #349
- loop.go: gate stall-retry on a turn-level forwardedAnything flag (wrapped
callbacks), so a retry never re-streams on top of output already forwarded earlier
in the turn (e.g. across the reactive-compaction retry). New test covers a
reasoning-then-stall (collected.Text empty but output forwarded).
- loop.go: route a reissued stream's non-stall error through the SAME recovery as
the initial stream (image-rejection wrapping / context-limit compaction) via a
shared recoverStreamError closure, instead of returning it raw.
- exec.go: drop the hardcoded worker/explorer/code-review guidance from the
unknown-specialist error; rely on the dynamically-rendered available list (a
custom registry may not have those names).
- streamer.go: kill-signal message lists OOM/timeout/cancellation evenhandedly
instead of asserting OOM (the branch also covers timeouts/cancellations).
- launcher_specialist.go + exec.go: preserve the child SessionID on a post-start
failure too, so FailWithSession keeps those failed members drillable.
- model.go: block /turns while a run is active (mid-run change would make the
inherited ZERO_MAX_TURNS budget inconsistent for later-spawned sub-agents).
|
||
|
|
5aced721b3 |
fix(tui): sub-agents inherit the parent's live provider after a /model switch (#348)
* fix(tui): sub-agents inherit the parent's live provider after a /model switch
Swarm members and Task sub-agents run as child 'zero exec' processes that re-resolve
the provider from config.json themselves — they're passed --model but never the
parent's provider. After switching provider/model in the TUI via /model (an in-memory
change), the child still resolved config.json's stale activeProvider, landed on a
provider whose credentials didn't match the live model, and failed auth in ~3s
(exit 3, 0 tool calls, 'API key missing or invalid / can't reach the provider') — so
the spawned agent appeared then instantly disappeared.
Export ZERO_PROVIDER (config.SetActiveProviderEnv) to the live provider name on a
/model provider switch. Child processes inherit the environment, and config
resolution already maps ZERO_PROVIDER -> activeProvider, so the child resolves the
SAME profile and its credentials (env key / stored key / OAuth) — covering all three
auth methods, for both Task and swarm (both spawn via the specialist executor).
Startup needs no export: the parent and child both read config's default, so they
already match; only an in-memory switch diverges.
* fix(config): clear ZERO_PROVIDER when switching to an unnamed/default profile
SetActiveProviderEnv("") now unsets ZERO_PROVIDER instead of leaving the previous
value in the environment. Switching from a named provider back to an unnamed/default
profile otherwise kept exporting the stale provider into child zero exec processes,
which would resolve the old credentials. Extended the round-trip test with the
stale-env regression case: seed a value, switch to blank, assert both os.Getenv and
applyEnv no longer see it.
Addresses CodeRabbit review on #348.
|
||
|
|
02a3d12f34 |
fix(providerio): bound heartbeat-but-no-output streams so they can't hang forever (#347)
* fix(providerio): bound heartbeat-but-no-output streams (content-stall watchdog) A stalled-but-alive upstream hung the agent indefinitely: SSE keep-alives reset the idle watchdog (intentionally — a heartbeating upstream isn't 'dead', and aborting it killed healthy long requests), so a stream that heartbeats while producing nothing never tripped the idle timeout and there was no other ceiling. Observed on both chatgpt/gpt-5.x (Codex) and ollama minimax-m3 — different provider code, same shared providerio SSE loop — so the fix belongs here, once, for every provider. Add a second 'content' watchdog alongside the idle one: keep-alives still reset idle (no regression for heartbeating connections), but only REAL data lines reset the content watchdog. If no data arrives for streamContentStallFactor (2) × the idle timeout, abort with ErrStreamStalled instead of hanging. A slow-but-producing stream (data between heartbeats) keeps resetting content and is never killed. Contained to internal/providers/providerio — no provider/caller changes. * fix(providerio): surface ErrStreamStalled in callers + widen flaky test margins Address review: - All four provider stream handlers (openai, codex, anthropic, gemini) now treat ErrStreamStalled in the same branch as ErrStreamIdle, via a new providerio.StreamTimeoutMessage helper that gives the stall a distinct, accurate message (it reports the content window and does NOT claim the upstream stopped sending data — keep-alives were still arriving). Previously a stalled stream fell through to the generic provider-error path. - Widen the content-watchdog test timing margins (idle 100ms, data/keepalive every 30ms) so CI/GC jitter can't trip the idle watchdog and mask the content path. - Add a StreamTimeoutMessage unit test. |
||
|
|
a0548d707b |
test(providers): isolate the OAuth store in the chatgpt Codex routing test (#346)
TestNewRoutesChatGPTCatalogToCodexProvider asserts the chatgpt-account-id header is empty when no OAuth login is stored, but never redirected the token store — so on a developer machine with a real chatgpt login it read that login and failed (it still passed in CI, where no token exists). Point ZERO_OAUTH_TOKENS_PATH at an empty temp path, mirroring TestNewRoutesChatGPTCatalogWithStoredAccountID, so the test is deterministic regardless of the developer's stored credentials. |
||
|
|
83a371d669 |
fix: stream Codex reasoning + drop swarm agents on their own completion (#345)
Two agent-visibility fixes surfaced diagnosing a 20-minute gpt-5.5 step and an 'agents never disappear' report. A) Codex Responses path streamed no reasoning, so a long thinking phase produced zero visible output and read as a hang (the activity clock never advanced). Root cause: the request never asked for a reasoning summary and the parser had no case for reasoning deltas. Now request reasoning.summary="auto" and forward response.reasoning_summary_text.delta as StreamEventReasoning — the existing TUI handler renders it live and refreshes the activity clock. B) A finished swarm member stayed in the AGENTS panel until the whole turn ended. Now it drops once ITS OWN task completes (fade over the linger window, then remove), independent of the overall run — matching how finished specialists already behave. Running members and mid-flight collect are unaffected. |
||
|
|
84823d7be1 |
fix(swarm): constrain agent_type to the registered roster via a dynamic enum (#344)
* fix(swarm): constrain spawn/handoff agent_type to the registered roster via enum swarm_spawn and swarm_handoff described agent_type / to_agent_type as a free-form string with only an 'e.g. teammate, subagent' hint, so a model could pass an unregistered type (e.g. 'worker') and only discover it was invalid from the runtime 'unknown agent type' error — wasting tool calls, especially on weaker models. Set the parameter's enum dynamically from the live roster (Registry.AgentTypes()), so the schema advertises the valid set (built-ins plus any user-registered agents) up front and an invalid type can't be sent. Falls back to a free-form string when the roster is unavailable, so the tool never advertises an empty, un-satisfiable enum. The runtime 'available agent types: …' error is kept as a backstop. * test(swarm): also assert handoff to_agent_type enum picks up a custom roster type Address review: the custom-type recheck only re-read spawnTool, so a regression leaving handoffTool on the built-in list would have passed. |
||
|
|
cfd065b565 |
fix(openai): always send message content so strict OpenAI-compatible servers accept it (#343)
Some OpenAI-compatible endpoints (e.g. glm-* on Ollama-cloud) reject a request whose message 'content' is absent or null with HTTP 400 'invalid message content type: <nil>'. The chat-completions mapper relied on 'content,omitempty', so a message with empty text (a tool result, an assistant turn that only makes tool calls, or a sub-agent that failed with no output) serialized with no content field at all. - Drop omitempty on chatMessage.Content and always set it (to "" when there is no text), so contentless messages serialize as "content":"" instead of being dropped. Standard OpenAI/xAI/groq already tolerated the omission; this makes the strict servers work too. - Skip a degenerate assistant turn that has neither text nor tool calls, mirroring the Anthropic/Gemini mappers (which already drop empty turns). Fixes the failure seen on glm-5.2:cloud after a sub-agent failed with empty output. |
||
|
|
fb3a0bff44 |
Document source-build sandbox helpers (#342)
Clarify that Linux source builds need the zero-linux-sandbox helper on PATH for native sandboxing, while macOS does not need an extra helper and Windows can self-dispatch from zero.exe. Also document optional release-style helper builds for Linux and Windows. Tested: git diff --check |
||
|
|
072f3877b5 |
fix(opengateway): correct gateway host, make it the recommended default (#341)
OpenGateway pointed at gateway.gitlawb.com, which does not resolve (NXDOMAIN), so every request failed with "no such host". The live gateway is opengateway.gitlawb.com — a flat OpenAI-compatible endpoint (/v1/chat/completions, Authorization: Bearer ogw_live_…) that smart-routes by model id across its upstreams (xiaomi-mimo, minimax, qwen, google, nvidia, z-ai). - catalog: fix base URL to https://opengateway.gitlawb.com/v1, default model to mimo-v2.5-pro; add a Recommended descriptor field and hoist OpenGateway to the top of the catalog so it is the first/badged pick everywhere. - models: replace the curated model list with the gateway's real upstream models; fix the stale model-discovery fallback host. - providers catalog: sort recommended-first (CLI + snapshot) and render the ★ … (recommended) badge; same badge in the TUI /provider wizard and the setup onboarding list. - providerhealth: OpenGateway's flat /v1/models 404s by design, so probe its public /health endpoint for connectivity instead of reporting a false fail. - tests: update catalog order golden + transport/discovery expectations; add coverage for the recommended provider and the health-endpoint override. Co-authored-by: Kevin Codex <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
1dd884d6e9 |
chore(launch): MIT license, windows-arm64 install guard, TUI TTY guard (#340)
* chore(launch): MIT license field, windows-arm64 install guard, TUI TTY guard Three launch-readiness fixes the docs cleanup doesn't cover: - package.json: set "license": "MIT" (was the placeholder "SEE LICENSE IN LICENSE"), matching the LICENSE file and the README badge. - scripts/postinstall.mjs: skip gracefully on windows-arm64. (win32, arm64) resolves to a valid platform/arch but the release matrix ships no windows-arm64 artifact, so the installer would download a 404 and hard-fail npm install. Treat it like any unsupported target (warnSkip, exit 0) and point to the x64 build (which runs under emulation on Windows on ARM) or source. - internal/tui/run.go: fail fast when stdin is not a terminal. The interactive shell otherwise blocks forever in the Bubble Tea event loop on piped/redirected input (e.g. `echo "" | zero`); now it prints guidance toward `zero exec` and exits non-zero. Dependency-free ModeCharDevice check (no x/term added). Tests: windows-arm64 skips with exit 0; the interactive shell returns non-zero (not a hang) on non-TTY stdin. * fix(tui): use term.IsTerminal for the stdin guard; pin exit code in test Address review on the no-TTY guard: - Replace the os.Stdin.Stat()/ModeCharDevice heuristic with term.IsTerminal (github.com/charmbracelet/x/term, already in the module graph via bubbletea). It is a true TTY check — it rejects pipes, regular files, and non-terminal char devices like /dev/null — and fails closed: anything not a verified terminal blocks the interactive shell. - The test now asserts the documented exit code 2 from the guard, not just a non-zero result, so it proves the stdin guard fired. |
||
|
|
726f1512a3 |
Clean up public release docs (#339)
Rewrite the README around the initial public-release user flow, add the logo asset, and refresh install/update documentation. Remove internal planning, audit, design, and work-split documents from the public docs tree. Update stale documentation references and workspace seed expectations to point at public docs. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/workspaceseed Tested: git diff --check |
||
|
|
8c5898d5db |
feat(reasoning): typed per-provider reasoning capability from a models.dev catalog (#338)
* feat(reasoning): typed per-provider reasoning capability from a models.dev catalog
Providers describe reasoning control with incompatible concepts — OpenAI a
discrete effort enum (split by minor version), Anthropic and Gemini 2.5 a
thinking-token budget, some models only a thinking on/off toggle — which a flat
effort string cannot represent and a model-name heuristic cannot detect.
Add internal/reasoning: a Capability type (a model's reasoning flag plus its
typed Controls — effort/budget_tokens/toggle) and a Catalog that looks capability
up by provider + api model id. The data is an embedded, trimmed snapshot of the
community capability database (first-party providers only; routers excluded since
they carry generic/stale option lists), so detection is offline and exact instead
of guessed.
The package depends only on the standard library, so the model registry and
provider adapters can consume it without an import cycle. This change is purely
additive — nothing is wired into the live resolution path yet.
Tests pin the per-version ground truth (gpt-5 minimal-not-none, gpt-5.1
none-not-minimal, gpt-5.1-codex-max xhigh, gpt-5-pro locked to high; Anthropic
budget-only legacy vs effort-only newer with the budget removed; Gemini 2.5
budget/toggle vs Gemini 3 effort) and that every reasoning model Zero ships today
is covered by the snapshot.
* fix(reasoning): address review — presence-aware budget bounds, deep-copy lookup, first-party-only snapshot
- Control.Min/Max are now *int so an explicit bound is distinct from an omitted
one: Gemini's `min: 0` ("thinking can be disabled") and Anthropic's missing
max (unbounded) are preserved instead of both collapsing to 0. Budget() becomes
BudgetControl() returning the control so callers read the pointers.
- Catalog.Lookup returns a deep copy (clone) of the matched Capability, so a
caller can no longer mutate the shared embedded catalog through the returned
Controls / Values slices or budget pointers.
- Drop the google-vertex section from the snapshot: it re-hosts third-party MaaS
models (Anthropic/Moonshot/OpenAI/DeepSeek/Qwen/Meta), violating first-party
-only. google (AI Studio) already covers first-party Gemini, so the gemini
provider kind resolves there. Snapshot 158 -> 123 models, 16.8 KB.
Tests pin the explicit min: 0 preservation, the nil max on budget-only Anthropic
models, and that a mutation of a Lookup result does not leak into the catalog.
* fix(reasoning): address round-2 review — accessor deep-copy, max tier, provenance
- EffortControl/BudgetControl now return a cloned Control, so a caller holding a
Capability that shares state with the catalog cannot mutate Min/Max pointers or
the Values slice through an accessor result (CodeRabbit). Lookup already deep
-copies; this closes the accessor path too.
- Add modelregistry.ReasoningEffortMax ("max") + accept it in ValidReasoningEffort,
so the snapshot's "max" tier (e.g. gpt-5.1-codex-max, newer Anthropic) has a
constant to map to. No curated model lists it yet.
- Snapshot provenance: add a _fetched date and a regeneration recipe (documented
in catalog.go), so refreshes are reproducible/auditable.
- Document that Lookup takes the API model id (wire name), not the friendly
registry id, and that the match is exact (no case-fold/prefix-strip).
- Record the source-of-truth intent in the package doc: this catalog is the
intended authoritative source; modelregistry's name-pattern inference is demoted
to a fallback when it is wired in (a later change). Rename the `cap` builtin
shadow to `entry`.
Tests pin the accessor deep-copy isolation.
|
||
|
|
7a09a42a17 |
feat(tui): remove the /mode command (superseded by /model + /effort) (#337)
The /mode presets bundled a hardcoded Anthropic/Google model + reasoning effort + turn budget, so on other providers (Ollama-cloud, xAI, GPT-5, custom) /mode would switch to a model the user isn't authenticated to. With /model (any provider's models) and /effort (reasoning effort) covering the same ground flexibly, /mode is redundant in the TUI. Removes the /mode slash command, its handler, the mode picker, and the dispatch + stale comments; updates affected tests. The CLI `--mode` flag and internal/ modelregistry mode presets are kept (still used by headless exec). |