On the Assistant page, dragging a file onto the sidebar attached it to
`window.sidebar.assistantDraft` — the Workspace sidebar's new-chat draft,
which is not on screen there, so the file appeared to vanish.
`AssistantPanelView` already declared the intent with `isEnabled:
isFocusedOnWorkspace`, but the flag never reached AppKit.
`FileURLDropNSView` registered its dragged types unconditionally in `init`
and never unregistered, and only `hitTest` consulted `isEnabled` — the
three dragging callbacks did not. A view that refuses the hit test but
stays in the window's registered destination list still wins the drop.
This is the asymmetry `AssistantConversationListContainer.isInteractive`
records for conversation drags; the shared file receiver had only the
hit-test half. Give it both: follow `isEnabled` when registering and
unregistering, and gate `supportedPayload(from:)`, the one chokepoint
`hitTest` and all three callbacks pass through. The callbacks already
treat a nil payload as "not mine", so their bodies are unchanged. The
second half is what holds when `isEnabled` flips during a drag already in
flight, after AppKit has resolved its destinations.
Fixing the shared view also covers two sites with the same flaw: hidden
Workspaces, which all stay mounted at `opacity(0)` over the same frame
carrying a pane drop target, and Keychain accepting drops mid-load.
`FileURLDropTargetTests` becomes `.serialized` — every test there stages
its case on the one drag pasteboard the system shares, and the suite was
already racing with itself before these two tests made it fail.
Verification: macOS build, full test suite (remaining failures match the
clean-tree baseline and are unrelated), swift-format lint adds nothing
new, and Scripts/check-zh-localization.sh passes. The end-to-end Finder
drag is still to be confirmed by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scratch_edit is now scratch_replace, and takes a regex mode and a per-entry
count — omit it to require exactly one match as before, 0 for every occurrence,
n for the first n. scratch_copy and scratch_move move a staged file or subtree
without its bytes passing through the transcript, which is what duplicating a
file before a risky change used to cost.
The app-side changes are the ones the rename and the two new tools reach:
ActionKind.copy in both the English and Chinese verb tables, the pull → copy →
replace → push recipe in the built-in config-edit skill, and the same wording in
the system prompt and the plan contract. The legacy tables are deliberately
untouched: legacyToolNames and legacyMetadata describe cards written before
presenter IDs were persisted, and scratch_edit belongs in both. AgentKit keeps
builtin.scratch_edit registered as an alias, so a transcript from before the
rename still draws its diff rather than falling back to generic fields — which
aCardWrittenAsScratchEditStillFindsItsProjection now pins.
AppLanguageTests no longer requires %lld results. That plural moved to AgentKit
with web_search's card, which is what db8796c removed from this catalog; the
test's list had been left behind and was failing.
AGENTS.md said AgentKit was referenced as a local SPM package. It is a versioned
remote dependency, so an uncommitted change in ../AgentKit is invisible to the
app and a change spanning both sides needs a tagged release in between. Said so,
because working the other way wastes a build cycle before the mistake shows.
Verified: AgentKit 0.3.0 builds and passes its 112 tests, and the six suites
this touches pass here. The five failures in the full run — ColorSystemTests,
two ChatComposerStateTests, FileURLDropTargetTests and WindowStateTests — all
reproduce on a clean worktree at HEAD and are unrelated.
The loop, the approval gate, the provider adapters, the MCP client, and the
tools with no opinion about transports now live in a package and are consumed
from it. Termind keeps everything that reaches a machine or a person: the SSH
and SFTP tools, the connection pool, the Vault resolver, scratch_pull and
scratch_push, the SwiftData store, and every view.
They meet at four seams. TermindAgentTools registers our tools alongside the
built-in groups; AgentToolServices carries the SSH credential prompt to them
without that prompt appearing in a runtime protocol; TermindAgentPolicy owns the
system prompt, the plan contract, and the ssh_exec plan-mode rule the runtime
cannot write for us; TermindAgentPresentation extends the card vocabulary with
our own semantics.
ChatPermissionMode becomes AgentPermissionMode and moves out of the conversation
model — who authorizes an action is a property of the run, not of a view.
AgentRunRequest takes the session context as rendered text rather than our
SessionContext, because the runtime does nothing with it but insert it in front
of the turn it describes.
Termind's system prompt and plan contract are byte-identical to what shipped.
Every provider cache is a prefix cache, so editing a word there would miss the
cache on every replayed conversation at once.