Commit Graph

1990 Commits

Author SHA1 Message Date
can1357 ab7f623ac4 Merge PR #4149: fix(tui): coalesce editor top-border rebuild to render tempo (#4145) (@roboomp) 2026-07-01 21:53:18 +02:00
can1357 debe71ae0e Merge PR #4129: fix(coding-agent): preserved explicit :auto suffix in modelRoles (@roboomp) 2026-07-01 21:53:17 +02:00
can1357 b7cb4b8cde fix(coding-agent): mark session disposing before draft save 2026-07-01 21:53:17 +02:00
can1357 1479b689d8 Merge PR #4121: fix(coding-agent): route SIGTERM/SIGHUP/uncaughtException through session teardown (@roboomp) 2026-07-01 21:53:16 +02:00
can1357 c704c5248f fix(rpc): wait for background bash before stdin shutdown 2026-07-01 21:53:16 +02:00
can1357 82232cefb6 Merge PR #4117: fix(rpc): dispatch bash concurrently and reset abort controller on restart (@roboomp) 2026-07-01 21:53:16 +02:00
can1357 edef5c052d Merge PR #4056: fix(tui): smooth write streaming previews (@roboomp) 2026-07-01 21:53:14 +02:00
can1357 cdd6e558e9 Merge PR #4049: fix(collab): enable mobile ask responses (@roboomp)
# Conflicts:
#	packages/coding-agent/src/collab/protocol.ts
2026-07-01 21:53:00 +02:00
can1357 3364c3c6fd fix(collab): surface remote transcript errors after rows 2026-07-01 21:50:54 +02:00
can1357 6019be42c5 Merge PR #3938: fix(collab): reject oversized remote transcript entries (@roboomp) 2026-07-01 21:50:54 +02:00
can1357 2289765840 Merge PR #3894: fix(cli): restore /copy code and /copy cmd shortcuts (@roboomp) 2026-07-01 21:50:54 +02:00
can1357 87a9b9e98e Merge PR #3891: fix(coding-agent): replay scrollback on theme changes (@lederniermagicien) 2026-07-01 21:50:53 +02:00
can1357 c078f92582 Merge PR #3884: fix(coding-agent): handle BigInt tool-call fingerprints (@wolfiesch) 2026-07-01 21:50:53 +02:00
can1357 637c1105de Merge PR #3851: fix(coding-agent): log Esc/Ctrl+C abort failures instead of swallowing them (@oldschoola) 2026-07-01 21:47:54 +02:00
can1357 dae0ab3580 Merge PR #3848: perf(coding-agent): memoize incremental grapheme slicing in streaming reveal (@oldschoola) 2026-07-01 21:47:54 +02:00
can1357 f3744bce8e Merge PR #3776: Fix hidden thinking glyph-only status rows (@jeffscottward) 2026-07-01 21:42:26 +02:00
can1357 b3b4a762ac Merge PR #3736: fix(coding-agent): replan title refresh honors TITLE_SYSTEM.md override (@roboomp) 2026-07-01 21:42:25 +02:00
roboomp 809a8a348d fix(tui): coalesce editor top-border rebuild to render tempo
EventController.handleEvent rebuilt the editor's status-line top border
synchronously on every session event via updateEditorTopBorder(). During
a long-running eval that fires 5-10 events/s, each rebuild ran
StatusLine.getTopBorder → #buildSegmentContext → getCachedContextBreakdown
→ session.getContextUsage → estimateTokens (with JSON.stringify per
toolCall block) — the render pipeline is throttled to ~30 fps, so most
rebuilds were dropped before painting. Combined with a scheduler that
collapsed cadenceDelay to zero whenever a frame overran the 33ms budget,
the TUI busy-looped at ~40-50% CPU.

Fix:
- Editor gains setTopBorderProvider(): a lazy builder invoked once per
  editor render. InteractiveMode installs it in the constructor and on
  setEditorComponent, so the rebuild coalesces to the render tempo
  regardless of event rate.
- Delete updateEditorTopBorder wrapper (now equivalent to
  ui.requestRender) and inline every call site.
- Add adaptive render backpressure: a frame that exceeds
  MIN_RENDER_INTERVAL_MS inflates the next scheduling delay to
  2 * last_frame_cost, capped at 200 ms, targeting a 50% render duty
  cycle instead of pinning the CPU at t=0.

New regression tests:
- editor-top-border-provider.test.ts: provider fires exactly once per
  render, wins over eager setTopBorder, falls back when cleared, gets
  the correct availableWidth.
- adaptive-render-backpressure.test.ts: cheap frames keep the 33 ms
  cadence, a slow frame idles proportionally, pathological frames are
  capped at 200 ms.

Verified with bun test packages/tui/test (all 246 relevant tests pass)
and bun test packages/coding-agent/test/modes (455 tests pass). Three
pre-existing agent-session-handoff snapcompact failures on main are
unrelated (snapcompactSupportedChars binding).

Fixes #4145
2026-07-01 13:51:43 +00:00
roboomp a4ae4c130c fix(coding-agent): preserved explicit :auto suffix in modelRoles
The model selector's persistence path dropped the `:auto` selector when parsing role values, producing a warning ('Invalid thinking level "auto"') and rendering the badge as `inherit` instead of `auto`. Reload of the default role also lost the auto state whenever the role value carried an explicit `:auto` suffix instead of relying on `defaultThinkingLevel`.

Widen the resolver chain (`parseThinkingSuffix`, `splitThinkingSuffix`, `parseModelString`, `parseModelPattern*`, `ResolvedModelRoleValue`, `ResolvedRoleModel`, `ResolveCliModelResult`) to carry the `AUTO_THINKING` sentinel end to end, and coerce it back to `undefined` at concrete-only boundaries (glob scope patterns, retry fallback, advisor, commit pipeline, guided-goal, bench).

Regression tests cover:

- `resolveModelRoleValue("provider/model:auto")` returns explicit auto without a warning.

- `ModelSelector` renders `DEFAULT (auto)` and `SMOL (auto)` when the role value has `:auto`.

- `cycleRoleModels` activates auto thinking on entering a `:auto` role.

- Startup resume activates auto thinking when `modelRoles.default` carries `:auto`.

Fixes #4128
2026-07-01 08:18:42 +00:00
roboomp 73a12c7ea2 fix(coding-agent): routed SIGTERM/SIGHUP/uncaughtException through session teardown
The postmortem SIGTERM/SIGHUP/uncaughtException handlers only ran the registered
cleanup callback list before process.exit, and the only session-related callback
was session-manager-flush. So a real kernel signal (terminal close, process
manager killing omp, IDE stop) skipped saveDraft, session.dispose (session_shutdown
emit, owned async job disposal, kernel disposal, MCP disconnect, browser tab
release), and violated the SessionShutdownEvent docstring contract that promises
delivery on SIGINT/SIGTERM. The LSP client also owned its own SIGINT/SIGTERM
handlers that called shutdownAll then process.exit(0), which could race postmortem's
async runCleanup and short-circuit the session teardown.

- Extracted a promise-memoized createSessionTeardown helper (modes/session-teardown.ts)
  that snapshots the editor draft, persists it via sessionManager.saveDraft, then
  invokes session.dispose. A saveDraft failure is logged but never aborts disposal.
- Memoized AgentSession.dispose so the keypress path and the signal path share one
  settled promise and cannot double-emit session_shutdown or double-drain the owned
  AsyncJobManager.
- Registered the teardown on postmortem as "session-teardown" in InteractiveMode.init,
  replacing the narrower session-manager-flush callback. InteractiveMode.shutdown
  now delegates the draft+dispose steps to the same helper.
- Replaced the LSP client's SIGINT/SIGTERM handlers with a "lsp-shutdown" postmortem
  callback so LSP cleanup runs alongside every other session teardown instead of
  racing them via process.exit(0). beforeExit is unchanged.
- Added session-teardown.test.ts covering: draft-then-dispose ordering, disposal
  after saveDraft rejects, empty-string clears stale sidecar, promise memoization
  under concurrent invocation, and snapshot-at-first-call semantics.

Fixes #4080
2026-07-01 07:19:30 +00:00
roboomp eb4d2a9e93 fix(rpc): dispatch bash concurrently and reset abort controller on restart
The RPC transport did not treat cancellation as an independent, resettable
control plane, so two lifecycle contract violations shared a root cause:

A. Server-side: the stdin loop in `rpc-mode.ts` awaited each commands
   `handleCommand` before pulling the next frame, so `abort_bash` queued
   behind the `bash` it must cancel. Extracted `dispatchRpcInputFrame` and
   dispatch `bash` in the background: the input loop keeps reading, so
   `abort_bash` (or any other command) can preempt an in-flight shell.
   Response correlation still rides `command.id`; ordering across
   concurrent commands is documented as not guaranteed.

B. Client-side: `RpcClient.stop()` aborted the shared `#abortController`
   but never replaced it, so a subsequent `start()` handed a pre-aborted
   signal to `readJsonl` and the stdout reader exited immediately with a
   spurious "Agent process exited before ready" while the spawned child
   leaked in `#process`. Mint a fresh `AbortController` inside `start()`
   and clean up the child on any post-spawn failure.

Adds:
- `dispatchRpcInputFrame` unit tests covering the concurrent bash + abort
  ordering, serial dispatch of other commands, and background error
  reporting.
- `RpcClient` lifecycle tests covering start->stop->start on the same
  instance (via a mock fixture agent) and retry after a failed start.
- Documents the bash concurrency contract in `docs/rpc.md`.

Fixes #4079
2026-07-01 07:15:09 +00:00
roboomp 19f3058c55 fix(tui): extended smooth streaming previews to edit and eval
Registered `edit.input` and `eval.code` alongside `write.content` so the reveal controller decodes those top-level string arguments incrementally between throttled full-JSON parses.
Added a regression test covering multi-key extraction so the wire-up survives future additions.

Refs #4043
2026-07-01 05:32:15 +00:00
roboomp 78fda49cfb fix(tui): smoothed write streaming previews
Added incremental decoding for streamed write content so preview args update below the full JSON parse throttle.
Added a regression test covering sub-throttle content growth in ToolArgsRevealController.

Fixes #4043
2026-07-01 05:25:36 +00:00
roboomp 9d3918bdac fix(collab): enabled mobile ask responses
Routed host UI requests through writable collab web guests and added mobile response controls for ask questions.

Fixes #4035
2026-07-01 05:15:37 +00:00
can1357 763f950b7f fix: streamlined tool-call cloning and validation in the stream pipeline
- Improved the leaked-thinking stream projector to clone and sync native tool-call blocks directly.
- Eliminated the need for placeholder IDs and complex rekeying logic in the event controller and argument reveal module.
- Simplified native tool-call validation in owned-stream processing by requiring only a non-empty name.
- Added comprehensive unit tests to ensure tool-call IDs and partial JSON parameters remain intact during healing.
2026-07-01 05:25:27 +02:00
can1357 ef7636805b feat(coding-agent): removed canonical model variant selection and tracking
- Removed the canonical model variant indexing, selection, and tracking logic from the model registry and resolver.
- Eliminated the `canonical` sub-command, tab view, search tokens, and equivalence configuration structures from the CLI and model selector components.
- Refined model identification, lookup, and provider fallback resolution to bind exclusively to standard, raw model IDs.
- Relocated the equivalence utility script within the catalog package to support script-only policy generation.
2026-07-01 05:22:42 +02:00
roboomp 21adbcf40b fix(collab): rejected oversized transcript entries
Return an explicit remote transcript error when the host cannot fit a complete JSONL entry inside the fetch cap, and stop the guest viewer poll loop after surfacing that error.

Fixes #3931
2026-06-30 23:39:37 +00:00
can1357 bdb33a184d feat(coding-agent/modes): tracked and migrate stream-key placeholders for tool calls
- Added `#streamTurnNonce` to prevent aborted streaming turns from corrupting content indexes of subsequent messages.
- Implemented temporary stream-key generation using content position and turn nonces for previewing tool calls without native IDs.
- Added migration logic to key pending tool previews by their real ID and rekey `ToolArgsRevealController` once the real ID is parsed.
2026-07-01 00:34:21 +02:00
can1357 112317bc8e fix(coding-agent): resolved duplicate todo reminders in terminal scrollback
- Anchors the incomplete-todo reminder block inside the scrollback transcript instead of a floating live container.
- Eliminates duplicate reminder copies piling up in terminal scrollback during terminal reflows.
- Removes the dedicated `todoReminderContainer` and simplifies state synchronization on todo reload.
- Updates tests to verify sequential reminders commit as separate blocks and are left intact when tools succeed.
2026-06-30 23:32:58 +02:00
can1357 407c44547b feat(coding-agent/modes): opened the in-session resume picker in a fullscreen overlay
- Migrated the `/resume` session selector from an inline component to a fullscreen overlay.
- Enabled alternate screen buffer borrowing and mouse tracking support for the picker.
- Ensured proper cleanup of the fullscreen overlay during normal termination or shutdown.
- Configured the selector layout to pin keybinding hints and the footer to the bottom of the screen.
2026-06-30 20:39:45 +02:00
can1357 f7df67ad72 Merge remote-tracking branch 'origin/farm/df125a92/fix-skill-slash-replace-prompt' 2026-06-30 20:17:24 +02:00
roboomp 095392d144 fix(tui,coding-agent): preserved draft when accepting mid-prompt /skill: autocomplete
The mid-prompt slash skill autocomplete added in #3654 replaced the
entire editor draft with /skill:<name> on accept so the dispatcher
(which only matched leading /skill:) would still fire. That wiped
every keystroke the user had typed before reaching for the skill.

Insert the /skill:<name> token at the cursor in the TUI editor —
replacing only the partial /sk slash token, leaving prose before and
after intact — and extend the skill-command parser so a /skill:<name>
token surrounded by whitespace is recognized as an invocation too,
with the surrounding prose threaded through to the skill as args.

The parser change is shared across all three dispatch sites
(interactive TUI, ACP, RPC) via a new parseSkillInvocation helper
in extensibility/skills, so the three Map<string,string> /
session.skills lookups stay aligned on the same parse.

Fixes #3913
2026-06-30 16:12:53 +00:00
can1357 3c012a6c54 Merge remote-tracking branch 'origin/farm/a3e74c54/preserve-auto-model-thinking' 2026-06-30 17:54:52 +02:00
can1357 e0bacbf2ec feat(coding-agent/modes): deferred handling streamed tool calls without an ID
- Skipped processing tool calls in the event controller streaming message when the tool ID is missing.
- Prevented creating orphaned empty placeholder cards caused by empty IDs during early Anthropic and OpenAI tool block streaming.
2026-06-30 17:38:41 +02:00
roboomp 51da3add83 fix(coding-agent): preserved auto thinking across plan approval
Captured the configured thinking selector when entering plan mode so approving a plan restores auto instead of the provisional concrete effort. Reloaded DEFAULT(auto) badges from defaultThinkingLevel and covered the plan-approval handoff plus /model display.

Fixes #3901
2026-06-30 14:28:07 +00:00
Magicien 7da80bc325 fix(coding-agent): replay scrollback on theme changes 2026-06-30 13:06:53 +01:00
roboomp ea47fb0efa fix(cli): restored copy code and cmd shortcuts
Fixes #3893
2026-06-30 11:15:54 +00:00
roboomp 3106a15f7d fix(mcp): raced oauth login against cancellation
Esc and wizard abort signals now race the MCP OAuth login promise directly, so cancellation wins even before OAuthCallbackFlow reaches its callback wait and registers an abort listener. OAuthCallbackFlow also checks pre-aborted signals before opening/waiting on the callback server and its wait path handles already-aborted signals.

Threaded the abort signal into MCP OAuth fetches so dynamic client registration, metadata discovery, authorization probes, and token exchange unblock promptly when the user cancels.

Added a regression test where MCPOAuthFlow.login never observes ctrl.signal, matching the pre-wait race called out in review.

Fixes #3888
2026-06-30 10:21:26 +00:00
roboomp a6b2bac882 fix(mcp): made Esc cancel /mcp reauth and /mcp add OAuth flow
#handleOAuthFlow now installs an editor.onEscape hook that aborts its
AbortController, and accepts an external abortSignal so the add-wizard
can thread its own controller through (the wizard owns focus and absorbs
Esc itself). Cancellation surfaces as MCPOAuthCancelledError, which the
reauth and add catches translate into a neutral status line instead of
the generic OAuth failure banner. Disambiguated from the existing 5-min
timeout via a userCancelled flag so timeouts still read as errors.

The wizard intercepts Esc/Ctrl+C while #oauthAbort is set so its own
"Press Esc to cancel" advertisement now matches the behaviour, and
renames its error heading + tip when the failure is a user cancel. Also
fixed the misleading "(Press Ctrl+C to cancel)" message in the chat
transcript onAuth block to say "Press Esc" — Ctrl+C is bound to the
editor clear action, not interrupt.

Fixes #3888
2026-06-30 10:07:05 +00:00
Wolfgang Schoenberger 6d15894c0a fix(coding-agent): handle BigInt tool-call fingerprints 2026-06-30 01:38:36 -07:00
roboomp 88be72e4d4 fix(coding-agent): seed tool-args reveal with the partial JSON already in hand
ToolArgsRevealController.setTarget initialized new entries with revealed=0, so the first message_update returned { __partialJson: "" } even when the provider had already parsed a complete chunk. For renderers without exposeRawPartialJson (e.g. write), the throttled re-parse + cached displayArgs short-circuited every subsequent setTarget, leaving the preview body blank until tool_execution_end.

Seed revealed with the full incoming partialJson length on entry creation (clamped to a surrogate-safe boundary). The first frame now carries the parsed path/content immediately; subsequent message_updates extend target and the reveal ticks pace only the newly arrived bytes — no field is ever truncated because the seeded prefix is the longest the entry has seen so far.

Fixes #3881
2026-06-30 08:04:49 +00:00
roboomp 64734021a7 fix(tui): deliver buffered double-Esc as two events and re-arm loader after task completion
StdinBuffer held a bare `\x1b\x1b` chunk (or emitted it as one when followed by
a non-CSI byte). `parseKey("\x1b\x1b")` returns undefined, so CustomEditor
fell through to the base editor and never fired the configured `onEscape` —
the double-escape gesture and the second-press single-Esc handler both went
dead whenever the terminal batched the two presses into one stdin read.

Split a bare `\x1b\x1b` into two ESC events at the buffer layer, mirroring
the existing split for ESC + SGR mouse report. Meta-CSI/SS3 chords
(`\x1b\x1b[A`, `\x1b\x1bO…`) still emit as one combined sequence.

EventController.tool_execution_update re-armed the working loader when a
transient overlay (auto-compaction / auto-retry / handoff) had torn it down
mid-tool; tool_execution_end did not. A subagent (`task`) call only fires
_end, so a task result landing after such an overlay left the UI looking
idle even though the session was still streaming. Mirror the reconciler
call in #handleToolExecutionEnd.

Fixes #3857
2026-06-30 03:41:24 +00:00
oldschoola 3f476d5bbc fix(coding-agent): log Esc/Ctrl+C abort failures instead of swallowing them
The Esc/Ctrl+C teardown path had three duplicated try { abortX() } catch {} blocks (compaction/handoff/retry) that silently swallowed any abort error. Replace them with a shared safeAbort helper that logs the failure at debug, so a failing abort stays diagnosable while teardown ordering and the aborted flag are unchanged.
2026-06-29 18:37:05 -07:00
can1357 ce20cfb68e merge #3829: align /extensions MCP status with /mcp list and persist re-enable 2026-06-30 03:01:01 +02:00
can1357 e8090bb48a feat: introduced binary file detection to prevent encoding corruption
- Introduced `isProbablyBinary` utility to sniff file headers for NUL bytes or invalid UTF-8 sequences.
- Updated `ReadTool` to use the binary sniffer, preventing mojibake corruption in output when reading non-text files.
- Refined `file-mentions` auto-reads to skip binary files and mark them as `binary` in the message transcript.
- Added comprehensive unit tests for binary detection logic, covering NUL bytes, truncated multibyte characters, and path-based file sniffing.
2026-06-30 02:59:41 +02:00
oldschoola 718c7cea2f perf(coding-agent): memoize incremental grapheme slicing in streaming reveal
Each 30fps streaming-reveal tick re-segmented the whole revealed prefix via sliceGraphemes (per-tick slice cost grew with the prefix). BlockUnitCounter already memoized per-block grapheme counts; apply the same idea to slicing: cache the slice boundary per block index and re-segment only the per-step delta from the boundary cluster, so per-update cost grows with the delta instead of the rendered prefix.

buildDisplayMessage gains an optional sliceOf seam (default = sliceGraphemes, so existing callers are unchanged); the controller wires its BlockUnitCounter.slice through it via a #build helper that also de-duplicates six previously-repeated buildDisplayMessage call sites.

Only an exact (text, units) hit skips segmentation; the incremental guard (text === cached.text || text.startsWith(cached.text)) && units >= cached.units re-segments from the boundary cluster, so an append that extends the final cluster (e.g. a -> a combining acute, ZWJ family merge) is never stale.

Benchmark (bench/streaming-throughput.bench.ts, 30520-grapheme message, 61 ticks/episode): 23.78ms -> 2.27ms per episode (~10x). Regression tests vs a pure Intl.Segmenter reference cover fixed-text growing units, append growth, boundary-cluster extension, multi-block indices, shrink/regrow, full replace, and a 400-step seeded fuzz.
2026-06-29 17:47:22 -07:00
roboomp e34f2a81e9 fix(tui): force-enabled MCP from tool-owned sources via enabledServers
Codex review on #3829: when an MCP server lives in a non-writable
source config such as opencode.json with enabled:false, the dashboard
re-enable had nowhere to write to — the writable mcp.json fallback
did not own the server, so setMcpServerEnabled fell through to the
denylist and the source's enabled:false kept the row disabled.

Added a parallel allowlist to the user-level mcp.json that overrides
a non-writable source's enabled:false flag without ever mutating the
foreign config:

- types + schema: new enabledServers array (mirrors disabledServers).
- config-writer: readEnabledServers + setServerForceEnabled helpers,
  and setMcpServerEnabled now writes to enabledServers on enable
  when no writable mcp.json owns the server, clears it whenever a
  writable source becomes the source of truth, and always clears the
  override on disable so a force-enabled server can be turned off.
- mcp/config (runtime loader) and state-manager (dashboard read):
  honor enabledServers as an override on enabled:false, while still
  letting disabledServers win.
- Added a regression test that walks the full lifecycle for an
  opencode.json server: enabled:false is surfaced as disabled, the
  dashboard re-enable force-enables via enabledServers without
  touching opencode.json, then disable clears the override and
  populates disabledServers.

Fixes #3827
2026-06-29 20:39:50 +00:00
roboomp 16ef3c54f4 fix(tui): re-enabled MCP alternate config sources
Codex review on #3829: the dashboard re-enable path still missed MCP
servers loaded from supported non-primary native config files such as
.omp/.mcp.json or user .mcp.json. Those rows carry enabled:false from
their source file, so falling back to the user disabledServers denylist
could not make the row active again.

- setMcpServerEnabled now accepts the loaded row's sourcePath and checks
  it before the primary project/user mcp.json paths.
- extension-dashboard passes the source path for writable MCP providers
  (native and mcp-json), avoiding accidental edits to third-party tool
  configs while still updating .omp/.mcp.json and standalone MCP JSON
  sources.
- Added a regression test for a server loaded from .omp/.mcp.json with
  enabled:false; re-enable flips that file to enabled:true and does not
  write the denylist.

Fixes #3827
2026-06-29 20:26:16 +00:00
roboomp 812b246e7e fix(tui): dashboard re-enable flips enabled:false in mcp.json
Codex review on #3829: when an MCP server's mcp.json entry carries
enabled:false, the dashboard toggle previously only removed the name
from the user-level disabledServers denylist. state-manager's new
`server.enabled === false` check (state-manager.ts:156) then still
marked the row disabled, leaving such servers impossible to re-enable
from /extensions.

Extracted setMcpServerEnabled() into mcp/config-writer.ts mirroring
/mcp enable | /mcp disable semantics:

- Server defined in project mcp.json -> update enabled on that entry.
- Else server defined in user mcp.json -> update enabled on that entry.
- Else (discovered third-party server) -> use the user-level disabledServers denylist.
- On re-enable, always clear any stale denylist entry.

extension-dashboard.ts routes mcp:* toggles through this helper. Added
four new regression tests covering: enabled:false re-enable, mixed
flag+denylist re-enable, disable on a config-resident server writing
enabled:false (not denylist), and discovered-server denylist round-trip.

Fixes #3827
2026-06-29 20:13:15 +00:00
roboomp 6256f97eaf fix(tui): aligned /extensions MCP status with /mcp list
Two read paths previously diverged on whether an MCP server was active or
disabled. /mcp list (slash-commands/helpers/mcp.ts:388) treats a server
as disabled when config.enabled === false OR the name is in the
user-level disabledServers denylist; the runtime MCP loader does the
same in mcp/config.ts:115. The /extensions dashboard only consulted
the dashboard-private settings.disabledExtensions array, so a server
disabled via /mcp disable or enabled:false kept showing as active.

Toggling MCP servers from the dashboard had the mirror problem: it only
wrote to settings.disabledExtensions, so /mcp list never noticed.

- state-manager: read user-level disabledServers from mcp.json once and
  consider enabled:false / denylist membership when deriving each MCP
  extension's state, matching /mcp list semantics.
- extension-dashboard: route mcp:* toggles through setServerDisabled
  against the canonical mcp.json denylist, and clean any legacy
  settings.disabledExtensions entry on re-enable so it doesn't keep the
  server marked disabled.
- Added a regression test exercising both read signals and the
  setServerDisabled round-trip the dashboard's MCP toggle now uses.

Fixes #3827
2026-06-29 20:05:04 +00:00