The OpenAI-compatible client only treated a base URL ending in "/v1" as
already-versioned; any other base got "/v1/chat/completions" appended.
Z.AI's coding-plan base ends in "/paas/v4", so requests were sent to
".../paas/v4/v1/chat/completions" — a path that does not exist — and
every GLM model returned 404.
Match any trailing "/vN" version segment instead. This is behaviour-
identical for all existing providers (their versioned bases all end in
"/v1") and only changes Z.AI, which now hits ".../paas/v4/chat/completions".
In flat (non-recursive) mode, typing a filter to locate a directory and
then opening it with Right re-applied that same filter inside the
directory. Typing "@eda" then Right to open eda/ showed nothing,
because no child of eda/ matches "eda". The filter the user typed
selected the directory at the current level; it has no meaning one
level deeper.
Clear the text after the last "@" (keeping the bare "@" so the picker
stays open) whenever Right or Left successfully changes the browse
level. The filter was scoped to the level just left, so dropping it
shows the new directory's full contents.
Adds a regression test that opens eda/ after an "@eda" filter and
asserts the directory's contents are listed while the stale filter
would have matched nothing.
The recursive @-picker only read the repo's root .gitignore, so a
nested .gitignore (e.g. .opencode/.gitignore ignoring its own
node_modules) was invisible. WalkDir visits lexically, so a
dot-prefixed vendored tree got walked first and its node_modules
flooded the 5000-entry budget before the walk ever reached deeply
nested source files. The picker then fuzzy-matched against junk and
never surfaced the real target.
- Add ignore.Stack: a per-directory .gitignore chain pushed/popped as
the recursive walk descends, with git-style nearest-file-wins
semantics including nested negations. scanRecursive now prunes
nested-ignored trees like node_modules.
- Raise maxRecursiveEntries 5000 -> 50000 and maxRecursiveDepth
12 -> 24. The bottleneck is per-keystroke fuzzy.Find, not memory:
a fileEntry is ~120 bytes (~6 MB at 50k), and benchmarked
fuzzy.Find latency is ~2ms @ 5k, ~13ms @ 50k, ~21ms @ 100k, so 50k
keeps ranking under one 60Hz frame while holding a large monorepo
once nested-gitignore pruning has done its job.
Verified against the reporting monorepo: the fully-pruned tree is
4397 entries (node_modules=0), scan ~360ms once (cached after),
match ~2.5ms per keystroke, and @pipeline.py now finds
eda/rjg/enk-1150/pipeline.py.
Adds regression tests at both the ignore.Stack layer and the
file_suggest layer, including a repro of the nested-node_modules +
deep-file scenario.
Four entries (bare, us., eu., global.) with 1M context, 128k output,
adaptive thinking, and Bedrock pricing (10/50, cache 1/12.5). The bare
id resolves through the cross-region inference profile logic like the
other anthropic.claude- models. Remove once Bedrock model discovery
picks the id up. Note: the Bedrock Converse client has no thinking-mode
wiring yet, so AdaptiveThinking is informational on this route for now.
Previously gitignore filtering ran only in recursive mode; the default
flat directory browse showed .git/, node_modules/, etc. Apply it in
both modes and make it user-controllable.
- Flat scan() now also skips .git and gitignored entries.
- New respectGitignore flag on the suggester (default on), persisted as
respect_gitignore in config.json, surfaced as a /settings checkbox,
and plumbed through SettingsStore/InteractiveConfig/cli. Toggling
flips the picker live.
- .git is always pruned in recursive mode regardless of the toggle, to
protect the entry budget.
- Tests for flat-mode filtering and the toggle across both modes.
Replace the static recursiveSkipDirs list (which would inevitably drift
as new tools appear) with the project's root .gitignore. Most caches
that bloat a recursive walk \u2014 build outputs, dependency dirs, and IaC
caches like .terraform/.terragrunt-cache \u2014 are already gitignored in
real projects.
- Extract the existing .gitignore matcher from agent/extcmd.go into a
new leaf package, packages/ignore, so packages/agent/modes can share
it without an import cycle. extcmd keeps thin aliases for its tests.
- scanRecursive now loads the root .gitignore and prunes ignored
entries, plus an unconditional .git skip (rarely self-listed).
- Tests: gitignore-driven pruning in the picker, plus unit tests for
the extracted matcher.
No new dependencies.
Add Terraform/Terragrunt/Pulumi/Serverless/CDK provider and module
caches to the recursive walk skip list. These hold copies of
downloaded providers and generated module trees that would otherwise
dominate the entry budget with non-source files.
The @-mention file picker previously did a plain case-insensitive
substring match within a single directory, only reachable nesting via
arrow-key drill-down.
- Rank matches with sahilm/fuzzy (pinned v0.1.1 to avoid the go 1.24.5
directive in v0.1.2, which would exceed CI's Go 1.23).
- Add a recursive mode that walks the whole project tree below cwd,
matching cwd-relative paths (e.g. @foobar finds src/foo/bar.go),
skipping heavy dirs (.git, node_modules, ...) and bounded by entry
and depth caps. Arrow drill-down is disabled in this mode.
- Persist as recursive_file_suggest in config.json, surfaced as a
/settings checkbox, plumbed through SettingsStore/InteractiveConfig/
cli. Toggling live flips the picker without a restart.
- Tests for fuzzy ranking, recursive cross-dir match, heavy-dir
pruning, and cache reset on toggle.
Speculative Anthropic entry (1M context, 128k output, adaptive thinking,
10/50 pricing) so the model resolves on both the api-key and OAuth route
with correct cost tracking and thinking mode. AdaptiveThinking cannot be
expressed via models.json, hence the catalog entry. Remove once the id
is live and discoverable upstream.
Fixes OpenRouter rejecting requests where max_tokens equals the served
context window. Prefer top_provider.context_length on discovery and clamp
max_tokens to ContextWindow minus a proportional reserve (window/8 capped
at 4096). Reworked from the original PR so the reserve derives from the
window, not MaxOutput: models whose output already fits are untouched and
small-window models are not over-penalized.
Co-authored-by: Neil-urk12 <neil-urk12@users.noreply.github.com>
OpenRouter enforces input + max_output <= served context_length and
rejects requests where max_tokens equals the whole window, which happens
for models whose catalog MaxOutput is set equal to ContextWindow (e.g.
nemotron-3-super-120B). Two parts:
- discover.go (from #24): prefer top_provider.context_length when it is
smaller than the inflated model-level context_length, so ContextWindow
reflects the limit OpenRouter actually serves.
- openai.go: clamp max_tokens to ContextWindow minus a reserve. The
reserve is derived from the window (window/8, capped at 4096), never
from MaxOutput, so models whose output already fits the window are
untouched and small-window models (gpt-4) are not over-penalized.
Adds buildRequest clamp tests (fits-window no-op, large-window cap,
small-window proportional reserve, floor, explicit-request passthrough)
and an httptest-based DiscoverOpenRouter test for the served-context
preference.
Co-authored-by: Neil-urk12 <neil-urk12@users.noreply.github.com>
Thread the resolved model's catalog MaxOutput through to
provider.Request.MaxTokens so each turn requests the model's full output
capacity. Fixes Bedrock silently truncating long writes/edits at its
4096 default (stopReason=length). Other providers already defaulted to
MaxOutput on a zero request, so this is a no-op for them. Also surfaces
StopLength explicitly in the TUI instead of ending silently.
Co-authored-by: Raymond Gasper <raymondgasper@fastmail.com>
Turns omitted MaxTokens on the provider request, so Bedrock applied its
conservative 4096 default and silently truncated long writes/edits with
stopReason=length. In the TUI this read like the interaction timed out.
Thread the resolved model's catalog MaxOutput through to the request:
catalog Model.MaxOutput -> Resolved.MaxOutput -> Agent.MaxTokens
-> provider.Request.MaxTokens
Zero still falls back to each provider's own default, so models without a
catalog MaxOutput are unaffected. The SDK path inherits this via NewAgent.
Also surface StopLength explicitly in the TUI ('response hit the output
limit -- ask it to continue') instead of ending silently.
Tests: TestAgentPropagatesMaxTokens (Agent.MaxTokens reaches the wire) and
TestBedrockBuildRequestMaxTokens (non-zero flows through; zero -> 4096).
- it currently rejects requests where input + max_output exceeds the
serving provider's context lmit (which may be tighter than the
model-level value)
- use the smaller of ContextWindow and MaxOutput as the cap, with a
4096-token input reserve
Run repairToolUseResultPairs on outbound messages in oneTurn so an
in-process aborted turn (cancel, connection drop, ECONNREFUSED) no
longer leaves a dangling tool_use that gets rejected by Anthropic/OpenAI
on the next request. Pure and idempotent, no-op on valid transcripts.
A turn aborted mid-flight (cancel, connection drop, dev-server
ECONNREFUSED) can leave an assistant tool_use block with no matching
tool_result in the live transcript. repairToolUseResultPairs already
fixes this, but only ran in OpenSession (load time), so an in-process
abort left the transcript broken until restart. The next request was
then rejected by Anthropic/OpenAI with 'tool_use ids were found without
tool_result blocks'.
Run the same repair on the outbound messages in oneTurn. It is pure and
a no-op on valid transcripts, so there is no hot-path cost beyond a
single linear scan and no behavior change for healthy sessions.
Allow extensions to open panels outside slash-command responses, enabling
human-in-the-loop tool gates and secret/input collection patterns. Removes
the review-only spontaneous panel plan before merge.
Co-authored-by: Raymond Gasper <raymondgasper@fastmail.com>
- Footer now shows '● this panel has focus' so users know keypresses
are going to the panel, not the editor
- Prompt line uses '› approve this action? [y/n]' cursor glyph
- Unhandled keys re-render with '› unrecognised key — press y or n'
instead of silently swallowing the input
- docs/extensions.md: add open_panel spontaneous frame section with
blocking tool pattern explanation, concurrent-panel note, and
references to new examples; add approve/secret to See also list;
add roadmap entry
- examples/extensions/approve/: approve_action tool — opens a y/n
panel from inside the tool handler, blocks until user responds
- examples/extensions/secret/: fetch_with_password tool — masked
password input panel, secret never leaves the extension process
Allow extensions to emit an open_panel frame at any time, not just as
the action of a command_response. This makes it possible to build
approval gates, secret collection, and freeform user-input prompts
directly inside tool handlers.
Changes:
- extproto: add OpenPanelFromExt wire type
- extensions/manager: route spontaneous open_panel frames to hooks.OpenPanel
- ext/ext.go: add Extension.OpenPanel() SDK method
- tests: TestSpontaneousOpenPanel (manager), TestOpenPanelEmitsCorrectFrame,
TestBlockingToolWaitsForPanelKey, TestBlockingToolDenied (SDK)
- docs/plans: add spontaneous-panel.md design doc
The blocking tool pattern (open panel → block on channel → key event →
tool_result) requires no additional wire changes; it falls out of
standard Go concurrency on the extension side.
Part 3 (intercept timeout for built-in tool gating) is out of scope
and tracked separately.
Inject a stub toolConfig when the message history contains toolUse or
toolResult blocks but req.Tools is empty (e.g. the /btw side-chat sends
the frozen main transcript). Bedrock's Converse API otherwise rejects
the request with HTTP 400. Bedrock-only; other providers unaffected.
Co-authored-by: Raymond Gasper <raymondgasper@fastmail.com>
Bedrock's Converse API returns HTTP 400 with "toolConfig field must be
defined when using toolUse and toolResult content blocks" whenever the
message history contains toolUse or toolResult blocks but toolConfig is
absent from the request.
The /btw side-chat sends the frozen main transcript as context with no
tools defined. If the main conversation included tool calls the serialised
messages will contain toolUse/toolResult blocks, triggering the 400.
Fix: add bedrockMessagesHaveToolBlocks() to detect this case and, when
req.Tools is empty but tool blocks are present in the history, inject a
minimal stub toolConfig with an inert placeholder tool. Bedrock accepts
the request and the stub can never be invoked since no tool_use stop
reason can fire when the advertised tool list is effectively empty.
Adds Converse API cachePoint blocks at the system prompt boundary and on
the last user message for Bedrock Claude models (PriceCacheWrite > 0),
mirroring the Anthropic provider's caching strategy. Nova models are
excluded (automatic caching).
Co-authored-by: Raymond Gasper <raymondgasper@fastmail.com>
Place Bedrock Converse API cachePoint blocks at the system prompt
boundary and after the last user message on every request to Claude
models (those with PriceCacheWrite > 0 in the catalog).
This mirrors the existing Anthropic provider strategy (cache_control:
ephemeral on system, tools, and last user message) using Bedrock's
equivalent syntax: a {"cachePoint":{"type":"default"}} content block
appended to the relevant arrays.
Changes:
- bedrockRequest.System widened from []map[string]string to
[]map[string]interface{} to accommodate mixed text/cachePoint blocks
- bedrockCachePoint: shared sentinel content block var
- bedrockModelSupportsCaching: gates on PriceCacheWrite > 0; strips
geo prefixes before catalog lookup; falls back to anthropic.claude-
prefix check for unknown models (cachePoint is silently ignored by
the API if unsupported)
- buildRequest: resolves model ID before caching check; injects
cachePoint into system array and calls bedrockTagLastUserCache
- bedrockTagLastUserCache: appends cachePoint to last user message
Nova models (PriceCacheWrite == 0) are excluded — they use Bedrock's
automatic caching and don't need explicit markers.
Tests: 8 new cases covering model detection, Claude vs Nova presence/
absence, multi-turn last-message targeting, no-system safety,
nil/empty panic safety, and JSON wire shape.