Commit Graph
9801 Commits
Author SHA1 Message Date
roboomp d41d0c4253 fix(agent): kept advisor enable idempotent
Compared the resolved advisor runtime inputs before rebuilding on explicit enable so unchanged /advisor on calls keep the live advisor agent and history.

Added regression coverage for the unchanged-config path preserving advisor context.

Fixes #4302
2026-07-02 11:36:07 +00:00
roboomp f3834dfce8 fix(agent): rebuilt advisor on explicit enable
Rebuilt live advisor runtimes when the explicit enable path runs so changed modelRoles.advisor values are re-resolved without restarting the session.

Added a regression test covering a live advisor moving from one role model to another through setAdvisorEnabled(true).

Fixes #4302
2026-07-02 11:27:47 +00:00
metaphorics 9bc94b7b26 fix: address cubic review feedback (iteration 5)
Cancel the ACP terminal timeout timer on completion: the hoisted Bun.sleep(timeoutMs) left a live ref'd timer for the full command timeout after every fast command, accumulating timers and delaying process shutdown in SDK/headless use. Switched to a cleared setTimeout, and rewrote the timeout regression test against real time instead of mocking Bun.sleep (the mock coupled the test to timer implementation and starved the event loop).
2026-07-02 19:08:49 +09:00
metaphorics 7d93fd206c fix: address cubic review feedback (iteration 4)
Replace scattered persisted-message cache invalidation (11 call sites across branch-mutation flows) with use-time validation: the memoized key set is anchored to (session file, leaf id) and rebuilds itself whenever the anchor moved. Every branch mutation changes the leaf by definition, so staleness is structurally unrepresentable and no mutation path has to remember to invalidate. Incremental append extends the cache only when it was fresh for the branch the append lands on.
2026-07-02 18:44:44 +09:00
metaphorics 41e958a73e fix: address cubic review feedback (iteration 3)
Validate the full-session retention cache with a rolling hash over the entire retained prefix instead of only the boundary message: a branch rewrite that leaves the boundary message unchanged (e.g. in-place edit of earlier messages) now also triggers a full rebuild. Regression test covers the boundary-unchanged prefix rewrite.
2026-07-02 18:31:46 +09:00
metaphorics 671169cf4d fix: address cubic review feedback (iteration 2)
Self-healing boundary guard for the full-session retention cache: fingerprint the last retained message; on branch shrink (rewind) or prefix rewrite (branch switch/compaction), rebuild the full transcript instead of retaining stale content or silently retaining nothing. Regression tests cover append, shrink, and rewrite.
2026-07-02 18:15:09 +09:00
roboomp fd6f733290 style: bun run fix 2026-07-02 08:53:25 +00:00
roboomp b38eba2be7 fix(coding-agent): exclude column-clipped lines from seen-line provenance
Codex reviewer flagged that the ranged-read fallback recommended by
the patcher's over-cap reveal path (`path:N-M`) itself applies the
512-column cap and still records the displayed line numbers via
`recordSeenLinesFromBody`. On a minified wide anchor line, `read
file:N` shows only the clipped prefix but the line lands in the
tag's seenLines — a subsequent edit anchored at N then slips past
the seen-line guard.

- `packages/coding-agent/src/edit/file-snapshot-store.ts`:
  `recordSeenLinesFromBody` grows an optional `excludedLines` set;
  parsed line numbers matching it are filtered before recording.
- `packages/coding-agent/src/tools/read.ts`:
  `#readLocalFileMultiRange` and the single-range disk path build a
  `clippedLines` set alongside `columnTruncated` for both direct-range
  lines and `buildLineEntriesWithBlockContext` context lines, then
  pass it into `recordSeenLinesFromBody`.
- `packages/coding-agent/src/tools/grep.ts`:
  same wiring for the match line via `match.truncated`, plus a
  conservative length+`...`-marker heuristic for context lines
  (native `crates/pi-natives/src/grep.rs` `truncate_line` doesn't
  propagate a per-line flag on `contextBefore`/`contextAfter`; a
  proper native-side flag is a follow-up).
- `packages/coding-agent/test/edit/seen-line-guard.test.ts`:
  new case reads a 4KB single line and asserts the clipped line
  number stays out of `seenLines` and the edit against it still
  rejects with the seen-line guard.
2026-07-02 08:53:18 +00:00
metaphorics da86dc9912 fix: address cubic review feedback (iteration 1)
TTL-based negative cache (30s) for failed !command config resolutions instead of process-lifetime: transient secret-provider failures recover without restart while still bounding the execSync retry storm to one probe per window.
2026-07-02 17:51:21 +09:00
roboomp 23cbeb49ff fix(coding-agent/dap): bound writeMessage flush and clean up leaked adapter processes
- Replace the module-level writeMessage with a private DapClient.#writeMessage
  that races sink.flush() against a 30 s cap and this.proc.exited, disposes the
  client, and rethrows on either failure. A wedged adapter stdin previously
  blocked writeMessage forever.
- sendRequest fires the write in the background and attaches a passive
  promise.catch guard so the request timer's rejection cannot become an
  unhandled promise before the caller's await subscribes. This also lets the
  caller's timeoutMs win over a hung flush instead of waiting the full 30 s
  write cap.
- #spawnSocketUnix and #spawnSocketClientAddr wrap the readiness / connect
  race in try/catch that kills proc before rethrowing. Add a private
  socketReadyTimeoutMs hook on DapSpawnOptions so tests do not have to wait
  the full 10 s socket cap.

Regression tests cover all three failure modes: a wedged stdin flush must
reject sendRequest at the caller timeout with no unhandledRejection, and the
Unix + TCP client-addr spawn paths must terminate the detached adapter
process when the socket never appears.

Fixes #4233
2026-07-02 08:43:26 +00:00
roboomp 8f6bd66a5f fix(status-line): routed gh pr lookup through git.github.run with timeout signal
The status-line renderer's #lookupPr method called `gh pr view` through
Bun's raw `$` shell with no signal, no timeout, and no non-interactive
environment. A stalled `gh` process (keychain prompt, network hang, auth
deadlock) wedged the await forever; because #prLookupInFlight was set
before the call and never reset, subsequent renders skipped the lookup
and the child leaked indefinitely.

Route the lookup through the existing `git.github.run` helper with
`AbortSignal.timeout(git.GIT_COMMAND_TIMEOUT_MS)` so the child inherits
GH_NON_INTERACTIVE_ENV (disabling terminal and keychain prompts) and
receives SIGTERM on the standard 5-minute deadline. Non-zero exit still
falls through to the null cache, preserving the failure-tolerant
behavior.

Fixes #4234
2026-07-02 08:42:17 +00:00
roboomp 0c4c2f2d91 style: bun run fix 2026-07-02 08:40:30 +00:00
roboomp 5bc796d4cb fix(clipboard): read the system clipboard via Bun.spawn instead of execSync
readTextFromClipboard called execSync for pbpaste, termux-clipboard-get,
wl-paste, and xclip; readMacFileUrlsFromClipboard did the same for
osascript; copyToClipboard for termux-clipboard-set. execSync parks the
event loop until the child exits or the 2000ms timeout fires, so a hung
clipboard daemon froze the TUI render loop for the whole budget on every
paste and copy chord (input-controller handleImagePaste and
handleClipboardTextRawPaste).

A new spawnCapture helper wraps Bun.spawn with the same 2000ms guard,
stdout-to-string decoding, and non-zero-exit/timeout throw semantics the
outer try/catch already assumed. Every synchronous clipboard shell-out
now yields to the event loop while the child runs. Regression test
under readTextFromClipboard runs a slow fake pbpaste and asserts a
concurrent setInterval keeps ticking; the pre-fix code delivered zero
ticks.

Fixes #4235
2026-07-02 08:40:14 +00:00
roboomp 76c480646e fix(cli): added fetch timeouts
Added timeout-backed AbortSignals to update, Hindsight, and Smithery fetch calls so stalled endpoints abort instead of hanging indefinitely.

Added regression coverage for the timeout signals on the exposed command/client paths.

Fixes #4229
2026-07-02 08:39:21 +00:00
can1357 88f0aab994 style: formatted eval fix commits with biome 2026-07-02 10:34:53 +02:00
roboomp d2be57a8f7 fix(plugins): drain subprocess pipes concurrently with proc.exited
PluginManager.install (bun install + bun update), PluginManager.uninstall,
PluginManager.#fixMissingPlugin, the legacy installer.ts install/uninstall
helpers, and generate-legacy-pi-bundled-registry.ts's formatInPlace all
called Bun.spawn with stdout/stderr piped and awaited proc.exited before
touching either stream. Once a child's output exceeded the ~64 KiB OS
pipe buffer, the child would block on write(2) while the parent blocked
on exit — a classic pipe-buffer deadlock. Even where Bun's current runtime
happens to buffer eagerly, the pattern silently leaked unbounded bytes.

Each site now starts new Response(proc.stdout).text() and stderr readers
immediately after Bun.spawn and awaits them alongside proc.exited via
Promise.all. Existing error semantics are preserved: install throws with
stderr, uninstall keeps its generic error, and formatInPlace still includes
Biome's stderr in the failure message.

Adds a regression test (plugin-install-git.test.ts) that models the
OS-pipe deadlock by holding proc.exited until both mock streams are
drained — install must read them before awaiting exit, else the test hits
its 2s Promise.race timeout.

Fixes #4230
2026-07-02 08:33:32 +00:00
roboomp 08899a4392 fix(ssh): bound pre-command SSH helpers with ptree.exec timeout
runSshSync and runSshCaptureSync in connection-manager.ts invoked ssh
through the Bun shell with .quiet().nothrow() but no timeout and no
abort signal. They sit on the ensureHostInfo -> probeHostInfo /
ensureConnection path that runs *before* SshTool.execute applies the
user-supplied command timeout, so an unreachable host or wedged
control-master hung the tool forever.

Switch both helpers to ptree.exec with a 30s timeout,
allowNonZero: true, allowAbort: true, and stderr: "full". The
timeout is a parameter (default 30_000) so tests can override with a
short bound; both helpers still return their existing failure-result
shape (exitCode / stdout / stderr), so ensureConnection can surface a
'Failed to start SSH master ...' error instead of blocking.

Add a regression test that stages a fake ssh binary trapping SIGTERM
and sleeping 300s, then confirms both helpers return within a bounded
window with a non-zero exit code.

Fixes #4232
2026-07-02 08:32:52 +00:00
can1357 3830ad353e merge PR #3843 (surviving delta): perf: streaming-reveal/render throughput + core hot-path optimizations (@oldschoola)
# Conflicts:
#	packages/coding-agent/src/config/model-resolver.ts
2026-07-02 10:31:45 +02:00
can1357 12bbf4a090 merge PR #1706 (+exit-guard port): fix(coding-agent): guard custom tool process exits during load (@roboomp) 2026-07-02 10:30:58 +02:00
can1357 eeeab97db0 fix(session): auto-reply awaited IRC sends to an idle plan-mode session
Plan mode records idle IRC deliveries into context without waking a turn,
so an 'irc send await:true' sender was stranded until its wait timeout:
delivery reported injected but no reply could ever be generated. Extend the
existing ephemeral side-channel auto-reply (previously only for mid-turn
recipients with async execution disabled) to the idle plan-mode case — the
second situation where a real reply turn cannot happen in time. No primary
turn is woken; plan-mode convergence stays user-driven.

The deliverIrcMessage eligibility change itself rode in the previous commit
(same-file hunks); this commit carries the bus doc and the regression test:
an awaited idle IRC message in plan mode resolves the sender's bus waiter
via the side-channel reply while the primary loop stays asleep.
2026-07-02 10:30:51 +02:00
can1357 71209f025d fix(session): drop stale plan-mode-decision tool choice on skip/exit
The settle-time reminder queues a hard 'required' tool choice paired with a
scheduled continuation. If that continuation never runs (user prompt bumps
the generation, dispose, compaction/handoff) or plan mode is exited first,
the queued directive leaked onto the next unrelated turn as a forced tool
call. Remove it by label on continuation skip, on user-initiated prompts,
and when plan mode is disabled.
2026-07-02 10:30:51 +02:00
can1357 2875dd3349 merge PR #3911: fix(session): converge plan mode on ask/resolve across continuation paths (@metaphorics)
# Conflicts:
#	packages/coding-agent/src/session/agent-session.ts
2026-07-02 10:30:51 +02:00
metaphorics 42543428cc fix(web): cap docs.rs gunzip decompressed size at 256 MB
docs-rs.ts:handleDocsRs downloads up to 50 MB of compressed rustdoc JSON (MAX_BYTES) and then decompresses it with gunzipSync without a size bound, so a zip bomb can expand 10:1+ and OOM/crash the process. Pass { maxOutputLength: 256 * 1024 * 1024 } to gunzipSync at line 402 to cap decompressed output; if the limit is exceeded it throws and falls through to the existing catch (signal check / return null), preserving the failure contract. Coding-agent typecheck (bun run check:types) and Biome check on the changed file pass; no dedicated test exists for this path.

Closes #4249
2026-07-02 17:30:22 +09:00
metaphorics 8bfbea1e0f fix(collab): add WebSocket backpressure with ordered pending-send drain
CollabSocket.send previously called ws.send() whenever the socket was OPEN without checking bufferedAmount, allowing the WebSocket send buffer to grow unbounded under a slow relay or guest. The fix adds 64 KiB high / 32 KiB low backpressure thresholds: new frames are queued when bufferedAmount is above the high watermark, and a 25 ms drain timer retries flushing queued frames once the buffer drops below the low watermark, preserving send order via the existing #sendChain. Overflow still drops frames once #pendingSends reaches MAX_PENDING_SENDS, and the drain timer is cleared on close, reconnect, and fatal failure. Verified with `bun test packages/coding-agent/test/collab` (64 pass, 0 fail).

Closes #4248
2026-07-02 17:30:14 +09:00
can1357 727bcf1401 fix: drop superseded /btw todo-reminder HUD clear
Main removed the floating todoReminderContainer in 112317bc8 (todo
reminders are now anchored inside the scrollback transcript and reset
by renderInitialMessages({clearTerminalHistory: true})), so the added
this.todoReminderContainer.clear() references a property that no longer
exists post-merge, failing typecheck and throwing on every /btw branch.
Revert the interactive-mode hunk and its test, and reword the changelog
entry to cover only the goal-mode todo context fixes.
2026-07-02 10:30:12 +02:00
can1357 4940a053ad merge PR #3777: Fix todo HUD and goal context follow-ups (@jeffscottward) 2026-07-02 10:30:12 +02:00
metaphorics 374698b9b1 perf(cli): parallelize prompt file resolution at startup
buildSessionOptions in packages/coding-agent/src/main.ts awaited three independent resolvePromptInput calls for SYSTEM.md, APPEND_SYSTEM.md, and TITLE_SYSTEM.md sequentially, adding unnecessary I/O latency on the startup path. Replace the sequential awaits with Promise.all so the three file reads run concurrently while preserving the same variable assignments and error propagation. Verified with bun run check:types and the coding-agent-heavy test suite.

Closes #4247
2026-07-02 17:30:06 +09:00
can1357 23d16a4c5e merge PR #3922: fix(task): scan OMP extension agents/ dirs in discoverAgents (@roboomp) 2026-07-02 10:30:06 +02:00
can1357 a23d1d6554 merge PR #4140: fix(coding-agent): stopped isolated task merges failing when working tree carries WIP for files the agent also modifies (@roboomp) 2026-07-02 10:30:06 +02:00
can1357 ebf41281e7 fix(tui): stop spinner ticks for frozen pending previews
The github renderer materializes plain Text per display rebuild, so its
animatedPendingPreview opt-in requested 30fps repaints with a frozen
glyph for the whole run_watch wait; drop the flag. Custom tools with
only one of renderCall/renderResult never route to the animated generic
fallback, so gate the unregistered-renderer spinner on both being
absent. Regression tests for both no-tick contracts.
2026-07-02 10:30:06 +02:00
can1357 6429de3e83 merge PR #4172: fix(tui): animate live tool spinners (@roboomp) 2026-07-02 10:30:06 +02:00
can1357 278056025b merge PR #4200: fix(tui): stop /move overlay from statting every entry per keystroke (@roboomp) 2026-07-02 10:30:06 +02:00
metaphorics c7fb052db6 perf(hindsight): cache full-session retention transcript incrementally
With retainMode: 'full-session' (the default), HindsightSessionState.retainSession previously called prepareRetentionTranscript on the entire message window every N turns, making total formatting cost O(n²) over a long session. The change caches the formatted transcript and the index of the last retained message, appending only newly added messages and advancing the cache after client.retain succeeds. The cache is reset when the session id changes, conversation tracking resets, or the mode leaves full-session. Verified byte-identical output against full-window formatting, plus bun run check:types and bun test packages/coding-agent/test/hindsight-backend.test.ts.

Closes #4246
2026-07-02 17:29:58 +09:00
can1357 4e12e5bf76 fix(extensibility): read lazy graph modules from disk at import time
The consume-once source map kept entries for graph modules the initial
import never loaded (modules only reached via lazy dynamic imports).
Their first import - possibly long after load, and after an on-disk
edit - was served the boot-time snapshot instead of current file
content, and the unconsumed sources stayed in the plugin closure for
the process lifetime.

Clear the map once the entry import settles: everything Bun loaded at
startup was already consumed (keeping the read-once win), and anything
left must be read at its actual import time, matching pre-dedup
behavior for lazy modules. The new regression test passes on the
pre-dedup baseline and fails on the unfixed dedup.
2026-07-02 10:29:57 +02:00
can1357 64e4bd89d1 merge PR #4205: perf(extensibility): read extension source graph once per load (@metaphorics) 2026-07-02 10:29:57 +02:00
can1357 2a7a7ec15c merge PR #4202: fix(coding-agent): scope discoverExtensionPaths to native extension-module provider (@roboomp) 2026-07-02 10:29:57 +02:00
can1357 c8fcf7a3c7 merge PR #4209: fix(coding-agent): cache plugin extension resolution (@roboomp) 2026-07-02 10:29:56 +02:00
metaphorics 37990d8f9f perf(export): skip TTSR delta buffering when no rule matches text/thinking
TtsrManager.checkDelta concatenated the full stream buffer on every text_delta and thinking_delta before scope filtering in #matchBuffer, making long streams O(n²) even when all registered rules are tool-scoped. Added #canMatchText and #canMatchThinking flags, set them only after successful rule registration from the computed scope, and returned early from checkDelta for text/thinking sources that no rule can match. Verified by re-reading the modified ranges; no test or typecheck suites were run per campaign constraints.

Closes #4245
2026-07-02 17:29:50 +09:00
can1357 a7ff81e0f6 merge PR #4217: fix(providers): hydrate runtime model cache before selection (@roboomp) 2026-07-02 10:29:47 +02:00
can1357 a5043b11ec merge PR #4221: fix(session): keep model switches active after rate limits (@roboomp) 2026-07-02 10:29:47 +02:00
metaphorics 6c94081888 fix(eval): surface unexpected JS eval worker exit via close listener
In `wrapBunWorker` (`packages/coding-agent/src/eval/js/context-manager.ts`), `error` and `messageerror` listeners were registered during normal operation, but the `close` listener was only added inside `close()`. If user code called `process.exit(0)` or the Bun worker otherwise exited cleanly, the `close` event fired with no handler, so `runOnce` never rejected and callers hung until the cell timeout.

Add a normal-operation `close` listener in `wrapBunWorker.onError` that forwards `new Error("JS eval worker exited")` through the existing error handler path, and remove it in the returned unsubscribe callback.

Verified with `bun run check:types` and `bun test test/tools/eval-*.test.ts test/core/eval-workflow-helpers.integration.test.ts` (32 pass, 0 fail).

Closes #4244
2026-07-02 17:29:42 +09:00
can1357 c2a9f97c41 fix(title): don't treat caseless scripts as shouting
isAllCapsWord matched any multi-letter token without a lowercase letter,
so CJK tokens registered as ALL-CAPS words: two adjacent ones marked the
whole source shouty and silently disabled acronym restoration for every
non-Latin-script message (e.g. '修复 CNPG 集群故障' kept 'Cnpg'). Require an
actual uppercase letter; cased-script shout detection is unchanged.
2026-07-02 10:29:38 +02:00
can1357 289c7b357d fix(title): allowlist ETL so acronym restoration matches the documented contract
The PR's changelog, doc comment, and prompt examples all name ETL as a
restored acronym, but the review-response narrowing (vowel heuristic +
allowlist) silently dropped it: ETL bears a vowel and was not listed.
Add it to COMMON_TITLE_ACRONYMS and pin it in the allowlist test.
2026-07-02 10:29:37 +02:00
can1357 7be66da1ef merge PR #4222: fix(title): preserve ALL-CAPS acronyms in auto-generated session titles (@roboomp) 2026-07-02 10:29:37 +02:00
metaphorics e09b72810a perf(session): cache persisted-message keys, invalidate on branch change
AgentSession.#sessionMessageAlreadyPersisted previously called SessionManager.getBranch() on every message_end event, rebuilding the root-to-leaf path and JSON-comparing each entry; for long sessions this is O(n) per message and O(n²) over a growing transcript. Add #persistedMessageKeys, a memoized Set<string> of persistence keys for the current branch, so the common missing-key check uses Set.has and only falls back to a branch walk on rare key collisions. Invalidate the cache whenever the leaf path changes (newSession, createBranchedSession, resetLeaf, branch, and branchWithSummary) and incrementally warm it after each real append. Verified with git diff --check, a scoped bun build of agent-session.ts, and bun test packages/coding-agent/test/agent-session-persisted-keys-cache.test.ts (2/2 passing).

Closes #4243
2026-07-02 17:29:34 +09:00
metaphorics ae21a7a7e7 fix(task): abort underlying MCP call when proxy tool timeout fires
`withAbortTimeout` rejected after `MCP_CALL_TIMEOUT_MS` but left `waitForConnection` / `callTool` running because the timeout never propagated an abort signal to those underlying promises. The fix creates a per-call `AbortController` in `createMCPProxyTools.execute`, combines its signal with the caller signal via `AbortSignal.any`, and passes both the combined signal and the controller to `withAbortTimeout`, which aborts the controller on timeout or caller abort. Verified by `bun build packages/coding-agent/src/task/executor.ts --target=bun --format=esm` and `bun test packages/coding-agent/test/task-executor-mcp-timeout.test.ts`.

Closes #4242
2026-07-02 17:29:26 +09:00
metaphorics 4eb90bf874 fix(bash): bound ACP terminal lifecycle with abort/timeout
ACP terminal RPCs in `BashTool.execute` were awaited without abort or timeout guards, so a hung `createTerminal`, `kill`, or `currentOutput` call could keep the tool alive past its configured timeout. The ACP polling loop also recreated `exitPromise`/`abortedP` `.then()` closures on every 250 ms tick, leaking memory during long-running commands. This change moves abort/timeout setup before `createTerminal`, races it against the timeout/abort signals, hoists the exit/abort racers outside the loop, tracks `lastPolledOutput`, and bounds kill and final-output waits with fallbacks; late-resolving `createTerminal` handles are killed and released. Verified with `bun run check:types` and `bun test test/bash-acp-terminal.test.ts` in `packages/coding-agent` (6 pass, 0 fail).

Closes #4241
2026-07-02 17:29:19 +09:00
metaphorics 1778ab93b4 fix(bash): bound interactive bash live display write queue
`BashInteractiveOverlayComponent.appendOutput` pushed every PTY chunk into an unbounded `#writeQueue` while xterm drained asynchronously, so fast output could grow memory without bound during the tool timeout. Added `MAX_LIVE_WRITE_QUEUE_CHUNKS = 512` and `#trimWriteQueue()` to drop oldest pending chunks at/after the first pending index after each append. `bun run check:types` passed for the package, and a focused JS sanity check verified the trim keeps the queue under the cap for idle and active-write cases without mutating active write state.

Closes #4240
2026-07-02 17:29:11 +09:00
metaphorics ed387e57f0 perf(tui): load persisted subagents asynchronously
`registerPersistedSubagentsFromDir` in `packages/coding-agent/src/modes/components/agent-hub.ts` used `fs.readdirSync` recursively to walk persisted subagent directories, blocking the event loop with unbounded synchronous I/O when the Agent Hub opened. Converted `registerPersistedSubagents` and `registerPersistedSubagentsFromDir` to async functions using `fs.promises.readdir`, and changed `AgentHubOverlayComponent`'s constructor to start registration fire-and-forget, exposing `persistedSubagentsReady` so callers can observe completion. Failures are logged with `logger.warn`, rows are refreshed on success, and a final render is requested in `finally`. Verified with `bun --check packages/coding-agent/src/modes/components/agent-hub.ts` and `packages/coding-agent/test/agent-hub-activate.test.ts`.

Closes #4239
2026-07-02 17:29:03 +09:00
metaphorics b589d45e6e perf(tui): memoize copy-selector preview highlight across renders
Every keystroke or arrow-key navigation in the copy selector re-rendered the preview by syntax-highlighting the entire `target.preview`, replacing tabs, and wrapping it to width. `CopySelectorComponent` now keeps instance-private memo fields for the last target and its generated source; `invalidate()` clears them, and rendering reuses the cached source while the selected target is unchanged. Verified with `bun test packages/coding-agent/test/modes/components/copy-selector.test.ts` (5 pass, 0 fail).

Closes #4238
2026-07-02 17:28:55 +09:00