Replace the deprecated acp.ClientSideConnection with the acp.client()
builder: register client handlers by ACP method name and hold the
persistent connection returned by connect(). Agent RPCs now go through
connection.agent.request("<wire-method>", ...), and session/cancel
through connection.agent.notify(). This is behavior-preserving — the
deprecated constructor built exactly this builder (legacyClientApp)
internally.
Drops the three @typescript-eslint/no-deprecated suppressions and the
extNotification catch-all (#137): on SDK 0.28 unhandled notifications
are silently dropped rather than logged as methodNotFound, so the
anti-spam goal holds; only the debug-log of custom notifications is lost.
Teardown is intentionally left as connection = null (no close()) to
avoid rejecting in-flight requests with a codeless Error that would
surface a spurious error banner.
Bump the ACP SDK from 0.14.1 to 0.28.1 (PROTOCOL_VERSION unchanged, so a
direct jump). Adapt the existing client to the SDK's breaking changes while
keeping existing features working; new 0.28 features (elicitation, NES,
providers, document lifecycle) are out of scope.
- Renames: unstable_listSessions->listSessions, unstable_resumeSession->
resumeSession (both stabilized); KillTerminalCommand{Request,Response}->
KillTerminal{Request,Response} (type-only).
- SessionConfigOption became a select|boolean union. The domain type now
carries boolean as data (UI renders/sets only select for now); the
converter and all consumers narrow on type === "select".
- The model API (unstable_setSessionModel, SessionModelState) was removed
from the SDK; model is now a config option. Remove the legacy model path
(setSessionModel, the model dropdown, SessionModel(State) domain types)
but KEEP lastUsedModels + the config-option model restore/save that back
model persistence. Mode keeps its legacy API (still in the SDK).
- zod is now a runtime (peer) dependency of the SDK for schema validation;
add it to dependencies so it bundles into main.js.
- Restore @eslint/json devDep (legacy-peer-deps had pruned this optional
peer that the lint config loads).
ClientSideConnection is deprecated in 0.28 (new: client() builder, scoped
connectWith) but kept for now with documented eslint-disables; migrating
the connection lifecycle is a separate follow-up.
vitest@4 declares an optional peer on @types/node>=20 while this project
pins @types/node@^16, so `npm ci` fails with ERESOLVE on npm 10 (node 22
in CI). Accept the resolution so release/CI/docs `npm ci` and local
`npm i` all succeed; the peer is optional and the vitest suite passes.
dev resolves built-in agents' API keys from secret storage into baseEnv
just before spawn (not into config.env). #312's WSLENV forwarding ran
before that and built its name list from config.env, so the resolved key
name was never listed and the key never crossed into WSL for built-in
agents in WSL mode.
Move the buildWslEnv call to after secret resolution and include
config.apiKey.envVarName in the forwarded name list. Custom agents
(no config.apiKey) are unchanged.
The msgId->height cache added for scroll preservation was only ever
written, never cleared, so entries from old sessions lingered in this
long-lived view and grew slowly over many new-chat/restore/fork cycles.
Clear it in the existing empty-messages effect. All reset paths
(new chat, restore, fork, restart) funnel through clearMessages() ->
setMessages([]) first, so a single clear on length===0 covers them all.
Mirrors how useAgentMessages clears toolCallIndexRef on reset.
Inactive chat tabs are hidden via display:none with the React tree kept
mounted, so the virtualizer persists. On re-show, items briefly re-measure
small while markdown re-lays-out; recording those shrinks collapsed
getTotalSize(), clamping scrollTop to 0 and losing the position.
Harden measureElement: key the size cache by message id, and during a
short settle window after a hide->show transition refuse to shrink the
cache (return max(measured, cached) without recording). Total size stays
stable, scrollTop is never clamped, and the position is preserved to the
pixel - no programmatic re-scroll, so no races with auto-scroll.
The err and empty-response branches called the same
findInKnownPaths(...).then(resolve, () => resolve(null)) expression
verbatim; extract it into a small void thunk. Behavior is unchanged.
- only return an executable regular file (match which's contract), fixing
the case where a directory/non-exec entry could be written into settings
- run the fallback asynchronously (fs/promises) to honor resolveCommandPath's
non-blocking contract
- guard against path separators in the command name
- always settle the promise even if the fallback rejects
- document the best-effort scope and the intentional WSL/Windows asymmetry
Builds on #317.
Replace the right-aligned wrapping button row with full-width rows
stacked vertically inside a bordered container, mirroring the diff
block styling within tool calls. Kind semantics move from solid
button backgrounds to a colored Lucide icon per row (check-check /
check / x / ban), gated by displaySettings.showEmojis like other
tool call icons. Long option names truncate with ellipsis and expose
the full text via the title attribute.
Layout is now stable regardless of option name length or view width,
and all !important declarations plus the unreachable selected/disabled
permission styles are removed.
Pass the agent-provided option kind through as-is so the UI can
distinguish 'reject once' from 'reject always'. The name-based
inference remains as a fallback for non-conforming agents that
omit kind.
- Agent launcher: `cd "$1" 2>/dev/null;` -> `cd "$1" || exit 1;` so a failed
cd doesn't silently run the agent in the wrong directory. Matches the
terminal wrapper and pre-#304 launch (both use `cd ... && ...`).
- buildWslEnv: guard `existing` with `typeof baseEnv.WSLENV === "string"` so
the helper truly never throws (its docstring's contract) even if WSLENV is
set to a non-string via a type cast.
- Tests: add regression guards for both.
Not changed: the `/bin/sh -l -c` fallback for non-POSIX shells (Copilot
flagged it as possibly unsupported). It matches the shipped buildWslShellWrapper
and `wsl.exe /bin/sh -l -c 'echo OK'` works (dash supports -l); revisit across
all three builders together if a shell without `-l` support turns up.
The argv/terminal launchers added in 850e2e1 and b91184f dropped the
explicit `. ~/.profile 2>/dev/null;` that buildWslShellWrapper has. bash -l
skips ~/.profile when ~/.bash_profile exists, and linuxbrew/nvm/mise install
their PATH there -- and bare command names (e.g. claude-agent-acp) resolve
via that PATH. Without it, affected WSL users would fail to spawn the agent
or run terminal commands.
Restore `. ~/.profile 2>/dev/null;` in buildWslArgvScript and
buildWslTerminalScript, matching buildWslShellWrapper and preserving the
intent of #177 (236843c / dd4f4ca / fce40a3). Add regression-guard tests
asserting the launchers source ~/.profile.
Terminal commands (tool_call terminals) still used the nested
`sh -c "<buildWslShellWrapper(innerCommand)>"` construction, which carries
the same fragility that broke agent launch in some environments (e.g.
RHEL8) and double-escaped the command line.
Run terminal commands the same way as agents now:
wsl.exe [--exec] /bin/sh -c '<launcher>' sh '<commandLine>'
The command line is delivered as a single positional and run under the
user's login shell ($SHELL -l -c "$1"). This keeps full shell parsing
(pipes, redirects, subshells, bash-isms under the user's shell, e.g. bash)
-- behaviorally identical to the previous `$SHELL -l -c <line>` -- while
skipping wsl's default-shell layer (--exec) and removing the doubly-escaped
nested string.
Also, in WSL mode omit the Windows-side spawn cwd for terminals: the
working directory is applied inside the launcher (cd '<wslCwd>'), and a
Linux path as the wsl.exe process cwd would make CreateProcess fail.
buildWslShellWrapper is retained (now used only by paths.ts for the WSL
`which` lookup).
Tests updated/added accordingly (43 cases).
WSL agent launch previously wrapped the command in a nested
`sh -c "<baked string>"` construction. In some environments (e.g. RHEL8)
this fails before ACP connects (the process exits 1), and command
paths/args with spaces broke because the command was interpolated
unescaped.
Launch agents instead with:
wsl.exe [--exec] /bin/sh -c '<constant launcher>' sh <pathDir> <cwd> <command> <args...>
This skips wsl's default-shell layer (--exec), runs under the user's
login shell so ~/.profile is sourced (environment preserved, unlike a
bare --exec), and passes command/args as argv (no quoting of user data).
Also forward configured env vars (API keys, custom agent env, tool env)
into WSL via WSLENV (buildWslEnv), so the plugin's API key field works in
WSL mode without requiring keys in ~/.profile. Defensive: skips empty
values and invalid names, merges existing WSLENV, never throws.
Add a vitest unit-test suite for the platform WSL helpers (39 cases,
incl. exact argv-ordering checks), with an obsidian stub alias and a
dedicated tsconfig for typed linting of tests.
Builds on #304 (direct WSL exec for absolute-path commands); this retains
the environment that a bare --exec would drop and also fixes paths/args
with spaces.
Terminal launch keeps the existing wrapper for now (env forwarding is
applied); a terminal-specific hybrid follows separately.
In WSL mode, launch simple absolute-path agent commands with `wsl.exe --cd <cwd> --exec <command> ...args` instead of routing them through the nested shell wrapper.
This avoids fragile `sh -c` quoting behavior when launching agents from Windows into WSL, especially for executable+argv CLIs such as OpenCode, Codex, Claude Code, and similar tools.
The existing shell-wrapper path is preserved for commands that may need shell semantics, including relative commands, PATH injection via `additionalPath`, or commands containing shell metacharacters. This keeps compatibility with existing configurations that rely on shell expansion or login-shell behavior while allowing straightforward WSL executables to launch without unnecessary quoting layers.