# src/shell/actions — bindable REAPER actions, drag/drop shells, ingest

## Scope

The bindable action families routed through REAPER's `command_id`/`gaccel`/
`hookcommand` contract (Design View toggle actions, bank actions, the prune
action, the bank-package import action, and the shared registration
plumbing/table), plus the three drag-out
outcome shells (OS hand-off, instrument drop, arrange drop), plus the
extension-side ingest-through-the-bank shell. This is
where user-facing REAPER actions and OS-level drag/drop live; the underlying
mutation logic (bank verbs, prune's orphan computation, view-mode reconciliation)
is owned by other directories and only skinned here.

## Invariants

- **Ingest is an extension act; the instrument is a read-only bank consumer.** Any
  instrument code path that captures, imports, inserts a timeline item, or writes
  back into the bank is a bug — the instrument reads and plays only.
- **`arrange_drop_win` is the only timeline-placing shell IN THIS DIRECTORY** — the
  claim scopes here, not to the system: `shell/capture` holds two more
  (`RunInsertSelected` and `render_in_place`, the third verb). `arrange_drop_win`
  places because the USER dragged a card onto the arrange. Root `CLAUDE.md`'s
  capture/placement separation forbids a CAPTURE placing an item; a deliberate drop
  is placement on demand. No other module here may grow an `InsertMedia` call.
- **Ingest NEVER inserts a timeline item.** Arrange capture→bank→assign reuses the
  existing capture add-path and assigns the resulting `Sample` id to the target
  instance; it never places anything on the timeline — capture/placement
  separation is load-bearing here same as everywhere else. Only the arrange-capture
  surface writes the `assignment_request` wire; Media-Explorer import and file-drop
  onto the bank panel do not, and neither affects a live instance's selection.
- **Every bank index verb wraps its mutation in a batched REAPER undo point**
  (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is
  one Ctrl-Z.
- **The prune action is the ONLY file-deletion action in the system**; it opens no
  undo point (file deletion is not REAPER-undoable). It halts on
  `blockedByTracking` and prints whichever blockers fired — the malformed ledger,
  the offending `rsusage_*` key names, or both.

## Modules

- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns), and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank.
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Builds and shows every message the import produces, but the ledger-refusal body itself is `core/package::ledgerRefusalMessage` — a pure fold this TU only supplies the channel-correct namespace to — so the wording is assertable without a DAW. `doImportBankPackage`/`doImportBankPackageFile` return the minted bank id on a landed import (empty otherwise) so a caller can focus it; the verb itself is promptless.
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.** Surface (2)'s action is the one in this directory published into a NON-main action section (Media Explorer) as well as Main — two ids, one handler, two dispatch hooks; see root `CLAUDE.md` §"REAPER extension contract" for the mechanism.

## Gotchas

- **Structural wart, partly closed:** `ingest.cpp` / `ingest.h` now live in this
  directory. `ext_keys.h` and `resource.h` still sit at `src/` root: `ext_keys.h`
  is consumed mostly from `shell/instrument/`, so it is not this directory's to
  claim, and `resource.h` is a build input paired with `src/resource.rc` (the SWELL
  resgen step) rather than a shell module.
- Media-Explorer import is single-file, pull-on-action (`OpenMediaExplorer` +
  `MediaExplorerGetLastPlayedFileInfo`) — there is no enumerate-selected-files or
  register-a-drop-handler API on the Media Explorer surface.
- REAPER exposes no drag-drop registration API; drop handling is only on
  ReaSampler's own HWNDs (`WM_DROPFILES`/`IDropTarget` on the docked `bank_panel`).
  A drop onto the VST3 editor window relaying to the extension is an unproven
  spike, not a shipped path.
