Wire workspace file tabs into the editor with Markdown preview/edit mode and guard tab closes so dirty buffers are not discarded silently.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Subagents like `general-purpose -> implement layout export` that generate
text without calling tools would emit no `task_progress` events, making
them appear to have no progress. Fix by emitting progress on every
assistant message, not only when tool_use is present.
Also fix three related issues:
- Local async/background/foreground agent `output_file` now points to the
real agent transcript path instead of the `.output` symlink, avoiding
empty files when symlink creation fails on Windows.
- Agent async output now includes `taskId` alongside `agentId` so callers
don't confuse which field to pass to `TaskOutput`.
- `TaskOutput` missing-task error now explains the task may have been
evicted or belong to another session, and suggests reading `output_file`.
- `initTaskOutputAsSymlink` fallback writes a diagnostic message with the
transcript path instead of creating a misleading empty file.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add an external-build recovery path that withholds prompt-too-long API errors, compacts the current context, and retries once before surfacing the original error.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Allow the tab strip scroll region to shrink inside the title bar so Windows window controls are not pushed offscreen by long or crowded tabs.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Add Solo Council panel collapse controls, surface the final-plan approval hint, and verify Solo mode toggles restart existing sessions so the prompt applies immediately.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Keep provider ordering compatible with older desktop store state, keep workspace traversal blocked even when external changed-file roots are registered, and align the quality-contract test with the current AGENTS wording.
Tested: bun test scripts/pr/quality-contract.test.ts
Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx src/__tests__/skillsSettings.test.tsx src/__tests__/pluginsSettings.test.tsx src/__tests__/diagnosticsSettings.test.tsx --run
Tested: bun run check:policy
Tested: bun run check:desktop
Tested: bun test src/server/__tests__/workspace-service.test.ts -t 'does not allow relative traversal'
Tested: bun test src/server/__tests__/sessions.test.ts -t 'workspace/file and tree should reject traversal|workspace/diff should reject traversal' --timeout=20000
Confidence: high
Scope-risk: narrow
Fix four root causes in the desktop preview pipeline, surfaced when the
model writes the files the user pointed it at:
- Output chips guessed paths from prose and could point at a missing file.
They are now reconciled against the turn's real changed files: a bare
`index.html` resolves to the `todo-app/index.html` actually written, and
mentions the turn never changed are dropped.
- A standalone single-page index.html got no browser preview (mistaken for a
Vite template). It is now only routed to the source view when a
package.json/vite.config ships in the same change-set.
- Files written outside the session workdir (another folder, or another drive
on Windows) failed to preview with 'Path is outside workspace'. The turn's
changed-file directories are registered as filesystem access roots; html
serves via /local-file and other files via a workdir-relaxed read.
- The visual-selection prompt leaked as a raw bubble on Windows because the
server-appended '[Image source: ...]' line broke replay dedupe. Replay text
is now metadata-normalized before comparison (affects any image message).
Adds unit tests for each: htmlPreviewPolicy, assistantOutputTargets
reconciliation, replay dedupe + stripGeneratedImageMetadataLines, filesystem
access roots, and workspace outside-workdir reads.
Mark <html data-touch-h5> before first paint when the bundle runs in a
phone browser (no Electron host + coarse pointer) and scope every
mobile-only fix under it, so desktop shells and desktop browsers are
untouched:
- raise form controls to 16px and cap the iOS viewport scale, so
focusing the composer no longer zooms the page and never zooms back
- disable content-visibility paint skipping on transcript/trace rows
there (long-press selection on iOS WebKit jumps or drops selections
when they extend into skipped rows); halve the virtualization
thresholds on touch as the replacement paint bound for long sessions
- size the app shell to visualViewport so the composer rides the soft
keyboard instead of being covered, snap back WebKit's keyboard pan,
and keep the transcript tail pinned while the container shrinks
- pad the shell with safe-area insets (viewport-fit=cover) and drop the
bottom inset while the keyboard is up
- keep message action bars (copy/branch) always visible on touch since
hover never fires there; kill the WKWebView tap flash and body
rubber-banding
- move the two inline content-visibility styles (trace message blocks,
trace list rows) to classes so the touch scope can reach them
Tested: cd desktop && npx vitest run (1474 tests)
Tested: cd desktop && npx tsc --noEmit && npx vite build
Tested: Playwright chromium smoke against the built dist - desktop
context unchanged, iPhone/WeChat and Android contexts get the marker,
viewport lock, 16px controls, visible action bars and selectable rows
Render provider settings through dnd-kit sortable rows, include official providers in the same order model, and persist providerOrder across server and desktop state.
Tested:
- bun test src/server/__tests__/providers.test.ts src/server/__tests__/persistence-upgrade.test.ts
- cd desktop && bun run test -- --run src/stores/providerStore.test.ts src/__tests__/generalSettings.test.tsx -t "providerStore reorderProviders|Settings > Providers tab"
- cd desktop && bun run lint
- cd desktop && bun run build
- bun run check:persistence-upgrade
Not-tested:
- bun run check:server (broad suite hit environment failures during this run: MCP stdio zshrc timeout, adapter dependency gap at the time, and e2e cascade)
Confidence: high
Scope-risk: moderate
Resolve locale conflicts with the Solo naming from main while keeping Solo Council panel translations.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Show the Solo Council flow, final synthesis, and structured review blockers in the panel while adding stable prompt markers for future runs.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Rebuild terminal background task state from persisted background_task messages so history restore keeps all task consumers in sync without reviving stale running tasks.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Keep Solo Council visible across task state loss by falling back to persisted background task messages and showing standby role cards when agents are idle.
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Require Solo's plan gate to launch real Planner, Reviewer, and Critic subagents instead of simulating roles in prose. Add a read-only plan-reviewer specialist and surface Council task status/verdicts in a desktop panel backed by existing background agent task events.
Tested: bun test src/tools/AgentTool/builtInAgents.test.ts src/coordinator/workerAgent.test.ts src/coordinator/soloPipelinePrompt.test.ts
Tested: cd desktop && bun run test -- --run src/components/chat/SoloCouncilPanel.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test -- --run src/i18n/lspError.test.ts
Tested: cd desktop && bun run build
Confidence: high
Scope-risk: medium
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
Treat user_message_replay as idempotent for the current turn, even after thinking or tool events have already appeared in the live message list. This keeps normal sent prompts and guided queued prompts from rendering a duplicate user bubble when the CLI replays the same prompt.
Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts --run
Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx -t "queues prompts submitted while a turn is running until the user guides them" --run
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Tested:
- cd desktop && bun run test -- src/components/browser/BrowserAddressBar.test.tsx src/components/browser/BrowserSurface.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- bun run check:desktop
Insert guided queued prompts into the desktop transcript as soon as the user clicks Guide, then confirm them on CLI replay without adding duplicate user bubbles.
Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx -t "queues prompts submitted while a turn is running until the user guides them"
Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx
Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Shorten the Solo mode label in the composer + menu and the session header chip from "Solo pipeline" to just "Solo" across all five locales. The mode tooltip already describes the A/B/C plan gate behavior.
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test -- --run src/i18n/lspError.test.ts
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
* feat(solo): add council-style plan gate
Introduce a prompt-level A/B/C planning gate so Solo mode requires Planner, Reviewer, and Critic perspectives before implementation, while preserving the existing staged workflow and human approval gate.
Tested: bun test src/coordinator/soloPipelinePrompt.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts -t "Solo Pipeline system prompt"
Tested: bun test src/server/__tests__/conversations.test.ts -t "routes coordinator and Solo"
Tested: cd desktop && bun run lint
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
* feat(agents): add plan critic specialist
Add a read-only plan-critic specialist with parseable PLAN_REVIEW verdicts, register it in built-in and coordinator agent registries, and teach Solo's plan gate to use Plan plus plan-critic when available before synthesis.
Tested: bun test src/tools/AgentTool/builtInAgents.test.ts src/coordinator/workerAgent.test.ts src/coordinator/soloPipelinePrompt.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts -t "Solo Pipeline system prompt"
Tested: bun test src/server/__tests__/conversations.test.ts -t "routes coordinator and Solo"
Not-tested: Full bun test remains blocked by unrelated existing failures in attribution header, shell PATH/env, release workflow, and desktop Vitest compatibility tests.
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
* chore(ci): retrigger PR checks after applying policy labels
Co-Authored-By: Claude GPT-5.5 <noreply@anthropic.com>
---------
Co-authored-by: 你的姓名 <you@example.com>
Co-authored-by: Claude GPT-5.5 <noreply@anthropic.com>
* fix(reverse-engineering): declare missing tool-binary prereqs for 4 servers
Real-world repro on a fresh Win11 machine showed 4 servers fail in ways
the desktop one-click install couldn't surface, because their declared
`prerequisites[]` only covered the runner (uvx / java) and not the
underlying tool binary the runner needs at startup.
Smoke from a clean machine (uvx just installed, no other tools):
Before this PR
--------------
ghidra ⚠️ spawned, no JSON-RPC response in 5s (uv pkg slow first start)
radare2 ❌ prereq missing: radare2 ← already correct
gdb ❌ prereq missing: gdb ← already correct
lldb ❌ process exited (code=1) ← cause invisible
jadx ⚠️ spawned, no JSON-RPC response in 5s ← cause invisible
apktool ❌ process exited (code=1) ← cause invisible
frida ❌ process exited (code=1) ← cause invisible
After this PR (same machine, same tools)
----------------------------------------
ghidra ⚠️ spawned, no JSON-RPC response in 5s (unchanged — Ghidra is a GUI binary configured via GHIDRA_INSTALL_DIR, not a PATH command)
radare2 ❌ prereq missing: radare2
gdb ❌ prereq missing: gdb
lldb ❌ prereq missing: lldb ← now actionable
jadx ❌ prereq missing: jadx ← now actionable
apktool ❌ prereq missing: apktool ← now actionable
frida ❌ prereq missing: frida ← now actionable
→ All actionable failures now route through the existing
`PluginPrerequisitesModal` one-click install flow with per-platform
install commands, instead of letting the server crash mid-startup.
What this PR adds (servers.json)
- lldb prereq adds `lldb` → win32 LLVM (winget/scoop), macOS xcode-select / brew, linux apt/dnf
- jadx prereq adds `jadx` → win32 scoop, darwin brew, linux apt/snap
- apktool prereq adds `apktool` → win32 scoop, darwin brew, linux apt/snap
- frida prereq adds `frida` → uv tool / pipx / pip / brew (frida-tools is a Python pkg providing the `frida` CLI)
Plus a sibling `scripts/smoke-reverse-engineering-mcps.ts` that:
- reads the same plugin's `servers.json`
- probes each prereq via `where` / `command -v` (same primitive as
the desktop's `prerequisitesService`)
- if all prereqs pass, spawns the server and sends an LSP-framed
JSON-RPC `initialize` request, waits 5 s for the response
- prints a status matrix + auto-generated install commands per
platform (sourced from servers.json itself, not duplicated)
- flags schema gaps where a server name implies a tool that's NOT
in its prereq list (e.g. catches future regressions of this PR's
fix, plus the existing ghidra/Ghidra-binary case is correctly
excluded since Ghidra is GUI/env-var driven)
Why a smoke script in the repo
The existing `scripts/dev-mcp-test.ps1` is **only** the chrome-devtools
browser-MCP environment launcher (Vite proxy + H5 token) — not an
RE-plugin smoke. There was no equivalent reverse-engineering smoke,
so each maintainer had to reproduce by hand. With this script, future
"is the RE plugin healthy?" questions are one `bun run` away.
Plugin version: 0.4.3 → 0.4.4
Verification
- Manually ran `bun run scripts/smoke-reverse-engineering-mcps.ts`
on a fresh Win11 26200 with uv 0.11.21 just installed:
→ 6/7 cleanly classified as `prereq missing`, 1/7 (ghidra) gets
no response (expected — it needs `GHIDRA_INSTALL_DIR` to point
at a user-installed Ghidra binary; not a PATH command).
- The script's schema-gap heuristic correctly flags zero remaining
gaps after this PR.
Tested: live smoke on a real machine; before/after diff above.
Not-tested: macOS / Linux paths (only Win32 install map exercised).
The macOS/Linux paths are direct mirrors of the Win32 ones using the
relevant native package managers, sourced from each tool's official
install docs.
Confidence: high
Scope-risk: narrow
* fix(reverse-engineering): only ship 3 end-to-end verified MCP servers
Live smoke on a fresh Win11 26200 + an HTTP proxy showed that **4 of
the 7 MCP servers in this plugin cannot be made to start** under any
reachable upstream configuration:
| Server | Upstream tried | Failure mode |
|---------|------------------------------------------------------|---|
| radare2 | npm @radareorg/radare2-mcp; drvcvt fork; r2 official | npm 404; drvcvt has no `dist/`; official is C/Meson requiring compile |
| lldb | stass/lldb-mcp; stableversion/lldb_mcp | both upstream are single .py with no pyproject.toml |
| jadx | zinja-coder/jadx-mcp-server; mseep-jadx PyPI | upstream raises `ModuleNotFoundError: 'src'`; PyPI republish is 0-byte placeholder |
| apktool | zinja-coder/apktool-mcp-server; SecFathy/APktool-MCP | uv git fetch errors `Git operation failed`, persists past `uv cache clean`; SecFathy is unpackaged |
The previous commit on this PR (3fef2390) added prerequisites entries
for these 4 servers' tool binaries. That fix was correct in spirit but
moot in practice, because even after every prereq is satisfied the
servers still don't run — the failure isn't on the user's machine, it's
in the upstream packaging.
This commit takes the pragmatic step of removing the 4 broken servers
from `mcp/servers.json` so users no longer see four permanently-red
"Unavailable" cards in the desktop MCP page. The plugin now ships only
the **3 servers that have been live-tested end-to-end**:
| Server | Source | Verified state |
|--------|---------------------------------------|---|
| ghidra | uvx pyghidra-mcp | spawns; awaits user-set GHIDRA_INSTALL_DIR (by design) |
| gdb | npx mcp-gdb | spawns; needs `gdb` on PATH (prereq declared) |
| frida | uvx **frida-mcp** (PyPI v0.1.1) | ✅ initialize OK in 694 ms; serverInfo.name == "Frida" v1.27.2 |
Note frida changed source: was `uvx --from git+...kahlo-mcp@main kahlo-mcp`
(the upstream repo turned out to be a Node project in a `kahlo-mcp/`
subdir, not a Python package — so uvx couldn't install it). The PyPI
package `frida-mcp` is a clean, properly-packaged equivalent.
What this commit changes
- `plugins/reverse-engineering/mcp/servers.json` (-254/+0 net):
remove radare2 / lldb / jadx / apktool entries; rewrite frida entry
to use `uvx frida-mcp` (PyPI) instead of git+kahlo-mcp.
- `plugins/reverse-engineering/.claude-plugin/plugin.json`: 0.4.4 → 0.4.5.
- `plugins/reverse-engineering/README.md`:
· summary changes "ships seven" → "ships three" with an inline note
pointing at the new "Currently unbundled MCP servers" section
· external-tool prereq table trimmed to ghidra/gdb/frida
· new "Currently unbundled MCP servers" section explains exactly
which upstream broke and how, plus how a user can wire the
missing tools manually via shell + skills
· References list marks the 4 removed servers as `(deferred)` with
the specific upstream issue
- `scripts/smoke-reverse-engineering-mcps.ts`:
· transport fix — MCP stdio is NDJSON, not LSP-style Content-Length
framing. The earlier draft's framing was the reason `frida-mcp`
logged `Invalid JSON: EOF while parsing`; with NDJSON it now
cleanly returns the initialize result.
· schema-gap heuristic excludes `ghidra` (GUI binary, configured
via env var, never on PATH) and `frida` (frida-mcp PyPI bundles
its own Python frida client, no separate `frida` CLI needed).
Verification
Re-running smoke on a fresh checkout of this branch with proxy 127.0.0.1:7887:
```
=== Reverse-engineering MCP smoke ===
Source: plugins\reverse-engineering\mcp\servers.json
Servers: 3
ghidra ⚠️ spawned but no JSON-RPC response in 5010 ms
gdb ❌ prereq missing: gdb
frida ✅ initialize ok (694 ms)
```
3/3 outcomes are correctly classified, 0 schema-gap warnings, and the
"Install commands for missing prereqs" section guides the user to
`scoop install gdb` / `pacman -S mingw-w64-x86_64-gdb` for the only
missing tool on this machine.
Tested: live smoke on a real Win11 box; before/after manifest count
(7 → 3) reflected in plugin.json bump.
Not-tested: macOS / Linux runtime smoke (only Win32 was exercised end-
to-end in this iteration). Each server's install map remains correct
across all three platforms.
Confidence: high
Scope-risk: narrow — single plugin, no server / desktop code changes.
---------
Co-authored-by: 你的姓名 <you@example.com>
When an upstream request was aborted mid-stream (SDK client timeout,
stream idle watchdog, non-streaming fallback timeout, or user
cancellation), the trace fetch hook waited on a clone of the response
body that could hang forever, so the call never left "pending" in the
trace panel — exactly the silent stall that misled the #766 report.
- captureResponseTraceSnapshot reads the body with abort awareness:
reader.cancel() on abort keeps the partial body, with a 2s grace
backstop for runtimes where cancel cannot wake a hung read.
- The fetch hook now records an error-state call on abort with the
abort reason (e.g. the watchdog's stream idle timeout), duration,
partial response body, and an api_call_aborted event; non-abort
capture failures also record an error instead of inferring ok, and
pre-response fetch rejections carry an aborted flag.
- The trace detail panel shows an "Aborted" badge plus guidance for
aborted calls, and labels the new api_call_aborted phase in all
locales.
Tested: bun test src/server/__tests__/trace-capture.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run test -- --run && bun run lint
api_retry heartbeats already reach the desktop status bar, but the
streaming-to-non-streaming fallback had no signal at all: the CLI only
flipped an internal flag, and the one-shot fallback response can take
minutes with zero incremental output, so the UI showed a bare spinner
the whole time.
- CLI: yield a {type:'system', subtype:'streaming_fallback', cause}
message at both fallback sites (stream error/watchdog and 404 stream
creation), mirroring the existing api_error -> api_retry path through
query.ts passthrough, QueryEngine SDK output, and the SDK schema.
- Server: translate it to a streaming_fallback ServerMessage;
unrecognized causes normalize to 'unknown' instead of dropping the
event.
- Desktop: track it as active-turn state (cleared at the same 12 sites
as apiRetry; a fallback notice supersedes the stale retry banner) and
render a neutral pill with the turn timer - expected state, not an
error, so no amber styling and nothing in the diagnostics panel.
- Retry banner now shows "retrying now" once the countdown elapses
instead of sticking at "waiting 0s".
With 62241a31 disabling the non-streaming fallback for desktop CLI
sessions, this notice mainly covers the 404 gateway path (which skips
the disable check), callers that re-enable fallback via env, and
non-desktop SDK consumers.
Constraint: Retries and fallbacks are expected states per the
diagnostics severity standard - lightweight active-turn UI only, no
error-panel entries, no transcript persistence.
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run test -- --run
Tested: cd desktop && bun run lint
Not-tested: live provider outage reproduction; verified via unit
coverage of the translation, store lifecycle, and indicator rendering.
Avoid the bunx launcher for local Electron packaging after it can be terminated before Electron Builder starts. Use the installed Electron Builder CLI through Node for the macOS package script and desktop package shortcuts.
Tested: SKIP_INSTALL=1 SKIP_PACKAGE_SMOKE=1 bash ./scripts/build-macos-arm64.sh
Tested: bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/macos-arm64
Tested: bash -n desktop/scripts/build-macos-arm64.sh
Tested: git diff --check
Not-tested: full bun run verify was not run because this is a narrow local packaging launcher fix.
Confidence: high
Scope-risk: narrow
The context usage indicator stayed at the pre-compact percentage (e.g.
100%) until the next API response arrived. Two stacked causes:
- The CLI anchors the displayed total to max(local estimate, last API
usage) so the meter never drops mid-turn — but preserved messages
(SM-compact / partial compact) still carried the pre-compact usage,
pinning the meter after compaction. buildPostCompactMessages now
zeroes token usage on preserved assistant copies, the established
stale placeholder that getCurrentUsage() skips, covering every
compaction path in one place. Originals are not mutated, so
transcript and cost accounting are unaffected.
- Right after compaction the CLI is often still busy, so the
indicator's refresh timed out ("Request timed out after 30s") and the
catch kept the stale context on screen with no later retry (auto
refresh is throttled to 10s and stops once the session goes idle).
compact_boundary now bumps a per-session compactCount; the indicator
force-refreshes on that nonce — bypassing the throttle and any
in-flight request that may still hold pre-compact data — and retries
once after 5s if the forced refresh fails.
Tested: bun test src/services/compact/
Tested: cd desktop && bun run test -- src/components/chat/ContextUsageIndicator.test.tsx src/stores/chatStore.test.ts
Tested: cd desktop && bun run lint
Confidence: high
Scope-risk: narrow
Desktop injected a far stricter timeout stack than the terminal CLI, so
healthy-but-slow third-party providers (sensenova/bailian/zhipu) died at
exactly API_TIMEOUT_MS while the UI showed "running" forever:
- API_TIMEOUT_MS is the SDK client's time-to-first-byte budget for
streaming requests; these gateways send zero bytes (no headers, no
SSE ping) until prefill finishes, which takes minutes at large
contexts. Raise the default from 120s to the SDK's own 600s and widen
the configurable range to 30-1800s.
- Widen the desktop-forced stream watchdog idle window to 240s so
silent thinking/prefill phases stop tripping the 90s default.
- Disable the non-streaming fallback for desktop CLI sessions: a
non-streaming request only responds after the FULL generation, so it
can never finish inside the same budget and loops timeout aborts
forever while the UI spins (also avoids double tool execution,
upstream inc-4258). All three knobs respect caller env overrides.
Repro: mock upstream whose SSE stays silent for 150s before a complete
event sequence — terminal env completes; desktop env aborts at exactly
120s (client timeout) or 90s watchdog + non-streaming fallback loop;
the fixed env completes both variants.
Tested: bun test src/server/__tests__/network-settings.test.ts src/server/__tests__/conversation-service.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx
Tested: cd desktop && bun run lint
Confidence: high
Scope-risk: medium
Add pending user-message queue controls while a desktop turn is running, replay queued prompts into the transcript when the CLI accepts them, and flush remaining prompts after turn completion.
Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx
Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: bun run check:desktop
Not-tested: bun run check:server remains blocked by unrelated E2E CORS preflight expectation, expected 403 but got 204.
Confidence: medium
Scope-risk: moderate
Increase the desktop session list fetch window so noisy observer sessions are less likely to hide real user sessions from the sidebar.
Tested: cd desktop && bun run test -- src/stores/sessionStore.test.ts src/components/layout/Sidebar.test.tsx
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Reveal-in-Explorer/Finder rejected ~-prefixed paths because no layer
expanded the tilde to the home directory. Expand it in the three path
normalization entry points: server validateOpenPath, frontend
resolveAbsolute, and Electron normalizeOpenPath. Tilde expansion is
platform-aware (~\ only on win32, where backslash is a separator).
Tested: cd desktop && bun run test -- src/components/common/OpenWithMenu.test.tsx src/components/workspace/WorkspaceFileOpenWith.test.tsx src/components/browser/BrowserSurface.test.tsx src/lib/handlePreviewLink.test.ts src/lib/openWithContextForHref.test.ts
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Phase 4 of editor-lsp-foundation. The CLI agent's FileEditTool /
FileWriteTool / NotebookEditTool path was already vendored from upstream
and already drives the upstream LSP system. This PR bridges agent edits
into the desktop workbench (CodeMirror editor + workspace panel) without
touching the vendored tool code: the desktop chatStore observes the
agent's tool stream and refreshes/conflict-flags the panel itself.
Approach (option A in the spec discussion):
- chatStore: when a FILE_EDIT tool ('Edit'/'Write'/'NotebookEdit'/
'MultiEdit') completes, remember the toolUseId -> file_path mapping
in a per-session pending map. On the matching tool_result with
isError === false, consume the entry and call
workspacePanelStore.notifyAgentFileEdit(sessionId, absolutePath).
- workspacePanelStore.notifyAgentFileEdit: refreshes loadStatus when
the panel is open, and sets a 'source: agent' conflict on any open
editor buffer whose workspace-relative path matches the agent's
absolute path by suffix on a normalized segment boundary. Skips
buffers that already have a conflict so user-source banners aren't
clobbered. Uses sentinel hash 'agent-edit' because the chat tool
stream carries no content hash.
- Path matching normalizes backslashes -> forward slashes and trims
trailing slashes, then either equals or endsWith('/' + bufferPath)
to avoid foobar.ts matching bar.ts.
- Cleanup: clearPendingFileEdits added at the 3 existing pending-map
cleanup sites (disconnect, clear messages, new-session path).
Why no FileEditTool change:
The vendored upstream tool has no sessionId in ToolUseContext and
shouldn't import server WS modules from CLI process scope. The chat
stream already carries (sessionId, toolName, toolUseId, input, isError)
to the desktop and is the natural seam for desktop-side reactions —
same pattern the store already uses for TodoWrite -> useCLITaskStore
and Task* tools -> refreshTasks.
Tests:
- desktop/src/stores/workspacePanelStore.test.ts +6 (30/30 total)
Suffix match, Windows backslash, non-suffix substring rejection,
existing-conflict preservation, loadStatus when panel open, no
loadStatus when closed.
- desktop/src/stores/chatStore.test.ts +6 (112/112 total)
Edit/Write/NotebookEdit forwarding, isError gate, non-edit tool
isolation (Bash), single-fire on duplicate tool_result.
Verifier (independent agent) ran lint + both test suites + build +
bundle budget + 9 ad-hoc helper edge cases (Unicode paths, trailing
slash, identical absolute, deeper suffix mismatch). All PASS.
Bundle delta: +2.41 KB gz vs origin/main @ 95931d49 baseline
(97.59 KB headroom remaining).
_Requirements: 3.1, 3.2, 3.3, 8.1, 8.2 (adapted)_
Co-authored-by: 你的姓名 <you@example.com>
Token counts were rendered with five inconsistent formats across the
chat UI (header "181,117 t", in-progress bare "↓ 2514", background
agents "12,345 tokens", compact summary "1.5k", trace "1.2k"), and the
in-progress count was actually stale: the server never set the status
event's tokens field, so the indicator showed the previous turn's value
(hence "first message shows nothing", "sometimes appears, never moves").
- Add shared desktop lib/formatTokenCount; route header, streaming
indicator, background agents, compact summary, and trace through it.
- Header shows compact "124.3k tokens" with the exact count on hover.
- Add common.tokens i18n key (en/zh/zh-TW/jp/kr).
- Estimate the in-progress count from streamed chars (÷4, mirroring the
CLI spinner) via a new streamingResponseChars per-session field, reset
on each send; drop the dead status.tokens/elapsed fields.
- CLI spinner rows use formatTokens to drop the "1.0k" artifact.
* fix(providers): route 'Fetch Models' through server to bypass mixed-content / CORS
The "获取模型 / Fetch Models" button in the provider settings dialog used
to call upstream `/v1/models` directly from the renderer with `fetch()`.
That works for HTTPS providers whose endpoint sets permissive CORS
headers (e.g. api.openai.com, api.anthropic.com), but fails for any
plain-`http://` provider — most commonly self-hosted relay endpoints
like `http://47.116.22.0:3000`.
Two failure modes both surface as the same opaque "Failed to fetch":
1. Mixed-content block. The desktop renderer runs in a secure context
(Tauri at `tauri://localhost`, Electron with web security on), and
modern webviews refuse to fetch plain-HTTP URLs from a secure-context
page. The request never leaves the browser.
2. CORS-missing. Even when reachable, many self-hosted relays do not
return `Access-Control-Allow-Origin` for `/v1/models`, so the browser
blocks the response.
The "Test Connection" button never had this problem because it already
proxies through `POST /api/providers/{id}/test`, which runs on the local
server (Node, no webview restrictions). The fix routes "Fetch Models"
through the same server so it inherits the same isolation.
What this PR adds
Server side
- ApiError.badGateway(message) — new 502 helper for upstream failures.
- POST /api/providers/fetch-models with the new FetchModelsSchema (zod):
{ baseUrl: url, apiKey: non-empty, apiFormat: 'anthropic' | 'openai_chat'
| 'openai_responses' default 'anthropic' }.
- ProviderService.fetchUpstreamModels(input) — server-side GET against
`${baseUrl}/v1/models` (or `${baseUrl}/models` if baseUrl already ends
in `/v1`), reusing the existing network-proxy / timeout plumbing. Sets
Bearer for OpenAI-compatible formats, x-api-key + anthropic-version
for native Anthropic. Returns `{ status, data }` where `data` is the
upstream JSON verbatim. Non-2xx upstreams surface as ApiError.badGateway
with the upstream's `error.message` if the body has one. Network
failures and timeouts also map to badGateway with a clear message.
Desktop side
- providersApi.fetchModels(input) — new client wrapping the endpoint.
- handleFetchModels in Settings.tsx now calls providersApi.fetchModels
instead of fetching directly. The existing `extractModelEntries`
parser already accepts arbitrary upstream JSON (data / models / items
/ results arrays, plus { id | name | model } per entry), so no
parsing change was needed.
Tests
- 5 new ProviderService unit tests covering the wire shape:
· OpenAI format → Bearer header, GET /v1/models, no x-api-key
· Anthropic format → x-api-key + anthropic-version, no Authorization
· URL hygiene — strips trailing slashes, doesn't duplicate /v1
· Non-2xx upstream → ApiError 502 with `HTTP {status}: {message}`
· Network error → ApiError 502 with "Upstream fetch failed: ..."
Verification
- `bun test src/server/__tests__/providers.test.ts -t "fetchUpstreamModels"`
→ 5/5 pass.
- `bun run lint` (desktop): tsc --noEmit clean (after `bun install` to
pick up the @codemirror packages added by an earlier PR — orthogonal
to this change).
- All existing provider tests unchanged (no regression in test file).
Tested: server unit (5 cases covering wire shape, URL hygiene, error
paths); existing testProviderConfig tests still pass; desktop tsc clean.
Not-tested: live smoke against an actual self-hosted relay (no provider
fixture configured locally). The bug repro is well-understood: the
secure-context webview's mixed-content block is documented browser
behaviour and the fix removes the renderer-side fetch entirely.
Confidence: high
Scope-risk: narrow
* test(providers): add desktop-side fetchModels client test (clears change-policy block)
Locks the renderer→server contract: providersApi.fetchModels must POST to the local server with baseUrl/apiKey/apiFormat in the body, NOT fetch the upstream URL directly. The same-origin assertion guards against any future regression that reintroduces the mixed-content / CORS bug class.
Two cases:
- success path: upstream JSON forwarded verbatim, body shape locked
- error path: 502 from server surfaces upstream message in thrown error
---------
Co-authored-by: 你的姓名 <you@example.com>
Replace the editable MCP server heading with neutral configuration copy so the settings page does not imply an update action is running.
Fixes#763
Tested: cd desktop && bun run test -- src/__tests__/mcpSettings.test.tsx
Tested: cd desktop && bun run lint
Not-tested: full verify and coverage, scoped desktop copy/test change only
Confidence: high
Scope-risk: narrow