* feat(mcp): support MCP resources and reliably surface prompts
Prompts were silently hidden for MCP servers that implement `prompts/list`
but under-declare the `prompts` capability in their initialize response.
Drop the capability gate in `listMcpPrompts` (and apply the same leniency to
resources): always attempt the list call and swallow `Method not found`, so
those prompts now appear as `/` slash commands like in other clients.
Add first-class MCP resource support:
- core: `listMcpResources` / `discoverResources`, a `DiscoveredMCPResource`
type, a `ResourceRegistry`, and `Config.getResourceRegistry()`. Discovery
is wired into the standalone `McpClient.discover()` path and the
connection-pool path (snapshot -> `SessionMcpView.applyResources` -> the
session's registry, with a `resourcesChanged` event on reconnect). A
resource-only server now counts as a successful discovery.
- cli: the `/mcp` dialog shows per-server Resources (and Prompts) counts.
- cli: `@server:uri` reads an MCP resource and injects its contents into the
message (text inline, blobs as inlineData); `@server:` autocompletes the
server's resource URIs. The `server` prefix must match a configured MCP
server, so existing `@path` file references are unaffected.
Docs updated; unit tests added across core and cli.
* fix(mcp): reload commands on discovery + harden resource ref parsing
Address review feedback and complete the prompt UX:
- Slash commands now rebuild when an MCP server finishes connecting.
Discovery is progressive (runs after the UI is interactive), so prompts
a server exposes via prompts/list were registered too late to appear in
the `/` menu — the `/mcp` dialog showed the count while the slash menu
stayed empty. A debounced MCP-status listener now reloads the command
tree on connect. Verified end-to-end in a real TUI against a mock server
that under-declares the prompts capability: `/greet` now lists.
- `isMethodNotFound` keys off the JSON-RPC `-32601` code rather than the
server-supplied message text (which may be localized or worded
differently); applied to both `listMcpPrompts` and `listMcpResources`.
- `useAtCompletion` uses `Object.hasOwn` so `@__proto__:` / `@constructor:`
and other inherited keys are not mistaken for configured servers.
* fix(mcp): lenient resource read to match discovery + review polish
- `McpClient.readResource` no longer prechecks `getServerCapabilities()
?.resources`. This PR is the first caller to make the read path
reachable, and the strict precheck meant a server that answers
`resources/read` but under-declares the `resources` capability — exactly
the servers the lenient `listMcpResources` discovery targets — would get
its resources discovered, listed in `/mcp`, and autocompleted, yet fail
every `@server:uri` with a misleading "does not support resources". The
read now matches discovery; a server that truly lacks resources answers
`-32601`, surfaced as the existing error card. Added a regression test.
- `@server:uri` success cards now report what was injected ("Injected N
chars" / "N attachments") or "(no readable content)" when a read yields
no text/blob parts, so a partially-empty multi-ref read isn't hidden.
- `useAtCompletion` resource filter reduced to a single `includes` match
(`startsWith` was subsumed; empty partial matches all via `includes('')`);
the overstated "prefers prefix" comment is corrected.
- Tests: mixed `@file` + `@server:uri` injection (both parts + both cards),
empty-content card, and the pool restart fan-out now asserts
`applyResources` alongside `applyTools`.
* perf+security(mcp): parallelize discovery/reads, cap & frame resources
Review round 3 fold-ins:
- Discovery now runs `listMcpPrompts` / `listMcpResources` / `discoverTools`
concurrently in `discoverAndReturn` (independent reads; the SDK client
multiplexes by JSON-RPC id), saving per-server round-trips at startup.
- `@server:uri` resource reads run in parallel (`Promise.allSettled`) instead
of serially, matching how the file path batches via `readManyFiles`; order
is preserved so cards/labels line up with refs.
- Resource injection is now capped and framed: text is bounded by
`MAX_MCP_RESOURCE_TEXT_CHARS` (100k) and oversized blobs are skipped, so a
misbehaving/hostile server can't overflow the context window or OOM; the
content is fenced with `--- Content from MCP resource <label> ---` /
`--- End ---` delimiters so the model can separate untrusted server output
from the user's prompt. The success card reports truncation.
- File-read error path now merges `resourceLabels` into `filesRead` /
recording, so a resource read that succeeded before a file read failed is
not dropped from the audit trail.
- `isMethodNotFound` (JSON-RPC -32601) now also covers `discoverTools` and
`invokeMcpPrompt`, replacing the remaining message-substring checks.
- `PoolEntry.markActive` `initialResources` is now required (no `= []`
default), removing a footgun where an omitted arg would wipe a server's
resources via `applyResources([])`.
- `useAtCompletion` resource suggestions rank prefix matches above mid-string
matches.
- `'Resources:'` added to the remaining 6 locales (ca, de, fr, ja, pt, ru)
for parity with `'Prompts:'`.
Tests: attribution framing, text truncation, completion prefix ranking.
* test(mcp): cover resource registration in discover() and resource-only discovery
Closes the two coverage gaps flagged in review: assert discover() registers
discovered resources into the Config ResourceRegistry, and that a resource-only
server (no tools/prompts) is a successful discovery rather than throwing.
* fix(mcp): idempotent resource re-discovery + cumulative blob cap
Review round 4 (all Suggestions):
- discover() now clears a server's resources (removeResourcesByServer) before
re-registering, so reconnect / incremental re-discovery is idempotent and a
resource the server dropped doesn't linger in the registry (matches the
pool path's SessionMcpView.applyResources).
- @server:uri injection now caps CUMULATIVE blob size per resource, not just
each blob, so many sub-limit blobs in one response can't inject unbounded
data. Added a test for the oversized-blob skip + card.
- Documented that file/resource content parts are grouped by type (model
correlates by delimiter labels, not position).
* test(mcp): cover the MCP-status command reload (prompts surfacing in /)
Adds the missing coverage for the discovery-driven reload: a CONNECTED status
fires the listener and rebuilds the command tree (so progressively-discovered
MCP prompts appear as / commands), and a non-CONNECTED status does not.
* fix(mcp): don't wipe resources when resources/list transiently fails
- discover() only clears + replaces a server's resources when listMcpResources
returns a non-empty set. Because that helper swallows all errors (including
transient network failures) and returns [], an unconditional clear-then-
register would silently purge a server's resources on a transient list
failure while tools/prompts succeed. Guarding on length>0 keeps the existing
set on failure; a real partial drop still re-registers the fresh set.
- Resource success card now shows '(truncated)' for capped/skipped blobs too,
not just text. Added a cumulative-blob-cap test (two sub-limit blobs whose
sum exceeds the cap).
* fix(mcp): guard pool applyResources against transient-failure wipe too
The non-pool discover() guard (resources.length > 0) left the pool path
exposed: on a restart, doRestart -> discoverAndReturn swallows a transient
resources/list failure to [], and applyResources([]) then wiped the session's
resources. applyResources is now a no-op on an empty snapshot (mirrors the
discover() guard; applyTools/applyPrompts keep their pre-existing clear-on-empty
behavior, out of scope). Added tests: applyResources([]) does not clear, and
discover() with an empty resource list does not call removeResourcesByServer.
* fix(mcp): preserve pool resource snapshot on transient restart failure
The applyResources([]) no-op only protected already-attached sessions; doRestart
still overwrote the pool entry's resourcesSnapshot with [] when the restart's
resources/list transiently failed, so any session attaching AFTER the restart
got zero resources. doRestart now only updates resourcesSnapshot when the
re-read is non-empty, preserving it for new and existing subscribers alike.
Tests: applyResources([]) preserves a pre-populated set; a restart whose
resources/list comes back empty still serves the prior resource to a new
session.
* fix(mcp): trust-gate resource completion, colon server names, narrower method-not-found
- useAtCompletion no longer surfaces resource URIs in an untrusted folder
(the read path is already blocked there); avoids leaking resource existence.
- parseMcpResourceRef / getMcpResourceSuggestions match the LONGEST configured
server name as a '<name>:' prefix instead of splitting on the first colon,
so a server whose name contains ':' (a valid settings.json key) resolves.
- isMethodNotFound's message fallback is back to the case-sensitive exact
'Method not found' substring (the -32601 code is the primary check), not a
broad /method not found/i that would swallow unrelated errors.
Tests: @my:server:uri resolution, untrusted-folder completion.
* refactor(mcp): extract shared longest-prefix server matcher + doc/test fixes
- Extract matchMcpServerPrefix (new mcpResourceRef.ts) and use it from both
parseMcpResourceRef (injection) and getMcpResourceSuggestions (completion),
removing the duplicated longest-prefix logic and its drift risk.
- Update parseMcpResourceRef JSDoc to describe longest-prefix matching.
- Tests: shared-helper unit tests; the @my:server colon test now configures
both 'my' and 'my:server' to exercise disambiguation; a colon completion
test; isMethodNotFound message-casing tests (exact 'Method not found'
swallowed, 'method not found handler' not swallowed).
* fix(cli): allow dotfile paths in Web Shell sendFile
res.sendFile() uses the send library which defaults to dotfiles:'ignore',
returning 404 for any path containing a segment starting with '.'. Users
who installed qwen via nvm/volta/asdf have the package under
~/.nvm/.../web-shell/index.html, causing 'Failed to load Web Shell' in
the browser. Pass dotfiles:'allow' to fix this.
* test(cli): add dotfile-path regression test for Web Shell sendFile
Per review suggestion on PR #5541: existing tests use temp dirs without
dot-prefixed segments, so the send library's dotfiles:'ignore' default
was never exercised. Add a test that nests webShellDir inside a
.fake-nvm parent to guard against future regression.
* feat(ci): add on-demand tmux real-user testing to triage workflow
Add a tmux-testing stage to qwen-triage.yml: a write-permission user runs
`@qwen-code /tmux` on a PR (or dispatches with tmux_pr=N) to launch the
changed app in a real tmux TUI session and exercise the affected flow,
catching interactive regressions that diff-based review can't.
- Gated on the PR AUTHOR having write permission (whose code runs), reusing
the authorize job; no separate fork check.
- Executes untrusted PR code with minimal blast radius: contents:read, no
GitHub token in the agent env, persist-credentials:false.
- Proxy bypass for the model call only.
- A separate publish-tmux job (clean runner, write PAT, no PR-code checkout)
posts the verdict back to the PR — keeping the write credential isolated
from the code execution.
* fix(ci): address tmux-testing review findings
- Gate the workflow_dispatch tmux_pr path on the PR author's write
permission via the authorize job, so dispatching never runs an
unauthorized contributor's code on the self-hosted runner.
- Make workflow_dispatch triage vs tmux mutually exclusive (tmux_pr
set skips the triage job), matching the documented input contract.
- Pin GITHUB_TOKEN/GH_TOKEN empty in the untrusted-code step so no
inherited repo-scoped credential reaches the agent.
- Serialize concurrent tmux runs per PR; skip PRs whose merge ref is
unavailable (CONFLICTING); clean the runner workspace on exit.
- publish-tmux now also reports infrastructure failures instead of
staying silent, and the comment fence widens past any backtick run
in untrusted output so it cannot break out and render markdown.
- Correct the proxy-bypass comment to describe actual behavior.
* refactor(ci): collapse tmux gating into one decision output
- Fold should_test + has_tui into a single tri-state `decision`
(skip | na | run); the five repeated step guards become
`decision == 'run'`, and the standalone `Mark not applicable`
step is gone (the n/a verdict is set inline in the resolve step).
- Resolve PR state + file list in one `gh pr view` call instead of
two.
No behavior change: skip stays silent, na posts n/a, run drives the app.
* fix(ci): wait out UNKNOWN mergeability before deciding
Right after a /tmux comment GitHub may not have computed PR
mergeability yet (mergeable=UNKNOWN), and refs/pull/N/merge is only
current once it has. Retry the resolve up to 5x (3s apart) until it
settles, so a freshly-pushed PR isn't checked out from a stale or
missing merge ref.
* fix(ci): harden tmux publish comment and mergeability/exit-code handling
- publish-tmux: render artifacts in HTML-escaped <pre> instead of a
backtick fence; removes the grep crash on backtick-free content under
set -euo pipefail and closes the </details>/fence-breakout injection
- tmux-testing: skip when mergeability stays UNKNOWN after retries
- classify exit 137/139 (OOM/segfault) as infra-error, not test fail
- upload-artifact: continue-on-error so a pre-write crash isn't masked
* ci: harden tmux testing workflow
* fix(ci): address tmux testing review follow-ups
* fix(ci): report tmux prepare failures
* fix(ci): use latest qwen CLI for tmux testing
* perf(core): read current git branch directly from .git instead of spawning git
Reading the current branch for the CLI status line previously shelled out to `git rev-parse --abbrev-ref HEAD` on the render path. Replace it with a direct read of .git/HEAD.
- Add shared module packages/core/src/utils/gitDirect.ts: resolveBranchName, readGitHead, a reference-counted reflog watcher (watchRepoBranch), ref-name validation (isValidRefName), and a gitDir resolution cache. gitDir resolution reuses the existing gitDiff.resolveGitDir (ancestor walk + worktree gitdir pointer); only HEAD is re-read on reflog changes.
- Rewrite useGitBranchName as a thin wrapper over the shared module; the (cwd) => string | undefined hook signature is unchanged.
* fix(core): handle fs.FSWatcher 'error' and don't cache a watcher-less entry
Addresses review feedback on the reflog watcher:
- Add an 'error' handler to the reflog fs.FSWatcher. fs.FSWatcher is an EventEmitter, so an unhandled 'error' (reflog removed by `git gc`/`reflog expire`, worktree removal, inode change, or a platform watch limit) would crash the process. The watch is now torn down instead, and subscribers simply stop auto-refreshing.
- When there is no reflog yet (unborn repo), return a no-op disposer without caching a watcher-less entry, so a later caller can establish the watch once the reflog appears (e.g. after the first commit).
- Tests: FSWatcher 'error' teardown, reflog-appears-later re-watch, concurrent-caller dedup (post-await re-check), and the hook's [cwd]-change re-subscribe path.
* fix(core): harden gitDirect against out-of-repo reads and fs.watch throws
Addresses security/robustness review:
- Containment guard: resolveTrustedGitDir rejects a .git-FILE `gitdir:` pointer that escapes the repo. After realpath, the resolved gitDir must be the repo's own `<root>/.git`, or live under some `.git/worktrees/` (linked worktree) or `.git/modules/` (submodule). This stops a crafted project from making the status line read/watch an arbitrary out-of-repo path (the old `git rev-parse` path refused such repos with exit 128).
- Refuse a symlinked HEAD (readGitHead) and a symlinked reflog (watchRepoBranch) via lstat, so neither follows a link out of the repo.
- Wrap fs.watch in try/catch: a TOCTOU vanish (git gc / reflog expire / worktree removal) or a platform watch limit makes fs.watch throw synchronously; return a no-op disposer rather than rejecting (which the hook's bare `void init()` would surface as an unhandled rejection). The existing 'error' listener only covers async emitter errors.
- Hook: guard `void init()` with .catch() as belt-and-suspenders.
- Tests: containment (decoy rejected, submodule accepted), symlink HEAD/reflog refused, fs.watch sync-throw no-op, and the hook still rendering when watch setup rejects.
* fix(core): validate the git object store instead of path-shape containment
Replaces the path-segment containment guard — which qqqys showed a crafted `.git/worktrees/x` path could spoof — with git's own validity check: a trusted gitDir must have an object store. A standalone repo has `objects/` + `refs/` directly; a linked worktree / submodule gitdir instead carries a `commondir` file pointing at the main gitdir that does. Incomplete forgeries (a lone HEAD, or a path-shaped `.git/worktrees/x` containing only a HEAD) have neither and are rejected — exactly what `git rev-parse` rejects with 'not a git repository' (exit 128). Verified the criterion against real git across standalone / unborn / worktree / fake structures.
- Addresses qqqys (path-shape `.git/worktrees/fake` with only HEAD) and yiliang114 (fake `.git` dir with only HEAD+logs) — both now return undefined.
- Tests: both PoCs rejected; worktree (via commondir) and submodule (own object store) accepted; makeRepo now creates objects/ + refs/ so fixtures are real git dirs.
* fix(core): harden ref-name gate (C1/length) and stop caching non-repo misses
- isValidRefName: also reject C1 controls (0x80-0x9f) and U+2028/U+2029, and cap length at 255. With git no longer vetting the value, a hand-written HEAD could otherwise carry terminal escape bytes (CSI/OSC) or layout-desyncing line separators into the status line, and string-width undercounts C1.
- getCachedGitDir: cache only successful resolutions. A null (non-repo) result was cached permanently, so a directory that became a repo mid-session (git init / clone) never showed a branch until restart — a regression from the old git rev-parse path which always hit the filesystem.
* fix(core): isolate subscriber callbacks in the shared reflog watcher
In the shared-watcher fan-out (one fs.watch per gitDir, many subscribers), a subscriber whose onChange throws synchronously would halt iteration — later subscribers never fire — and the exception would escape to the event loop as uncaughtException, poisoning every component watching that repo. Wrap each callback in try/catch. The sole current caller (useGitBranchName) can't throw, but watchRepoBranch is exported public API.
* fix(core): close readGitHead symlink TOCTOU with O_NOFOLLOW; per-component ref rules
Addresses review:
- readGitHead: open HEAD with O_NOFOLLOW instead of lstat-then-readFile, so a symlinked HEAD is refused atomically (ELOOP) and can't be swapped in during the check->read gap. Mirrors the existing O_NOFOLLOW use in gitDiff.ts; falls back to plain O_RDONLY where the flag is absent (Windows).
- isValidRefName: also reject names where any slash-separated component starts with a dot or ends with .lock (git's check-ref-format applies per component, not just to the whole name).
- hasGitStore: run the two isDir probes in parallel.
- Reword the module doc (drop the cross-product reference) and note the residual, bounded lstat->watch TOCTOU on logs/HEAD (the watch only ever fires readGitHead, which opens HEAD with O_NOFOLLOW, and never reads logs/HEAD content).
* fix(core): clear watchers in clearGitDirCache; debug-log watcher failures; guard refresh
- clearGitDirCache now also closes the shared reflog watchers. Both maps are gitDir-keyed, so clearing only the resolution cache would leak the watchers' fds.
- Add a debug logger and warn on the unexpected paths (fs.watch synchronous throw, FSWatcher 'error'). The common silent fallbacks (not a repo, no HEAD) stay quiet so a status-line read can't log-spam.
- useGitBranchName: guard the watcher-triggered void refresh() with .catch() — the synchronous try/catch inside watchRepoBranch can't observe an async rejection.
* fix(core): bound the HEAD read, cap ref length per-component, guard fs.constants
- readGitHead: read a bounded 4 KB prefix and parse only the first line instead of loading the whole file — a pathologically large HEAD can no longer be read into memory.
- isValidRefName: the length cap is now per slash-separated component (git's actual filesystem limit), not the whole ref — a valid deeply-nested ref longer than 255 total is no longer wrongly rejected.
- Access fs.constants via optional chaining (O_RDONLY/O_NOFOLLOW/F_OK), matching gitDiff.ts, so a mock or platform without `constants` can't throw.
* fix(core): bound/O_NOFOLLOW the commondir read; block bidi/zero-width + more ref rules
- Share a readFirstLineNoFollow helper between HEAD and commondir, so the commondir read is now also bounded (4 KB) + O_NOFOLLOW — a crafted oversized or symlinked commondir can no longer OOM the status-line path or redirect the validity check out of the repo.
- isValidRefName also rejects: bidi-override (U+202A-202E, U+2066-2069) and zero-width (U+200B-200D, U+FEFF) characters (display spoofing); a slash-separated component ending in a dot (git check-ref-format); and the literal name 'HEAD' (ambiguous with a detached HEAD, which git rejects as a branch).
* fix(core): O_NONBLOCK against FIFO hangs, swallow close errors, test commondir symlink
- readFirstLineNoFollow: open with O_NONBLOCK so a crafted FIFO .git/HEAD or commondir can't block indefinitely and pin a libuv thread-pool slot (the old git rev-parse path had subprocess timeouts; the direct read had none). And `await fh.close().catch(() => {})` so a close error (EIO / stale NFS handle) can't escape — the helper promises null on any failure — matching gitDiff.ts / fileHistoryService.ts.
- Test: a symlinked commondir is refused via O_NOFOLLOW, closing the coverage gap alongside the existing symlinked HEAD and reflog tests.
* feat(core): add Requesty provider
Requesty (https://requesty.ai) is an OpenAI-compatible model gateway that uses
the same provider/model identifier format as OpenRouter, so it is added by
mirroring the existing OpenRouter provider.
- RequestyOpenAICompatibleProvider + isRequestyProvider detection, mirroring
the OpenRouter provider (base https://router.requesty.ai/v1, attribution headers)
- register in the openaiContentGenerator dispatch + provider preset registry
- auth migration entry + docs (auth, model-providers)
Signed-off-by: Thibault Jaigu <thibault.jaigu@gmail.com>
* fix(core): harden Requesty provider hostname detection
Address review feedback on isRequestyProvider:
- Replace substring matching (baseURL.includes) with URL-parsed hostname
detection (host === 'router.requesty.ai' || host.endsWith('.requesty.ai')),
matching the ownsModel gate in presets/requesty.ts and the MiMo/MiniMax/
Mistral providers. Rejects crafted URLs like router.requesty.ai.evil.com.
- Add hostile-hostname rejection tests and determineProvider dispatch tests,
matching the coverage in the other provider suites.
---------
Signed-off-by: Thibault Jaigu <thibault.jaigu@gmail.com>
isFunctionResponse / isFunctionCall use parts.every(...), which is vacuously
true for an empty parts array — so a user (or model) message with parts: []
was reported as an all-function-responses (or all-function-calls) turn. In
checkNextSpeaker that makes an empty user turn look like a function-response
turn and hands the next turn to the model with the wrong reasoning. Require at
least one part before the every() check so an empty message is neither.
* fix(extension): accept uppercase URL schemes in Claude plugin sources
resolvePluginSource compared a string plugin source against 'http://' and
'https://' case-sensitively, so a marketplace.json source such as
'HTTPS://github.com/owner/repo' fell through to local-path handling and
failed with "Plugin source not found". Lowercase the source before the
scheme check, matching #5426 / #5429 / #5439.
* test: return a GitHubDownloadResult from the download mock
The mockImplementation returned void, which tsc --build rejected (TS2345)
even though vitest passed. Return a GitHubDownloadResult so the build is clean.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
The Token Plan preset was missing three models that are available on the
ModelStudio Token Plan endpoint: qwen3.7-plus, glm-5.2, and kimi-k2.7-code.
Users who had these models installed would see a "Built-in Provider Update"
prompt offering to remove them.
- Add qwen3.7-plus (1M context, thinking, image+video) — already existed in
the Coding Plan preset but was missing from Token Plan
- Add glm-5.2 (1M context, thinking) — already existed in the Z.AI preset
but was missing from both Alibaba plan presets
- Add kimi-k2.7-code (256K context, always-on thinking, image+video) —
a new Kimi coding model not present in any preset
Also widen the modalityDefaults pattern for Kimi from /^kimi-k2\.5/ to
/^kimi-k2\./ since k2.5, k2.6, and k2.7 all support image+video input
per the Kimi documentation.