diff --git a/docs/superpowers/plans/2026-07-26-command-activation-semantics.md b/docs/superpowers/plans/2026-07-26-command-activation-semantics.md new file mode 100644 index 00000000..e2180d7d --- /dev/null +++ b/docs/superpowers/plans/2026-07-26-command-activation-semantics.md @@ -0,0 +1,354 @@ +# Command activation semantics + +**Status:** planned, 2026-07-26 +**Origin:** user reported that ⌥P (Prompt Template…) does nothing. Six classification +agents then swept all 99 catalog commands and the surface/store wiring. + +--- + +## The one-sentence rule this establishes + +> Invoking a command that shows a surface makes that surface visible; invoking it +> again dismisses it. That must be true from **every** invocation source, not just +> from inside the palette. + +Nothing in the app enforces this today. Three independent layers break it, and no +single-layer fix is sufficient — a toggle written in a command body is unreachable +if the router refuses the second keypress, and a router fix is pointless if the +command has no way to ask "am I already open?". + +--- + +## Layer 1 — the interaction-ownership gate makes dialogs un-dismissable by chord + +`useKeybinds.ts:333` returns out of the entire key handler when +`hasAppInteractionOwner()` is true. That marker is stamped by **every** Radix +`DialogContent` (`components/ui/dialog.tsx:57`). The binding router lives at +`useKeybinds.ts:579`, well after the bail. + +Consequence: **any surface built as a Dialog makes its own chord unreachable while +it is open.** The command's `run` is never called. This is not an admission +refusal — nothing in `when`, `resolveCommandAvailability` or the gateway is +involved, and no change confined to a `commands/*.ts` file can fix it. + +This is why the user's chords split the way they do: + +| Round-trips today | Dead second press | +|---|---| +| ⌥R Reader, ⌥S Spotlight (`MainSurface` inline) | ⌘⇧U Usage (Dialog) | +| Agent Status (plain `