The card window's ⌘V drops its picture branch — the attachments header now offers one instead

Raw image data on a card window's ⌘V was a keyboard shortcut with no visible
trigger; the sidebar's Attachments header now grows a quiet control — beside
the existing add affordance, present only while the pasteboard holds a
picture this card could take — that pastes it through the exact seam the
retired branch used (ClipboardStore.pasteImage(intoCard:in:), the board's
"Paste Image into Card" row's own call). The file-URL branch stays on ⌘V; a
Finder copy is still unambiguous. CardBodyTextView's paste-yield mechanism
needed no change at all — it forwards by capability, not by picture-specific
logic, so a screenshot ⌘V with the body editor focused is now a genuine
no-op there, served by the new control instead.

The pasteboard's re-read gains a fourth checkpoint — a window becoming key —
alongside menu-tracking, ⌘-down and app activation: a persistent visible
control has to read true continuously while its window is frontmost, not
only at the instant a menu or chord probes it.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 10:37:55 -04:00
parent 546fd2840f
commit 200fbce276
8 changed files with 343 additions and 49 deletions
+28 -3
View File
@@ -48,9 +48,9 @@ import os
///
/// `NSPasteboard.changeCount` is a machine-wide counter, so a value that moved without this store
/// moving it means another app owns the pasteboard now. It is checked exactly where 04 says menu
/// validation, app activation, and before every paste and nowhere else. `payload` is observable
/// state rather than a computed pasteboard read precisely so the menu items' enablement re-evaluates
/// when it changes rather than whenever SwiftUI happens to rebuild them.
/// validation, app activation, before every paste, and a window becoming key and nowhere else.
/// `payload` is observable state rather than a computed pasteboard read precisely so the menu items'
/// enablement re-evaluates when it changes rather than whenever SwiftUI happens to rebuild them.
///
/// **"Menu validation" is two checkpoints here, not a hook**: the commands validate by conditional
/// responder attachment (`ClipboardCommands` availability *is* the handler's presence), which
@@ -62,6 +62,15 @@ import os
/// screenshot hotkey, 4 would leave V dead until the next app switch, which is the image
/// branch's headline gesture failing in the exact case it was built for. Both checkpoints are one
/// `changeCount` read in the common case, which is why they can afford to fire on every -chord.
///
/// **A window becoming key is the fourth checkpoint** (widened 2026-08-09 for the card window's
/// paste-image affordance, `CardPasteImageAffordance`): a visible control, unlike a menu item, has to
/// read true continuously while its window is frontmost rather than only at the instant something
/// probes it. Switching between two already-open windows of this app no menu opened, no pressed
/// crosses neither of the first two checkpoints and stays inside one app activation, so the third
/// checkpoint misses it too; `NSWindow.didBecomeKeyNotification` is the moment that is left. Fired
/// for every window rather than filtered to card windows, for the same cost reason: one more
/// `changeCount` read on an already-cheap path.
@MainActor
@Observable
public final class ClipboardStore {
@@ -196,6 +205,22 @@ public final class ClipboardStore {
) { [weak self] _ in
MainActor.assumeIsolated { self?.refresh() }
})
// **The fourth checkpoint** (type comment takeover, widened 2026-08-09 for the card
// window's paste-image affordance): any window becoming key. The first three checkpoints
// exist for *menu* validation, which only ever runs when something is about to fire but a
// visible control has to read true continuously while its window is frontmost, not only at
// the moment a chord or a menu happens to probe it. Switching between two already-open
// windows of this app crosses neither of those moments and stays inside one activation, so it
// needed a trigger of its own. Every window, not just card windows: cheap (one `changeCount`
// read in the common case, `refresh()`'s own guarantee) and simpler than teaching this store
// which window kind is asking.
stalenessObservers.append(NotificationCenter.default.addObserver(
forName: NSWindow.didBecomeKeyNotification,
object: nil,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated { self?.refresh() }
})
commandKeyToken = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
// Local monitors run on the main thread, before the event reaches its window
// (`LocalModifierFlipWatch`'s note) and the event is returned unchanged, always, for