The use-input.test.ts added in #1198 broke the full `bun test` run two ways:
1. It imported `@testing-library/react-hooks`, which was never installed and
is React 16/17/18-only (incompatible with this repo's React 19), so the
file errored on load.
2. Its top-level `vi.mock('./use-stdin.js', …)` registered a module mock that
leaks across every later file in the same `bun test` process. The fake
eventEmitter's `.on` was a no-op, so `useInput` silently registered no
listener and dropped all keystrokes — surfacing as timeouts in
MonitorPermissionRequest and the agent-menu/wizard TextInput tests (which
passed in isolation but failed in the full suite).
Rewrite the test to inject the stdin handle via StdinContext.Provider (no
leaking global mock) and render through the real ink root (no
@testing-library/react-hooks). Drop the now-dead react-hooks and
react-test-renderer devDependencies and reconcile the lockfile.
Full suite: 3429 pass, 0 fail (was 9 fail + 1 error).
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* fix: preserve raw mode across component re-renders (issue #843)
* fix(input): only reset raw mode on explicit isActive=false, not on MCP re-render churn (issue #843)
* fix: balance raw mode for isActive false transitions + add regression test
Fixes the issue where cleanup closes over stale isActive=true and returns
early without calling setRawMode(false), leaving rawModeEnabledCount
incremented after UI no longer has active useInput.
Changes:
- Use a ref to track whether raw mode was actually enabled
- Check the ref in cleanup instead of stale isActive closure value
- Add 6 regression tests covering the true->false/unmount paths
Addresses jatmn's review feedback: 'fix raw mode balance for isActive: false transitions'
* fix(input): debounce raw-mode reset to survive MCP re-render churn (issue #843)
* fix: add react-test-renderer dep and fix use-input test for CI
- Add react-test-renderer devDependency (required by @testing-library/react-hooks)
- Add @testing-library/react-hooks to INTENTIONALLY_BUNDLED in externals.ts
- Fix use-input.test.ts 'MCP re-render churn' test to use isActive rerender
instead of separate renderHook calls (refs don't persist across instances)
* fix: address P1 raw-mode counter imbalance and P2 test-dep scope (PR #1196)
P1 (use-input.ts:64-68): skip setRawMode(true) on isActive false->true
when a deferred reset is pending, preventing counter over-increment
that leaked raw mode on final unmount. Test updated to assert
balanced 1-then-1 call pattern (no redundant setRawMode(true)).
P2 (package.json, externals.ts): move @testing-library/react-hooks
from dependencies to devDependencies; remove from INTENTIONALLY_BUNDLED.
* fix: show vision-specific error when provider returns 404 for image requests
When a user sends images to a provider/model that doesn't support
vision (e.g., MiMo V2.5 Pro via opengateway.gitlawb.com), the provider
returns HTTP 404. Previously this showed a misleading "verify
OPENAI_BASE_URL" message with no mention of images.
Now detects image content in the request body and classifies the 404 as
'vision_not_supported', showing a clear message: the model may not
support image/vision inputs, with a suggestion to remove images or
switch to a vision-capable model.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: add Accept-Encoding: identity to opengateway gateway config
The opengateway.gitlawb.com server returns gzip-compressed responses
by default, which causes ZlibError in the fetch client. Adding
Accept-Encoding: identity header requests uncompressed responses.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: move Accept-Encoding header to openaiShim.headers for chat requests
The Accept-Encoding: identity header was on transportConfig.headers which
is only used for model discovery. Chat/completions requests read headers
from transportConfig.openaiShim.headers. Move it there and add a
request-capture test confirming the header is sent.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: use structured image detection instead of substring matching
Replace serializedBody.includes('"image_url"') with a bodyContainsImages()
helper that inspects the structured message content blocks. For chat
completions it checks content[].type === 'image_url', for responses it
checks content[].type === 'input_image'. This avoids false positives when
user/tool content happens to contain the literal string "image_url".
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: move bodyContainsImages above stableStringify comment
The helper was inserted between the stableStringify rationale comment and
the serializeBody function it describes, making the comment appear to
document bodyContainsImages. Move it above the comment so the comment
remains adjacent to serializeBody.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(cron): enforce MAX_CRON_PROMPT_CHARS cap on durable cron prompt length (issue #131)
* fix(cron): move prompt-length cap to validateInput so errors surface properly (PR #131 P2)
The over-limit check was returning { success: false, error } from call(),
but the tool's output type only declares { id, humanSchedule, recurring,
durable } and mapToolResultToToolResultBlockParam doesn't handle error
results — the user sees undefined fields instead of the length-cap error.
Moving the check to validateInput() routes it through the normal
validation error path which IS properly rendered as an error message.
* fix(cron): respect durable kill switch in validateInput prompt-length cap (PR #131 P2 fixup)
* fix(BashTool): include captured output in non-zero-exit error result (#1231)
When a Bash tool command exits non-zero, the error result reaching the
model and the UI was supposed to carry the merged stdout/stderr so the
failure can actually be debugged. In practice users were seeing the
result collapse to just "Error: Exit code N" with no diagnostic
detail (see #1231 — succeed_with_output / fail_with_output reduced
case).
The failure path was sourcing the output from `result.stdout` directly
while the success path used `stdoutAccumulator.toString()`. The
accumulator is the canonical buffer — it captures the streamed output
exactly as the success path returns it (with the trimEnd + EOL
normalization at the top of the post-completion block), independent of
whether the underlying ExecResult.stdout slot is populated. Whenever
the shell runner streamed everything through the accumulator and left
result.stdout unset (or only partially set), the failure path emitted
an empty error body.
Switch the ShellError throw to use the accumulator content as the
primary source, with `result.stdout` as a fallback. Strip the trailing
"Exit code N" marker so it isn't duplicated by getErrorParts(), which
already prepends the code from ShellError.code. Behaviour is identical
when both buffers agree.
* fix(BashTool): recover failure output from progress.fullOutput when stdout slot is empty
Addresses @jatmn's P1 finding on #1249.
Previously the failure-body recovery picked between:
1. the truncating accumulator (after stripping the synthetic "Exit code N")
2. result.stdout
Both sources can be empty in the failure mode reported in #1231: the shell
runner streams every line through progress callbacks but the final
ExecResult.stdout slot ends up empty (flush-after-result race, exit before
EOF, output persisted to a file path, etc.). With both empty the patch
collapsed back to the original "Error: Exit code N" body.
Add the most recent progress.fullOutput value yielded by the streaming
generator as a third fallback source, captured in the consumer loop. The
selection logic is extracted into a pure selectFailureOutput() helper so
it can be exercised directly by unit tests — including a reproducer for
the exact failure mode jatmn called out (accumulator empty, result.stdout
undefined, fullOutput non-empty).
Local: 39 / 39 utils.test.ts pass, including 7 new selectFailureOutput cases.
Default fresh installs to the Gitlawb OpenGateway profile, keep validation behavior for saved profiles, and mark OpenGateway as the recommended provider in the picker.
Update setup docs and generated integration metadata to reflect the API-key-backed OpenGateway route, and add coverage for the fresh-install startup environment.
* fix(mistral): show all configured models in model picker (#1360)
* fix(mistral): use correct API model names and prepend selected model
* chore(mistral): fix comment and normalize model ids for consistency
* fix(mistral): remove retired models, fix descriptor ids, add multi-model test
* fix(mistral): preserve original delimiter and fix codestral descriptor id
* fix(mistral): revert codestral descriptor to match shared model registry
* fix(mistral): revert profile mutation; align with documented contract
persistActiveProviderProfileModel() is now a no-op that returns the
active profile. Runtime model selection is a session-level choice
handled by mainLoopModelOverride (set by onChangeAppState before this
helper is called); the profile's model list should only change via an
explicit provider edit, not as a side-effect of /model.
This addresses the Copilot and jatmn review threads that flagged the
prior prepend behavior:
- contradicted the comment above the function stating session-level
switching is owned by mainLoopModelOverride
- caused unbounded list growth on rotation (A->B->C->D on a profile
starting with A; B produced D; C; B; A; B)
- used a separator inferred from a single-character substring of
the model field that broke on mixed-separator inputs
- the catalog also used inconsistent id naming
Catalog ids are normalized to the mistral-* scheme (mistral-devstral,
mistral-large, mistral-small, mistral-ministral-3b, mistral-codestral)
to match the existing prefix convention. apiName and modelDescriptorId
are unchanged so route metadata and descriptor lookups are unaffected.
New coverage:
- providerProfiles.test.ts: locks the no-op contract for single- and
multi-model profiles (semicolon and comma separators) and the
in-list pick path that returns the active profile unchanged.
Resolves#1360
`finishLoadingPluginFromPath` supplemented `plugin.hooksConfig` with the
marketplace entry's hooks via object spread:
plugin.hooksConfig = {
...(plugin.hooksConfig || {}),
...(entry.hooks as HooksSettings),
}
`HooksSettings` values are matcher arrays keyed by event name. Object
spread replaced the entire per-event array from `plugin.json` with the
marketplace entry's array, silently dropping any matchers the manifest
already registered for the same event (e.g. both contributed
`PreToolUse` matchers — only the marketplace ones survived).
`mergeHooksSettings` already exists in this file and concatenates
per-event arrays correctly; it is the helper used in
`createPluginFromPath` for the analogous merge. Use it in the
marketplace supplement path too, and export it so the concat-not-replace
contract is locked in by a unit test.
Reorganized and expanded contributing guidelines to reduce fly-by PRs
and align documentation with actual CI/release processes.
Changes:
- Reordered sections so rules and expectations come before local setup
- Added duplicate PR detection requirement to Before You Start
- Added full Pull Requests section with PR description requirements
- Added merge-conflict policy — conflicts block review until resolved
- Added "What Gets Closed Without Review" section covering: duplicate
PRs, bundled unrelated changes, undiscussed scope drift, drive-by
contributions, automated bounty-hunting, and promotional submissions
- Added ban policy for repeated automated/bounty-driven PRs
- Added Project Consistency section on dependency and runtime changes
- Added AI-assisted/vibe-coding guidance with review checklist
- Expanded Provider Changes with doc review requirement and provider
tag assignment restriction
- Updated Validation section to mirror actual CI checks: full check,
test:full (--max-concurrency), provider tests, provider
recommendation tests, Python tests, PR intent scan, typecheck, and
web checks
- Added Code Style section
* Add reasoned permission rejection option
Add an explicit No, provide reason option to file edit, IDE diff, Bash, and PowerShell permission prompts.
Require non-empty feedback for the explicit reason option while preserving the existing Tab-amended No feedback flow.
Extend the shared select component so empty input submissions can be handled by callers without accidentally cancelling or rejecting.
* Stabilize attribution settings tests
Mock the settings module used by the nonced attribution import so CI does not depend on Bun sharing the same settingsCache module instance across fresh imports.
* Fix reasoned reject option typing
* feat(minimax): add MiniMax M3 model with 1M context window
M3 is MiniMax's next-gen flagship with coding/agentic capabilities,
1M token context (1,048,576), and benchmark performance on par with
Opus 4.7 on SWE-bench.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* feat(minimax): add M3 model definition to integrations models
The integrations/models/minimax.ts was missing the minimax-m3
model definition that was added to the vendor config. This
caused the model picker to not show MiniMax M3 as a selectable
option despite it being present in the picker list.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(minimax): show full catalog in /model picker and default to M3
When a MiniMax provider profile was active, the /model picker collapsed
to the single model pinned in the profile (e.g. MiniMax-M2.7), hiding the
rest of the catalog — including M3. mergeActiveProfileModelOptions now
surfaces the complete catalog for native vendor routes (which ship a
curated static catalog), while gateways keep the profile model list as a
whitelist. Added isNativeVendorCatalogRoute() to draw that distinction.
Also harden isMiniMaxProvider() to detect the MiniMax host on
ANTHROPIC_BASE_URL (anthropic-proxy transport), not just OPENAI_BASE_URL,
and switch the vendor defaultModel to MiniMax-M3.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* test: fix 13 cross-file isolation failures in the full suite
These tests passed individually but failed under `bun test` because bun does
not unregister mock.module() overrides on mock.restore(), so stubs leaked
into later files. Root-caused and fixed each leak at the source:
- providerFallback: inject settings/profiles into getProviderFallbackChain
and resolveNextFallbackProviderFromState; the test now uses DI instead of
mock.module on settings.js/providerProfiles.js (fixed 7 attribution fails).
- apiPreconnect: accept an injected apiProvider (defaults to getAPIProvider);
the test passes it explicitly so a leaked providers.js mock can't force
'firstParty' (fixed 3 preconnect fails).
- preflightChecks: build a COMPLETE axios stub (defaults + interceptors) and
re-register the real module in afterEach, so the partial stub no longer
breaks proxy.ts's axios.defaults usage in tests/sdk/query-lifecycle (fixed
2 query-resume fails).
- flagSettings: realpath the temp dir so the macOS /tmp -> /private/tmp
symlink doesn't mismatch the canonicalised --settings path (fixed 1 fail).
Full suite now green: 3343 pass / 0 fail under both `bun test` and
`bun test --max-concurrency=1`.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(minimax): default env-only MiniMax sessions to M3
The descriptor/profile path and --provider minimax already default to
MiniMax-M3, but getDefaultMainLoopModelSetting() still fell back to
MiniMax-M2.7 for env-only sessions (only MINIMAX_API_KEY / a MiniMax base
URL set, no explicit model env). That left part of the default-model
update unapplied. Align the env-only fallback (and the dead applyProviderFlag
fallback) on M3, and update the regression test accordingly. Users who pin
a model via env/profile are unaffected.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* Arch Linux installation instructions
* Include Arch Linux installation instructions for OpenClaude
* Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs: Update installation instructions for Arch Linux
* docs: Clarify Arch Linux installation instructions and AUR usage
* Grammar fix
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: use mistral-vibe-cli-latest as default model for Mistral AI
Change the default model from devstral-latest to mistral-vibe-cli-latest
which has better rate limits and is more current. Keep devstral-latest
in the catalog as an available alternative.
Closes#1181
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: add model descriptor and brand entry for mistral-vibe-cli-latest
The catalog entry referenced a modelDescriptorId that had no matching
descriptor in src/integrations/models/mistral.ts, causing integration
test failures (gateway modelDescriptorId references have model metadata,
registry is valid after loading all descriptors).
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: update DEFAULT_MISTRAL_MODEL and add drift guard test
The /provider interactive setup still defaulted to devstral-latest via
DEFAULT_MISTRAL_MODEL, even though the gateway descriptor points at
mistral-vibe-cli-latest. Update the constant and add a test that
asserts DEFAULT_MISTRAL_MODEL matches the gateway defaultModel to
prevent future drift.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix: resolve TS2769 type error in drift-guard test
GatewayDescriptor.defaultModel is optional, so
mistralGateway.defaultModel is string | undefined which doesn't match
the toBe() overload. Add an explicit definedness assertion and a
non-null assertion on the toBe call so tsc --noEmit is clean.
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* feat(provider): auto-switch on rate limit via providerFallbackChain
Adds the "smallest useful version" described in #768 — when the active
provider returns a rate-limit error and the user has configured an
ordered list of provider profile ids in settings.providerFallbackChain,
swap to the next chain entry and retry the turn instead of bubbling the
error to the UI.
Three pieces:
- New `providerFallbackChain?: string[]` field on the user settings
schema. List of providerProfile ids, ordered by preference.
- New `src/utils/providerFallback.ts` resolver:
- `getProviderFallbackChain()` — read + defensively filter
- `resolveNextFallbackProvider(activeId, chain, profiles)` — pure
function: returns the next chain entry past `activeId`, skipping
chain entries that no longer resolve to a real profile, refusing
to wrap past the last entry (avoids a degraded-network churn
loop), starting from `chain[0]` when the active profile isn't in
the chain (treats the chain as an absolute priority list)
- `resolveNextFallbackProviderFromState()` — convenience over the
pure resolver that reads chain + active from settings/state
- 11 unit tests covering each branch + malformed input
- Query loop recovery branch in `src/query.ts`, sibling to the
existing reactive-compact / context-overflow recovery paths:
- Detect `lastMessage.error === 'rate_limit'` on an isApiErrorMessage
- Skip for compact / session_memory fork query sources — those run
against the same conversation tail the outer turn just committed
to a credential set, switching mid-fork would change credentials
under the parent
- Call `setActiveProviderProfile()` (same path /provider uses;
persists active profile, swaps env vars including
OPENAI_BASE_URL / OPENAI_API_KEY, refreshes startup file)
- Emit an inline `Provider <from> rate-limited — switched to <to>`
assistant message tagged `error: 'rate_limit'` so existing
UI/transcript handling renders it consistently
- One-shot per turn via `state.hasAttemptedProviderFallback`;
reset at next_turn / continuation_nudge / token_budget_continuation
so a fresh user turn can fall back again
- On no-fallback-configured / chain-exhausted / activation-failed,
fall through to the standard rate-limit termination so the user
still sees the original error
Out of scope (separate followups per the issue's "smallest useful
version" framing): a `/switch` slash command, `/provider next` UI, and
quota-vs-burst-429 disambiguation.
Closes#768
* test(providerFallback): mock getSettings_DEPRECATED directly
CI surfaced 3 fails on the settings-cache path: setSessionSettingsCache()
works locally but doesn't survive a fresh `import('?ts=...')` because the
settings module loads its own cache instance on each nonced re-import.
Locally bun reused the cache across re-imports, hiding the issue.
Stub `getSettings_DEPRECATED` / `getInitialSettings` on the mocked
`./settings/settings.js` factory so the resolver sees the test's intended
`providerFallbackChain` regardless of how the settings module's session
cache behaves under fresh imports.
Spreads `...actualSettings` so the rest of the settings surface is
preserved per the 2026-04-30 mock-leak lesson.
* fix(provider-fallback): withhold 429 before fallback retry + fix type import
- Withhold rate-limit assistant errors in the streaming loop when a
providerFallbackChain entry is still resolvable, mirroring the recovery
branch guards (querySource, one-shot). SDK consumers that terminate on
any yielded error no longer see the original 429 before the provider
switch. If activation fails or the chain exhausts, the recovery
branch's fall-through now yields the original message so the user
still gets the error.
- Import ProviderProfile from ./config.js directly — providerProfiles.ts
only imports the type and does not re-export it, so the previous
import failed `tsc --noEmit` even though `bun test` erased it.
Refs #768.
* fix(provider-fallback): emit switch notice as system warning, not API error
jatmn's review (2026-05-19) flagged the remaining gap on PR #1176: the
original 429 was withheld correctly, but the success path then yielded
`createAssistantAPIErrorMessage({ error: 'rate_limit' })` for the
"switched provider, retrying" notice. SDK consumers forward the
`error` field via `normalizeMessage`, so hosts that terminate on
rate-limit assistant errors hit this synthetic notice and never observe
the retry result.
Switch the notice to `createSystemMessage(..., 'warning')`, mirroring
the existing model-fallback recovery branch at `src/query.ts:1048`.
The notice is still visible (warning level surfaces in non-verbose
mode) but no longer carries the `error: 'rate_limit'` SDK tag — only
the chain-exhausted terminal yield does.
* fix(provider-fallback): update in-session model when activating fallback profile
When the rate-limit-driven fallback activates the next provider in
`providerFallbackChain`, the retry was still routing through the original
rate-limited model id. The outer loop re-derives `currentModel` from
`appState.mainLoopModelForSession ?? appState.mainLoopModel`, which still
held the previous provider's model (e.g. a Claude id), and
`resolveProviderRequest` lets that explicit `options.model` win over the
new profile's `OPENAI_MODEL`. The retry then hit the fallback endpoint
with a stale model id and failed immediately.
Mirror the model_fallback branch in the same file: after
`setActiveProviderProfile` succeeds, set `appState.mainLoopModel` to
`getPrimaryModel(activated.model)` (and clear `mainLoopModelForSession`),
then update `toolUseContext.options.mainLoopModel` so the in-progress
turn picks up the new id without re-reading app state.
* feat(nvidia-nim): dynamic model discovery against integrate.api.nvidia.com (#1099)
Mirror the #1143 Groq hybrid-catalog pattern for the NVIDIA NIM
gateway: replace the single-entry static catalog with discovery
against https://integrate.api.nvidia.com/v1/models and a filter
that excludes embedding, retriever, reranker, ASR (whisper,
parakeet, canary, riva), TTS, image-gen (SDXL, flux, stable-diffusion,
kosmos, florence), safety (llama-guard, nemoguard, content-safety),
and reward models so the /model picker only surfaces chat/instruct
ids.
Settings match Groq's:
- catalog.source: hybrid (keep the existing Nemotron 70B as the
static fallback when discovery is unavailable)
- discoveryCacheTtl: 1d
- discoveryRefreshMode: background-if-stale
- allowManualRefresh: true
Adds a focused gateway test (`nvidia-nim.test.ts`) pinning the
filter regex against representative real NVIDIA model ids — keeps,
embedding drops, ASR drops, image-gen drops, safety drops, inactive
drops, plus context_window forwarding — so the filter does not
silently start admitting non-chat models as NVIDIA's catalog grows.
The existing `src/utils/model/nvidiaNimModels.ts` env-var path
(used when users set NVIDIA_NIM or detect via OPENAI_BASE_URL) is
unchanged for now; its hand-rolled list keeps working. Wiring that
path through the discovery service is a separate, larger change.
* fix(nvidia-nim): allowlist chat models + accept discovered ids via /model
Two reviewer-flagged blockers on the dynamic-discovery PR:
1. The existing exclusion regex silently admitted any non-chat model
whose id did not contain one of its keyword tokens, so the live
catalog at integrate.api.nvidia.com was pushing entries like
baai/bge-m3, google/deplot, nvidia/gliner-pii, and
nvidia/ising-calibration-* into the /model picker.
Switch the filter to a positive allowlist keyed on
instruct/chat/reasoning/code markers and known chat families.
Retain a tight non-chat blacklist as a defense-in-depth pass for
the rare instruct-tuned classifier (gliner-pii, deplot, etc.).
2. Inline `/model <id>` rejected ids that only existed in the
discovery cache because validateModel checked the static catalog
only. Add getDiscoveredNvidiaNimModelIds(), which reads the
persisted discovery cache via getDiscoveryCacheKey/getCachedModels
(includeStale: true), and consult it from validateModel before
surfacing the not-found error.
* fix(nvidia-nim): share discovery cache partition with descriptor picker
Addresses @jatmn's [P2] re-review finding on #1177. The inline
`/model <id>` fallback in `getDiscoveredNvidiaNimModelIds` built its
cache key from a hard-coded `process.env.NVIDIA_API_KEY`, but the
descriptor picker / startup discovery path
(`getOpenAIDiscoveryRequestOptions` in `src/commands/model/model.tsx`)
goes through `resolveRouteCredentialValue({ routeId: 'nvidia-nim' })`
— whose credential list for the OpenAI-compatible `nvidia-nim` route
includes both `NVIDIA_API_KEY` *and* `OPENAI_API_KEY`.
Result: a valid NVIDIA setup authenticating via `OPENAI_API_KEY` would
populate the discovery cache under the OpenAI-compatible partition,
while `getDiscoveredNvidiaNimModelIds` looked in a different no-key
partition and rejected the very models the picker had just learned
about.
`getDiscoveredNvidiaNimModelIds` now mirrors the picker's resolution
shape:
- `resolveProviderRequest({ model: OPENAI_MODEL, baseUrl: OPENAI_BASE_URL })`
resolves the active route's effective base URL.
- `resolveRouteCredentialValue({ routeId: 'nvidia-nim', baseUrl, processEnv })`
walks the route's full credential list, so both `NVIDIA_API_KEY` and
`OPENAI_API_KEY` setups land on the same cache partition the picker
wrote.
Behaviour-preserving:
- `getDiscoveryCacheKey` still receives `undefined` for `apiKey` when
no credential is present (matching the picker's pass-through), so the
no-key case keeps the same `apiKeyHash: ''` partition as before — the
failure-mode shape is unchanged for users who never set either env var.
Local: `bun test src/integrations/gateways/nvidia-nim.test.ts` 10/10.
* fix(nvidia-nim): include custom headers in discovery cache key (#1099)
jatmn re-review on #1177 (2026-05-21): the inline `/model <id>`
fallback in `getDiscoveredNvidiaNimModelIds()` was still rebuilding
the discovery cache key with only `(baseUrl, apiKey)`. The descriptor
picker side
(`getOpenAIDiscoveryRequestOptions` in src/commands/model/model.tsx)
passes `headers: parseCustomHeadersEnv(process.env.ANTHROPIC_CUSTOM_HEADERS)`
into `getDiscoveryCacheKey`, so two users sharing a baseUrl + apiKey
but differing in `ANTHROPIC_CUSTOM_HEADERS` ended up on different
cache partitions and the inline validator missed the discovered ids
the picker had just written.
Pass the same parsed custom headers into the inline cache-key build
so both code paths hash the same `(baseUrl, apiKey, headers)` shape.
Add `nvidiaNimModels.test.ts` to pin the partition parity:
- custom headers shift the partition off the no-headers default
- inline validator key equals picker key for the same headers env
- absent `ANTHROPIC_CUSTOM_HEADERS` keeps both keys identical
* fix(promptinput): keep bash-mode `!` out of the local mirror (#1179)
Typing `!` into empty input is meant to enter bash mode and leave the
prompt buffer empty (the `!` shows in the mode prefix only). The
useTextInput special case at the default keystroke handler was
`cursor.insert(text).left()`, which placed `!` into the cursor text
with the offset at 0, then called `onChange("!")`. PromptInput's
`detectModeEntry` then stripped the controlled parent value back to
"" with cursor 0 — values it numerically already held.
Because the parent's controlled props ended up identical to what they
were before the keystroke, React did not re-render PromptInput, the
useLayoutEffect in useTextInput never re-ran, and the local mirror
retained `!` at offset 0. Subsequent keystrokes inserted before the
retained `!`, producing "!git status" with the cursor wedged before
the `!` instead of a clean "git status" buffer.
Fix is in useTextInput's default handler: when the keystroke is the
input-mode character at the start of an empty buffer, emit `onChange`
as a one-shot mode-entry notification but return `undefined` so
`setValue` is not called. The local mirror stays at "" / offset 0,
the parent strip remains a no-op on the controlled state, and the next
character is inserted into a clean buffer.
Test:
- New regression in TextInput.test.tsx that mounts a controlled
TextInput with a parent `onChange` mirroring PromptInput's strip
(return early with `setValue('')` when the new value starts with
`!`), types `!` then `git`, and asserts the rendered frame
contains `git` and does NOT contain `!`, `!git`, or `git!`.
- 4/4 in TextInput.test.tsx, 24/24 across PromptInput + hooks suites,
build clean.
* fix(promptinput): require empty buffer before suppressing bash-mode `!` mirror
The previous condition keyed off cursor.isAtStart(), which is true at
offset 0 even when the buffer is non-empty. If the prompt already
contained 'git status' and the user moved the cursor to the start to
prepend '!', the bang-mode handler emitted onChange('!') and skipped
the cursor.insert, so the parent's strip handler wiped the buffer
back to ''.
Require cursor.text.length === 0 as well so the one-shot onChange
path runs only on a truly empty buffer; otherwise fall through to
cursor.insert(text) and let the parent see '!git status' for normal
prepend behaviour.
* feat: add conversation cache and session persistence
- ConversationCache: LRU cache for conversation history with TTL
- Session persistence with encrypted save/load
- Cross-device sync support
- Integrated into sessionHistory
* fix: address PR review feedback
- Remove broken XOR encryption - store sessions as plain JSON
- Fix key not being persisted issue
- Integrate cacheSession into fetchLatestEvents for actual use
- Remove dead code: no more unused integration functions
- Use proper config directory path
* test: add unit tests for conversationCache and sessionPersistence
- conversationCache.test.ts: 8 tests (LRU, TTL, get/set, delete/clear)
- sessionPersistence.test.ts: 7 tests (create, save/load, list, delete)
* fix: use getClaudeConfigHomeDir for consistent config path
- Replace custom path logic with getClaudeConfigHomeDir() from envUtils
- Ensures consistency with rest of codebase (122 other usages)
* fix: address PR #705 blockers
* fix: fully address PR #705 blockers
1. Remove dead listPersistedSessions (no consumer)
2. Integrate loadCachedSession + cacheSession into fetchLatestEvents
- fetchLatestEvents now checks cache first (loadCachedSession)
- fetchLatestEvents now saves to cache + disk (cacheSession)
3. Add extractSessionId() function for session ID extraction
4. Proper serialization/deserialization with CacheMessage type
* fix: address all non-blocking issues for PR #705
1. Fix O(n) accessOrder - use Map instead of array filtering (O(1))
2. Remove maxMemoryMb - add deprecated function, memory limit not enforced
3. Add test override for session dir - OPENCLAUDE_TEST_SESSIONS_DIR env var
All blockers and non-blockers now addressed.
* fix: address PR #705 remaining blocker
- Add timestamp to CacheMessage for SessionMessage compatibility
- Replace as any with explicit cast for SessionMessage compatibility
- Use serializeToCacheMessage consistently for both cache and persist
* chore: remove PR705 review comment file
* fix: preserve full SDKMessage fields in cache round-trip
- Extend CacheMessage interface with id, type, model, created_at, stop_reason, usage, is_development, index
- serializeToCacheMessage: preserve all relevant fields with type guards
- deserializeFromCacheMessage: restore all preserved fields
- Prevents data corruption on structured message history
* fix: resolve PR 705 blocking issues
- Fix cache-hit returns hasMore:true/firstId:null - now always fetch latest
- Fix deserialize reconstructs structured content from JSON
- Fix extractSessionId uses regex for robustness
- Fix debounce saveSession - only persist on meaningful change (new count)
Fixes reviewer feedback from gnanam1990 and Vasanthdev2004
* fix: use temp test directory in sessionPersistence test
Non-blocking fix: use /tmp/openclaude-test-sessions instead of default
to avoid touching real local state outside CI
* fix: resolve PR 705 remaining blockers
- fetchLatestEvents returns cached immediately for offline/restart support
- Background fetch after returning cached
- cacheSession checks message IDs not just count
- Test uses temp directory
* fix: resolve PR 705 remaining blockers - fetchLatestEvents returns fresh data, fixes firstId
* fix: PR 705 - round-trip content type safety and pagination metadata
Blocking:
- Add contentIsArray flag to track whether content was originally string vs array
- Serializer stores the flag; deserializer uses it instead of heuristic (startsWith '[')
- Prevents corruption of string content like '[]' or '[1,2]' being parsed as JSON
Non-blocking:
- Wire OPENCLAUDE_TEST_SESSIONS_DIR in sessionPersistence.test.ts beforeEach
- Add afterEach to clean up env var
- Store hasMore/lastId metadata in cache, use real values on fallback instead of fabricating hasMore: true
* fix: PR 705 - persist pagination metadata across restarts
- Add pagination field to Session interface for hasMore/lastId
- cacheSession() now saves pagination to persisted session
- loadCachedSession() reconstructs sessionMetadataCache from persisted session
- After restart/offline resume, fetchLatestEvents() returns correct hasMore from saved metadata
* fix: preserve full SDKMessage shape in cache serializer
Add missing type-specific payload fields to serialization/deserialization:
- message (assistant/user/system payload)
- uuid, session_id, parent_tool_use_id, tool_use_result (user messages)
- subtype, result (result/system messages)
- event (stream events)
Previously only role/content were stored, dropping type-specific
payloads needed by convertSDKMessage().
* fix: add error handling to PR intent scan entry point
* fix: persist all SDKMessage variant fields through cache round-trip
- Add error field for SDKAssistantMessage errors (was silently dropping)
- Add errors field for SDKResultMessage error variant (was degrading to 'Unknown error')
- Add status field for SDKStatusMessage ('compacting' was being dropped)
- Add compact_metadata field for SDKCompactBoundaryMessage
- Add tool_name and elapsed_time_seconds fields for SDKToolProgressMessage (was rendering undefined)
- Add 11 regression tests verifying every variant round-trips correctly
Fixes P1: Persisted history still does not round-trip the full SDKMessage union
* fix: persist pagination cursor and use uuid for cache-dirty detection (PR review)
The release workflow ran `bun test` before `bun run build`, but the
bundle regression tests in scripts/missing-module-stub.test.ts read the
shipped dist/cli.mjs. On a fresh release-tag checkout dist/ (gitignored)
does not exist yet, so both tests threw "dist/cli.mjs not found" and
failed the npm publish job. pr-checks.yml already builds first (via
`bun run smoke`); reorder release.yml to match.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* fix(loader): batch markdown reads + cap file size to unblock startup
Unbounded `Promise.all` over a multi-thousand-file directory (e.g., an
Obsidian vault symlinked into `~/.openclaude/agents`) opened that many
file descriptors at once and blocked the event loop on
parseFrontmatter, freezing the REPL during "Initializing/Indexing"
(issue #769). CPU stuck at 100%, Ctrl+C unresponsive.
Two-part fix in `loadMarkdownFiles`:
1. Batch concurrent reads — process 32 at a time. Same pattern as
`listSessionsImpl.ts:READ_BATCH_SIZE`. Caps fd usage; keeps a worker
pool of constant size; event-loop yields between batches.
2. Pre-stat and skip files larger than 256 KB by default. Legitimate
commands/agents/skills/output-styles are small. Oversized files in
these dirs are almost always vault notes dragged in via symlink, and
loading multi-MB markdown blobs into memory is what spikes RSS into
GC pressure. Override with `CLAUDE_CODE_MAX_MARKDOWN_FILE_SIZE_BYTES`
for users who do keep large config files.
Skipped files emit a debug-log warning naming the path + size so users
diagnosing a missing agent know why.
Refs #769
* test(loader): lock batching + size-cap behavior for #769
Three cases that fence the new contract:
- Loads 80 agent files (>2 batches of MARKDOWN_LOAD_BATCH_SIZE=32)
without dropping any. Without batching, this PR'd be the regression
surface; with it, the result must still cover the full file set.
- Skips a 2 KiB file under a 1 KiB cap and keeps the small sibling, so
oversized notes don't silently displace real agents.
- Honors CLAUDE_CODE_MAX_MARKDOWN_FILE_SIZE_BYTES override — users with
legitimately large config files can opt back in.
* fix(loader): warn on stderr + track skipped oversized md files
A debug-only log was the only signal when a user agent/skill/command was
silently dropped for exceeding the size cap, so the symptom looked like
"my custom agent disappeared" with no discoverable cause. Now emit one
stderr warning naming the override env var on the first skip per process
and expose a getOversizedMarkdownSkips() accessor for diagnostics.
Refs #769.
* fix: third-party provider compatibility — update, metrics, and refusal message
Four fixes for third-party provider users:
1. cli/update.ts: Allow self-update for non-Anthropic builds by checking
PACKAGE_URL instead of blocking all non-firstParty providers.
2. utils/autoUpdater.ts: Same fix for assertMinVersion() — allow version
checks for builds with custom PACKAGE_URL.
3. api/metricsOptOut.ts: Gate Anthropic metrics endpoint behind firstParty
check so 3P providers don't hit api.anthropic.com and get auth errors.
4. api/errors.ts: Replace hardcoded 'claude-sonnet-4-20250514' in refusal
message with getDefaultMainLoopModel() for provider-appropriate suggestion.
* fix(api): gate checkMetricsEnabled before disk cache for 3P providers
Address review feedback: stale first-party metrics cache could leak
enabled:true to third-party sessions. Short-circuit non-firstParty
before reading the shared disk cache.
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
* fix(fork): add UserForkBoilerplateMessage, drop unmirrored /fork command
FORK_SUBAGENT ships enabled, which made two missing-module paths live:
1. UserTextMessage renders <UserForkBoilerplateMessage> whenever a user
message contains <fork-boilerplate> (produced by
forkSubagent.buildChildMessage). No source file existed, so the build
stubbed the import to a noop default and the named component was
undefined, crashing the render of any forked-worker message.
2. The /fork slash command required ./commands/fork/index.js, whose source
was never mirrored; its noop .default was spread into the command list,
registering a command with no name/description/call.
Add the component, rendering a compact dimmed marker with just the
directive (parsed off FORK_DIRECTIVE_PREFIX) instead of dumping the verbose
worker rules block into the transcript. Remove the /fork registration:
the implicit-fork machinery (AgentTool/forkSubagent) is present and keeps
working; only the unmirrored slash command is dropped.
Verified: both stubs gone from dist/cli.mjs, the real component is bundled,
smoke passes, and component + commands tests pass.
* docs(fork): align /fork contract with implicit-fork-only behavior
Removing the unmirrored /fork command left two stale contract references:
- forkSubagent.ts claimed '/fork <directive> slash command is available'.
- branch/index.ts dropped /branch's 'fork' alias when FORK_SUBAGENT was on
(on the assumption a dedicated /fork command existed), so the build had
neither the command nor the alias and /fork resolved to nothing.
Restore /branch as the unconditional owner of the 'fork' alias (its
historical pre-FORK_SUBAGENT behavior, honoring the original 'always have a
fork entry point' intent), drop the now-unused feature import, and correct
the forkSubagent doc to state the slash command is not in this build and
forking is implicit.
#1399 already fixed the specifier-collision class by tracking missing
relative imports per importer, which also resolves the WebFetch ssrfGuard
case (the test-file string literal now only stubs the test importer, never
WebFetch). The remaining gap is bundle-level coverage: the existing
security-hardening test reads source only and would pass even if the
shipped CLI bundle had stubbed the guard to a noop.
Rebase onto current main (dropping the now-redundant scanner change) and
add a dist/cli.mjs assertion alongside the /dream regression test: the real
ssrfGuard blocked-address error is present and ssrfGuard is not replaced by
a missing-module stub.
* fix(sandbox): guard annotateStderrWithSandboxFailures against missing runtime method
Fall back to a passthrough when BaseSandboxManager.annotateStderrWithSandboxFailures
is absent, so BashTool no longer throws "is not a function" on every command when the
underlying sandbox-runtime build doesn't provide the method. No behavior change when it
is present.
* fix(sandbox): complete the SDK SandboxManager stubs so they match the CLI's Proxy-noop
The SDK build stubs @anthropic-ai/sandbox-runtime two ways: the native-stub
namespace uses `new Proxy({}, { get: () => noop })` (every access is safe), but
defaultExportOverrides replaces SandboxManager/BaseSandboxManager with hollow
classes that omit annotateStderrWithSandboxFailures. The class form wins in the
SDK bundle, so SDK embedders crash on every Bash command
(`SandboxManager.annotateStderrWithSandboxFailures is not a function`) while the
CLI build — which keeps the Proxy-noop and ships the real native runtime — is
unaffected.
Add a passthrough `annotateStderrWithSandboxFailures` to both stub classes so
they behave like the Proxy form (return stderr unchanged when no real runtime is
present). Combined with the call-site `?? passthrough` guard, the SDK now
degrades gracefully on builds without sandbox-runtime instead of throwing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(mcp-skills): implement MCP skill discovery via skill:// resources
- mcpSkills.ts: fetchMcpSkillsForClient — lists MCP resources, filters skill://
URIs, reads each via resources/read, parses frontmatter, builds skill commands
with loadedFrom/source: 'mcp'. Memoized per server name (LRU, size 20).
- isSkillResource: pure helper to detect skill:// URI scheme
- deriveMcpSkillName: namespaced name builder (mcp__<server>__<name>)
- Enable MCP_SKILLS: true in scripts/build.ts
All call sites, cache-invalidation paths, and consumers were already wired
behind feature('MCP_SKILLS'). Only the module itself was missing. Fixes the
"fetchMcpSkillsForClient is not a function" crash (#856) when the flag was
force-enabled without the module present.
* fix(mcp-skills): discard hooks frontmatter from remote MCP skills
A skill:// resource's hooks frontmatter was carried through the
parseSkillFrontmatterFields spread into the Command, and the slash-command
path registered command.hooks as session hooks on invocation. This let any
connected MCP server install local command hooks that later run shell in the
user's workspace, bypassing the loadedFrom === 'mcp' inline-shell guard by
moving the payload into frontmatter hooks instead of the markdown body.
Discard hooks at the MCP construction site so untrusted remote skills can
never become registrable session hooks.
* fix(mcp-skills): discard allowed-tools frontmatter from remote MCP skills
Like hooks, a skill:// resource's allowed-tools frontmatter flowed through
the parseSkillFrontmatterFields spread into the Command. On the user-typed
slash path (/mcp__server__skill) those tools are written into
alwaysAllowRules.command, so a remote MCP server could auto-approve tool
calls (e.g. Bash) that its own skill body then drives the model to make —
with no permission prompt. The inline-shell guard for loadedFrom === 'mcp'
does not cover this.
Discard allowed-tools at the MCP construction site so remote skills can't
auto-grant tools; the model still prompts on each tool use. The model-invoked
SkillTool path already gates non-empty allowedTools via
skillHasOnlySafeProperties, but the slash path bypasses checkPermissions.
* fix(mcp-skills): skip @-mention attachment scanning for remote MCP skill bodies
A skill:// resource's markdown body flows through getMessagesForPromptSlashCommand
into getAttachmentMessages, which scans for @-mentions and MCP resource refs and
reads them before the model continues. skipSkillDiscovery only gates skill
discovery, not @-mention file reads, so a remote skill could embed @~/.ssh/config
or @.env and exfiltrate local file contents into the conversation with no tool
permission prompt — the same class as the already-stripped hooks/allowed-tools.
Gate the scan input on loadedFrom === 'mcp' (new attachmentScanInputForCommand
helper): the body still reaches the model verbatim, but its @-mentions are no
longer auto-read. Thread-level attachments are unaffected (input=null only gates
the user-input branch in getAttachments).
* Add full access permission mode
Introduce a Full Access mode as a second-level dangerous permission option that bypasses normal confirmation prompts and hard safety-check prompts while still preserving deny decisions.
Wire fullAccess through permission mode types, SDK schemas/types, CLI and REPL control paths, settings, mode cycling, spawned teammate inheritance, prompt speculation, and setup safety checks.
Update permission handling so Full Access skips ask rules, requiresUserInteraction prompts, content-specific ask results, safety-check asks, and hook-forced asks while preserving updatedInput from tool permission checks.
Add a separate Full Access warning acknowledgement and render Full Access selections in red to make the higher-risk mode visually distinct.
Allow the project-local .git/OPENCLAUDE_COMMIT_MSG helper file in dangerous modes for /commit while keeping default mode and other .git paths protected by safety prompts.
Add focused regression tests for Full Access prompt bypass behavior, hook ask handling, commit message file permissions, mode cycling, spawned teammate propagation, and SDK permission mappings.
* fix: restore sdk permission fail-closed behavior
Preserve host canUseTool and onPermissionRequest enforcement in fullAccess instead of short-circuiting around SDK policy callbacks.
Keep the default SDK permission path fail-closed when no host callback is configured, while still allowing interactive tools to surface guidance prompts under fullAccess.
Add focused regression coverage for SDK permission routing and fullAccess user-interaction behavior, plus filesystem coverage for the project-local OPENCLAUDE_COMMIT_MSG path.
* fix: complete full access permission mode integrations
- keep Full Access out of persisted default permission mode settings
- sync Full Access to Claude in Chrome skip-all permission mode
- restore Full Access correctly when exiting plan mode
- add regression coverage for settings, Chrome sync, and plan-mode exit
* test: harden dangerous mode startup flow
* feat: add permission mode management tab
Add a dedicated permission mode tab for switching session modes from the permissions UI.
Keep dangerous modes visible when currently active, route dangerous mode changes through the confirmation dialog without exiting settings, and surface availability errors for auto or bypass modes.
Also add focused tests for permission mode option visibility.
* feat: add full access approval flows
Add fullAccess as an approval option across file, shell, skill, monitor, web fetch, fallback, and plan-exit permission prompts.
Introduce a shared dangerous-mode confirmation hook, wire fullAccess session mode updates through the permission handlers, and gate the new option on dangerous-mode availability.
Also fix the plan-exit follow-up review findings by preserving hook order around the dangerous-mode dialog and restoring Shift+Tab to the explicit accept-edits approval path.
Verified with focused permission tests and Bun module import smoke checks.
* Harden dangerous permission mode boundaries
Tighten fullAccess and bypassPermissions entry paths so elevated mode always respects explicit local confirmation and authoritative org policy gates.
This hardens SDK and bridge activation, Chrome integration, session resume and rewind restoration, team and plan mode transitions, and shared permission update handling. It also keeps session dangerous-mode state in sync and adds focused regression coverage for permission setup, killswitch behavior, conversation recovery, and SDK permission flows.
* refactor: centralize permission mode transitions
Route permission mode changes through shared decision and live-transition helpers so dangerous/full-access confirmation, plan/auto side effects, and mode application stay aligned across CLI, REPL, prompts, and swarm surfaces.
Add a shared UI request hook for resolved dangerous-mode confirmations, persist in-session dangerous-mode acceptance, and remove duplicated request/confirm/apply flows from prompt input, teams, plan exit, and permission settings.
Also fix follow-up correctness issues by validating all setMode updates consistently, applying live permission updates before persisting them, and rebasing those live updates on the latest permission context to avoid partial commits or stale-state overwrites.
* refactor: simplify permission request flows
Centralize permission mode changes behind requestPermissionModeChange and reuse it from the CLI, REPL bridge, inbox poller, and UI callers.
Consolidate duplicated permission request behavior by introducing shared shell and simple permission helpers, routing file permission actions through a shared executor, and unifying remote permission queue-item construction.
Add a shared PermissionScaffold for the common dialog frame, remove redundant shell option/helper modules, and keep focused permission mode transition coverage in permissionSetup tests.
* Enable full access from the permissions UI
Expose bypass/full-access modes in the /permissions picker so dangerous modes can be enabled in-session instead of only via launch flags.
Propagate a session-only bypass-enable signal through the permission mode change flow, preserve the existing dangerous-mode confirmation and policy checks, and keep the session marked as bypass-capable after the user enables one of the dangerous modes.
Also add targeted tests covering picker visibility, local session unlock behavior, and the post-enable session state.
* Refine bypass permissions warning copy
* fix(powershell): anchor commit message .git exception to project root
Align the PowerShell .git write safety exception for
.git/OPENCLAUDE_COMMIT_MSG with the shared filesystem permission rule.
The PowerShell helper was resolving the path from the mutable shell cwd,
which made the bypassPermissions and fullAccess cases order-sensitive in
the full test suite. Resolve the exception from getOriginalCwd() instead
so the temp commit message file is only exempted inside the project root
.git directory while other .git writes still require a safety prompt.
Verified with:
- bun test src/tools/PowerShellTool/powershellPermissions.test.ts --max-concurrency=1
- bun test src/utils/permissions/filesystem.test.ts --max-concurrency=1
- bun test src/tools/PowerShellTool src/utils/permissions --max-concurrency=1
* test: fix dangerous mode prompt suite hang
* Fix monitor permission test isolation
* Harden monitor permission state selector
---------
Co-authored-by: JATMN <12479882+jatmn@users.noreply.github.com>
Co-authored-by: TechBrewBoss <dash@hicap.ai>
* fix(bash): show output for ! shell commands (#1265)
Apply the fix from upstream PR #1270: use raw stdout (with escapeXml)
for normal ! commands, only use processToolResultBlock when output
is persisted or backgrounded. This prevents the model-facing formatter
from silently losing stdout.
Fixes#1265
* fix(bash): address PR review comments
- Remove escapeXml from backgrounded formatter output (trusted XML)
- Force bash routing in tests via mock.module to avoid PowerShellTool
routing on Windows
* chore(bash): clarify formatter output is trusted in both metadata branches
* fix(bash): decode XML entities in user-visible bash stdout/stderr display
* fix(bash): fix unescapeXml entity order and decode in export path
* fix(bash): only unescape stdout/stderr in exports, not bash-input
Scope missing-module stubs for relative imports to the importer file so the unmirrored KAIROS dream skill stub no longer replaces the real /dream command module during bundling.
The in-process teammate runner re-created the progress tracker on every
prompt iteration, so task.progress.tokenCount and toolUseCount were reset
between leader prompts to the same teammate. TeammateSpinnerLine,
InProcessTeammateDetailDialog and the Spinner aggregate all read these
counters directly, which is why agent-team pills appeared to lose tokens
and tool uses partway through a session.
The Claude API returns input_tokens as cumulative-per-request (each turn
re-sends forkContextMessages history), so latestInputTokens already
captures the running context cost. The fix moves createProgressTracker
out of the while-loop so cumulativeOutputTokens and toolUseCount also
keep their running totals across multiple prompts.
Adds src/tasks/LocalAgentTask/progressTracker.test.ts pinning:
- output tokens accumulate across multiple assistant messages
- cumulative semantic survives a simulated multi-prompt teammate session
- fresh-tracker-per-prompt regression repro (prior outputs + tool uses lost)
- tool use count accumulates
- cache_creation/read input tokens fold into latestInputTokens
- recentActivities stays capped while toolUseCount keeps climbing
bun test (full): 2998/2998 pass. bun run build clean.
* feat(provider): add OpenCode Zen/Go subscription support
Add OpenCode as a first-class provider, enabling users to connect their
Zen (pay-as-you-go) and Go ($10/mo) subscriptions via the /provider command.
New integration descriptors:
- vendors/opencode.ts — OpenCode Zen vendor (41 models)
- gateways/opencode-go.ts — OpenCode Go gateway (12 models)
- brands/opencode.ts — brand descriptor
- models/opencode.ts — full model catalog (GPT, Claude, Gemini, Qwen,
GLM, Kimi, MiniMax, Grok, DeepSeek, MiMo, Nemotron)
Modified files:
- integrationArtifacts.generated.ts — register descriptors and presets
- providerProfile.ts — add OPENCODE_API_KEY env/secret key, 'opencode'
profile type, and buildLaunchEnv handler
- providerConfig.ts — add DEFAULT_OPENCODE_BASE_URL constants
Auth: OPENCODE_API_KEY env var or interactive key entry in /provider
Transport: openai-compatible (chat_completions)
Base URLs: https://opencode.ai/zen/v1 (Zen), /zen/go/v1 (Go)
* feat(provider): add [Zen]/[Go] tags to OpenCode preset labels
Add visual tags in the /provider preset selection to distinguish
OpenCode Zen (pay-as-you-go) from OpenCode Go (subscription).
* feat(provider): enable dynamic model discovery for OpenCode
Switch OpenCode vendor and Go gateway from static to hybrid model
catalog with openai-compatible discovery. Models are fetched from
/v1/models on startup and cached for 1 hour. Manual refresh is
supported via the /provider UI.
Static model list is preserved as fallback when discovery fails.
* test(provider): add comprehensive OpenCode Zen/Go test suite
97 tests across 2 files covering:
Integration tests (72 tests):
- Vendor descriptor: id, label, classification, base URL, model, auth,
transport, preset, validation, catalog, discovery, usage metadata
- Gateway descriptor: id, label, vendorId, category, base URL, model,
auth, transport, preset, catalog, discovery
- Brand descriptor: id, label, canonicalVendorId, capabilities, modelIds
- Model catalog: registration, vendor/gateway associations, required
fields, valid classifications, reasoning/coding tags, no duplicates,
model counts (41 Zen, 12 Go), modelDescriptorId consistency
- Cross-reference: brand↔model, vendor↔model, gateway↔model,
shared OPENCODE_API_KEY
- Registry validation: no errors, no preset conflicts
- Edge cases: unique ids, unique apiNames, non-empty labels, valid
contextWindow/maxOutputTokens, valid defaultModel format, validation
message content, discovery config
Profile tests (25 tests):
- Type guard: isProviderProfile('opencode'), rejects invalid values
- buildLaunchEnv: persisted env, defaults, process env precedence,
OPENCODE_API_KEY mapping, whitespace/null/undefined/empty handling,
very long keys, special characters, concurrent access, boundary
values, no credential leakage
* fix(provider): add per-model endpoint routing (P1)
Add endpointPath field to OpenAIShimTransportConfig so catalog entries
can specify which API path to use per model. This addresses the
maintainer's [P1] finding that all models were routed to
/chat/completions regardless of their upstream endpoint.
Changes:
- descriptors.ts: add endpointPath?: string to OpenAIShimTransportConfig
- openaiShim.ts: buildRequestUrl checks shimConfig.endpointPath first
- vendors/opencode.ts: add transportOverrides to 31 catalog entries
(GPT→/responses, Claude/Qwen→/messages, Gemini→/models/<id>)
+ switch to source: 'static' to prevent free models from live API
- gateways/opencode-go.ts: add transportOverrides to 4 entries
(MiniMax/Qwen→/messages) + switch to source: 'static'
- opencode.test.ts: update tests for static source, remove discovery tests
* refactor(opencode): model OpenCode Zen/Go as gateways (P2)
* docs(provider): document OpenCode setup and move badge metadata to descriptors
- Add OpenCode Zen/Go rows to README supported providers table
- Add OpenCode Zen/Go examples and OPENCODE_API_KEY to advanced-setup.md
- Add PresetBadge type to descriptor/manifest with badge propagation in
artifact generator
- Move 4 hard-coded preset badges ([FREE], [Sponsor], [Zen], [Go]) from
ProviderManager.tsx into descriptor preset metadata
- Add badge field to providerUiMetadata so UI components read from manifest
- Update integration overview docs to recommend preset.badge for future
gateways
* fix(provider): match request body to endpoint format for OpenCode /messages and /responses (P1)
Extend the openaiShim transport so that endpointPath overrides select
both the URL and the correct body/response format:
- /responses → OpenAI Responses API body (input, max_output_tokens)
- /messages → Anthropic Messages API body (content blocks, system, max_tokens)
Also fixes: abort listener leak in SSE passthrough, system prompt
content-block flattening, and removes [Zen]/[Go] badge entries (P3).
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
* fix(provider): add Google AI SDK body/response format for OpenCode Zen Gemini models (P1)
The three Gemini models in the OpenCode Zen catalog (gemini-3.5-flash,
gemini-3.1-pro, gemini-3-flash) were sending chat-completions body to
the /models/gemini-* endpoint, which expects Google AI SDK format.
- effectiveTransport now detects /models/gemini- endpointPath → 'gemini'
- buildGeminiBody() converts Anthropic messages → Google contents[]
with role mapping, systemInstruction, generationConfig, functionDeclarations
- geminiSseToAnthropic() parses Google SSE frames → Anthropic stream events
with text deltas, functionCall tool_use, finishReason mapping
- _convertGeminiToAnthropicResponse() for non-streaming responses
- Streaming/non-streaming routing via URL detection (/models/gemini-)
- serializeBody(), hasToolsPayload, omitGeminiTools all updated
* fix: prevent OpenCode model descriptors from shadowing canonical limits
P1: Prefix all defaultModel values in opencode.ts with 'opencode-'
so the fallback findModelDescriptorForApiName() doesn't match
canonical model names. The OpenCode descriptors are still found
via catalog entry lookup when the OpenCode route is active.
P2: Add 'OpenCode Go' and 'OpenCode Zen' to PRESET_ORDER in
ProviderManager.test.tsx between 'OpenAI' and 'OpenRouter'
so navigateToPreset() sends the correct number of j keypresses.
* fix: align OpenCode Go descriptor metadata with Zen
- category: 'hosted' → 'aggregating' (both are aggregating gateways)
- add validation block with OPENCODE_API_KEY guidance
- update test assertion from 'hosted' to 'aggregating'
* fix: accept OPENAI_API_KEY as fallback in OpenCode validation
When users set up OpenCode Zen/Go via /provider, the key is saved as
OPENAI_API_KEY (via buildCompatibilityProcessEnv). The validation block
only checked OPENCODE_API_KEY, causing a startup warning even though
the runtime auth header had the key it needed.
Add OPENAI_API_KEY to validation.credentialEnvVars for both gateways,
matching the pattern used by Hicap and Gitlawb Opengateway.
* chore: trigger mergeability recheck
---------
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>