Split root CLAUDE.md into 19 per-directory files scoped to their source area. Roll v0 history into docs/ARCHIVE.md; retire CONTEXT.md, CONTEXT-ARCHIVE.md, PLAN.md, COMPLETED.md. Move plan docs under docs/. Rescue 9 live deferrals into docs/TODO.md.
35 KiB
Multi-bank — product notes
Framing, rationale, and design-direction calls behind the Multi-bank phase.
The tickable spec's landed history lives in docs/ARCHIVE.md (Phase B) and the
architecture detail in src/core/model/CLAUDE.md + src/shell/bank_ops/CLAUDE.md
(§Multi-bank). This doc holds the why — the workflow
narrative, the pool-privilege reasoning, the movement semantics, and the
design-direction recommendations — so those don't clutter the build docs.
Status: framed by product-designer (2026-07-23). All five forks settled by Daniel (2026-07-23) — fork 5 (tab rendering + move affordance) is now closed; the record at the bottom carries the decision and rationale. Settled forks are folded into the prose below. Only the active-bank indicator placement remains an open B4 polish detail.
What it is (and what it is not)
Multi-bank generalizes the single per-project bank into a pool plus named
banks. Today ReaSampler has exactly one bank (BankIndex) — every capture lands
in it, and it is the whole library. That is fine at ten samples and unusable at two
hundred. Multi-bank keeps the one bank as the pool — the default catch-all
every capture still lands in — and lets the user create named banks ("Drums",
"1-Shots", "Synth Hits") that group samples for a purpose. Move is the primary
gesture — a sample relocates from one bank to another, living in exactly one bank at
a time — with copy the deliberate secondary act when the user wants a sample in
two banks at once. One bank at a time is the active bank: the capture target. The
pool is active by default.
It is not a new file layout. Banks are logical groupings over one shared file
pool, not folders on disk. Moving a sample between banks moves an index entry, not
a .wav. This is deliberate: it keeps movement cheap and non-destructive, and it
keeps every file exactly where M4's project-relative path resolution already puts
it. Anyone expecting "move to Drums" to reorganize the bank folder on disk will be
surprised — and that expectation is headed off in the spec, not discovered in
review. (See movement semantics below.)
It is not a change to how capture works. The capture pillar's load-bearing rule
stands untouched: capturing writes a file plus an index entry and never inserts into
the arrange. Multi-bank changes exactly one thing — which index the entry lands
in. Everything downstream of "add a Sample to a BankIndex" is unchanged.
The pool is to banks what Arrange is to modes
This phase deliberately reuses the structural pattern Phase D established for modes, because the problems are the same shape.
In Design View, Arrange is structurally just another mode in an N-mode registry, but semantically privileged: it is the default home for every untagged leaf. The data model is uniform (no Arrange-shaped special type); the rules layer enforces Arrange's privilege.
In Multi-bank, the pool is structurally just one BankIndex in an N-bank
registry, but semantically privileged: it always exists, is un-deletable, and is
un-renamable, and it is the default capture target and the default home for a
sample that isn't grouped anywhere else. Same move: uniform data model (a bank is a
bank), privilege enforced in the rules layer (bank_book rejects delete-pool /
rename-pool / zero-banks).
Why this over a pool-shaped special type? A special pool type would fork every operation into "pool path" and "named-bank path" — serialize, iterate, render, move-target-resolution all branch. Treating the pool as bank-zero with three enforced rules keeps one code path and one JSON shape, and the privileges live in exactly one place. This is the same reasoning that kept Arrange from being a boolean special case.
Why a container, not a bank-id on the sample
The obvious alternative to a container is a bankId field on Sample plus one flat
BankIndex: filter by bankId to get a bank's contents. Rejected, for three
reasons:
- It modifies
BankIndex, the tested heart. The whole discipline is thatbank_modelis the pure, hard-tested core. AbankIdfield threads bank awareness through the one module that should stay bank-agnostic. A container wrapsBankIndexand leaves it byte-for-byte as tested. - Dedup-by-hash wants per-bank scope.
BankIndexalready dedups by content hash within itself. That is exactly the semantics we want per bank — and it is what makes copy meaningful (the same hash can live in the pool and in "Drums"). A flat index with abankIdfield would need dedup to become "dedup within abankIdpartition," reimplementing per-bank scoping that a container gets for free. - It mirrors the two pure cores we already have.
bank_modelandview_mode_modelare both "a pure registry with JSON round-trip, unit-tested outside the DAW."bank_bookis the third instance of that exact pattern. AbankIdfield would be a fourth, different pattern bolted onto the first.
So: bank_book is an ordered registry of { bank id, display name, ordinal, BankIndex }, pool seeded as bank-zero. Bank id is the stable key (minted GUID-style
on create); name and ordinal are mutable. Display names are unique — two banks
cannot share a name (compared trimmed + case-insensitively, so "Drums" and "drums"
are the same name), enforced in the model on create and rename; the pool's "Pool" is
reserved by the same rule. BankIndex is untouched. This is the defer-the-feature,
design-the-seam principle: the seam is a container above the tested core, not a
modification of it.
Movement semantics (the settled rules, in prose)
- Move is index-only, and is the default gesture. Moving a sample from bank A to
bank B removes the
Samplefrom A'sBankIndexand adds it to B's. The file never moves — one shared file pool under the project bank folder, exactly where M4 put it. Cheap, non-destructive, immune to the path-rewrite bug class M4 closed. After a move the sample lives in exactly one bank; this is the primary, low-friction affordance (see design-direction, below). - Copy is index-only too, and is the deliberate secondary act. Copy adds the sample to B and leaves it in A. Two index entries, one file, two banks. Copy is the mechanism that lets a sample live in the pool and in a named group at once — or in two named groups. It is the explicit way to keep a sample in more than one place; move (the default) puts it in one place.
- Dedup-by-hash is per-bank, and collapse is observed across a move. Each
BankIndexdedups within itself, unchanged. If you move (or copy) a sample into a bank that already holds its hash, the destination collapses onto its existing entry — the same collapseBankIndex::addalready does, now visible across a move. On a move the source entry is still removed, so the sample ends up in the destination once, as expected. - No cross-bank dedup. The same hash may exist in the pool and in a named bank simultaneously. That is the whole point of copy — do not add a global dedup that collapses across banks (guardrail in the spec).
Why index-only movement over moving files into per-bank subfolders? The subfolder approach makes "which bank" a filesystem fact, which reads tidy — but it reintroduces exactly the path-rewrite fragility M4 spent three iterations eliminating (Save-As relocation, relative-path resolution, GUID-primary identity). Every bank-move would be a file operation that has to stay non-destructive and relative-path-correct across Save-As. Index-only movement sidesteps all of it: files never move, so paths never rewrite, so none of that machinery is re-exercised. Banks are a view concept, files are a storage concept, and keeping them separate is what keeps movement trivially correct.
Persistence and migration
The book rides the existing "reasampler" project ext-state namespace under a new
key, banks, alongside view_state and project_guid. Same namespace, same
travel-with-the-.rpp guarantee, same GUID-primary identity and Save-As-relocation
machinery from M4. One shared physical bank folder; one ext-state namespace.
The pool folds into the banks blob; the legacy bank_index key is retired
(fork 1, settled (a)). The pool's index is stored inside the banks blob as
bank-zero, exactly like every named bank — one blob, one section, one JSON shape.
The old top-level bank_index key is not written going forward; after migration the
banks blob is the single authoritative source for all bank state including the
pool. This is the cleaner shape and keeps the "pool is just bank-zero" data-model
uniformity honest all the way down to storage: the pool is not persisted
differently from a named bank.
Migration of an existing single-bank project is the load-bearing edge case. A
project saved before this phase has a bank_index key and no banks key. On load,
that legacy index becomes the pool's BankIndex, and the book is { pool } with
zero named banks. One-way, lossless promotion — no sample is lost, no path changes,
the user opens their old project and finds everything in the pool exactly as before,
now with the ability to add named banks. This must be a first-class, tested path
(B1 pure-model migration + B2 in-DAW load), not an afterthought.
The migration is a one-time promotion, and it is one-way. On load: legacy
bank_index present, banks absent → promote the legacy index into the pool inside
a freshly-minted banks blob, then treat the blob as authoritative. Once the banks
blob exists it is the only source consulted; the legacy key is not read back or
kept in sync. A project touched by this version and saved will carry banks and
(going forward) will not carry a written bank_index — the retirement is complete
after the first save. The promotion is lossless: no sample, no path, no ordinal is
dropped. This trades a small amount of forward-only commitment (a project saved by
this version will not re-open cleanly in a pre-multi-bank build that only knows
bank_index) for the clean single-blob shape — an acceptable, one-directional
migration, consistent with how M4 project state already moves forward.
Precision-invariant implications
Multi-bank sits above the file entirely, so the capture-side invariants (determinism, bit-identical repeats, null test, exact bounds) are untouched — they are properties of the capture path and the file. The invariants it does touch:
- Relative-paths-only is enforced N times instead of once — once per
BankIndexin the book, by the exact code that enforces it today. Movement adds no path handling because files never relocate. - Non-destructive extends to bank operations: create / rename / delete / activate / evacuate and sample move / copy mutate only index + ext-state. No file is written, moved, or deleted; no timeline item is touched. In particular, deleting a named bank drops its member index entries but does not delete their files — file lifecycle stays owned by the capture/prune path, never the bank container (fork 2, settled). A file that was referenced only by the deleted bank becomes an orphan on disk — present but indexed by no bank — until the capture/prune path reclaims it. That orphaned-until-prune window is a designed consequence of "delete drops members, files persist," not an accident (see design-direction → evacuate below for how the workflow avoids it).
- Travels-with-the-.rpp is preserved by riding the M4 machinery unchanged.
Design-direction (settled forks 2–4, plus rationale)
Move is the default; the pool is a bank, not a permanent mirror
Settled (fork 3): move is the primary, low-friction gesture; copy is the deliberate secondary act. This inverts the earlier framing (which leaned copy-as-default on a "pool is the complete library, banks are views" model). Daniel's call is a partition mental model: a sample lives in one bank at a time, and moving it to "Drums" takes it out of the pool. Copy remains available for the user who genuinely wants a sample in two banks at once, but it is the explicit, deliberate act — not the default.
This changes what "the pool is the complete library" means, and the docs must be honest about it. Under move-as-default the pool is not a permanent mirror of everything ever captured — it is the default catch-all every capture lands in and the from-which/back-to-which home for organizing, but once a user moves a sample into a named bank it is no longer in the pool. The honest framing is:
- The pool is the default home and the staging ground, not a guaranteed superset. Fresh captures land there; from there the user moves them into named banks. What is "in the pool" is what hasn't been filed yet plus whatever the user deliberately keeps there.
- Named banks are where filed samples live, not lightweight views over a still-complete library. A sample in "Drums" is in Drums and nowhere else unless copied.
- Copy is the escape hatch for the "I want this in two places" case — borrowed from the move-vs-copy distinction every file manager makes. It is deliberately the less prominent of the two so the default stays a clean partition.
Prior art: this is the file-manager / DAW-media-pool model (move relocates, copy duplicates) rather than the playlist-library model. It suits a capture-heavy workflow where the user is actively filing rather than curating views over a fixed corpus.
Evacuate: the "keep the samples, drop the grouping" gesture
Settled (fork 2): delete drops a bank's member index entries; a separate evacuate operation moves all members back to the pool. These are two distinct verbs for two distinct intents:
- Delete removes the bank and its member index entries. Files are not touched (capture/prune owns file lifecycle). Use it when you want the grouping and its contents' index entries gone.
- Evacuate moves every member of the bank back to the pool (index-only, observing the same destination-collapse-by-hash as a normal move), leaving the bank empty. Use it when you want to dissolve the grouping but keep the samples.
The intended workflow is "evacuate, then delete" when the user wants to keep the
samples and drop only the grouping, versus a plain delete when they want the
grouping and its members gone. Evacuate is a pure bank_book operation (B1) and a
bindable action (B3). Pool-privilege rules still apply: you cannot evacuate the pool
(it has nowhere to evacuate to, and it is the evacuation destination).
The non-empty-delete hazard, and the recommended guardrail. Because delete drops members (rather than reabsorbing them, the earlier lean) and move is the default (so a sample is often in exactly one bank), a plain delete of a non-empty named bank can orphan those samples out of every index — their files persist on disk until prune, but no bank references them. That is the orphaned-until-prune window called out in the precision-invariant section. It is a real footgun: "I deleted Drums and my kick is gone from the library."
Recommendation (B4, panel-polish, pending Daniel): confirm on non-empty delete, with the confirmation naming the escape hatch. Something like "Delete 'Drums'? Its 14 samples will be removed from the library (files remain on disk until pruned). Evacuate to pool instead?" — a three-way affordance (Delete / Evacuate then delete / Cancel) turns the hazard into a teaching moment for the evacuate verb. An empty bank deletes with no confirmation (nothing to lose). This keeps delete fast where it's safe and guarded where it bites. I recommend adopting the confirm-on-non-empty guardrail; the exact copy and button set are a B4 detail.
The vertical split: pool on top, named banks as a tab strip below
Daniel's directive is a vertical split — pool on top, named banks below — with full-height toggles for either region. This reads well against the "pool is the library, banks are views" model: the library is the persistent top region, the curated views are the swappable bottom region.
- Named banks as a tab strip (one tab per named bank, one bank visible at a time in the region) rather than a stack of grids. A tab strip scales to many banks without eating vertical space, and "one active tab" pairs with "one active bank" cleanly (though shown tab and capture-active bank are distinct — see below). Borrowed from browser/IDE tab strips and sample-library browsers (Ableton's collections rail, Kontakt's multi rack).
- Full-height toggles collapse the split to one region: pool full-height (hide named banks — "I'm just capturing into the pool right now") and banks full-height (hide the pool — "I'm organizing"). These are mode-of-work toggles, not layout fiddling, and they map to the two halves of the workflow (capture vs. curate).
Active bank and shown tab stay distinct (settled fork 4)
Settled (fork 4): the capture-active bank and the currently-shown named-bank tab are distinct concepts, and the active bank carries an unmistakable visual indicator. The capture-active bank (where new captures land) and the currently-shown tab are different things — you might be looking at "Drums" while capturing into the pool, or vice versa. Browsing a tab never retargets capture. Conflating them ("the tab you're viewing is the capture target") was considered and rejected: it makes it too easy to capture into the wrong bank by merely browsing, and capture-into-wrong-bank is a silent error the user won't notice until later.
Active bank is an explicit state, set by an explicit "activate" affordance (and action), not implied by which tab is shown. The pool is active by default; activating a named bank is a deliberate act. The indicator must be visually unmistakable — this is now a settled constraint, not a suggestion. The capture target is a thing the user must be able to verify at a glance before hitting capture. The indicator's exact placement (per-region header vs. single header readout vs. lit-tab treatment) remains a B4 panel-polish detail — but that it reads unmistakably is fixed.
The Design View mode switch and multi-bank are orthogonal — and should read that way
The window header already carries the Design View segmented mode switch
([ Arrange | Design ]). That governs timeline visibility. Multi-bank governs
sample grouping. They are fully orthogonal — you can be in Design mode capturing
into "Synth Hits," or in Arrange mode capturing into the pool. The UI must not
suggest a coupling: keep the mode switch where it is (header, timeline concern) and
the bank controls in the bank body (library concern). Do not, for instance, put bank
tabs next to the mode segments as if they were the same kind of switch.
Tagging vs. banking are different verbs — don't blur them
Worth stating because both phases involve "putting a thing into a named group." Design View tags tracks into modes (a track-visibility concern). Multi-bank moves samples into banks (a library-organization concern). Different objects (tracks vs. samples), different purpose (timeline stance vs. library grouping). The vocabulary should stay distinct — "tag into Design" vs. "move to Drums" — so a user never conflates the two systems. They rhyme structurally (both are N-collections with a privileged default) but they are not the same feature and should not share UI metaphors beyond what's genuinely shared.
Action set (proposed)
Stable command-id strings follow the sampler family prefix. Bank-activate and move/copy are MIDI-bindable to suit the capture-heavy, hands-on workflow.
- Create bank / Rename bank / Delete bank — manage the named-bank set. (Pool is un-deletable / un-renamable — the actions refuse on the pool.) Delete on a non-empty bank confirms (see evacuate, above).
- Evacuate bank → pool — move all of a bank's members back to the pool, leaving it empty; the "keep the samples, drop the grouping" verb. (Refuses on the pool.)
- Activate bank (direct-by-id) / Cycle active bank — set the capture target; distinct from browsing a tab.
- Move selected samples → bank / Copy selected samples → bank — the two movement verbs; move is the low-friction default, copy the deliberate secondary.
- Pool full-height / Banks full-height (toggles) — collapse the split.
Module breakdown (pure / persist / actions / UI)
Mirrors the capture and Design View pillars exactly.
Pure bank_book (REAPER-free, unit-tested — the mirror of bank_model /
view_mode_model):
- Ordered bank registry:
{ bank id, display name, ordinal, BankIndex }; pool seeded with fixed id + fixed name. - Create / rename / reorder / delete named banks; pool-privilege rules enforced here (reject delete-pool, reject rename-pool, never zero banks). Display names are unique — create/rename reject a name already used by another bank (trimmed + case-insensitive; the pool's "Pool" is protected).
- Active-bank id (get/set, defaults to pool); resolve the active bank's
BankIndex. - Move / copy a sample between banks — index-only, destination collapse-by-hash observed, move removes the source entry.
- Evacuate a bank — move every member to the pool (index-only, same destination collapse-by-hash as move), leaving the bank empty; pool cannot be evacuated.
- JSON round-trip of the whole book (banks + per-bank indices + ordinals + active
id), pool included as bank-zero inside the blob; legacy-
bank_index→pool migration on parse (one-way, blob becomes authoritative). BankIndexis untouched —bank_bookwraps, never modifies it.
persist slice:
- Serialize/deserialize the book under the
bankskey in"reasampler"(shared namespace, distinct section fromview_state; the pool rides inside thebanksblob as bank-zero — no separatebank_indexkey going forward). - Migrate a legacy
bank_indexkey into the pool on first load (one-way, lossless; after migration thebanksblob is authoritative and the legacy key is retired). - Reload-on-open and Save-As survival via the existing M4 machinery; the session
exposes the book; the active bank's
BankIndexis the capture add target.
actions entries: the set listed above (including evacuate bank → pool),
registered with the command_id / gaccel / hookcommand pattern; bank-activate +
move/copy + evacuate MIDI-bindable.
UI (extending the M5 bank_panel): the vertical split (pool grid top,
named-banks tab-page region bottom); the two full-height toggles; the active-bank
indicator; create / rename / delete / activate affordances; the sample move/copy
affordance (drag between regions and/or a "send to bank" menu on selection). Reuses
the M5 LICE grid render loop per region.
Settled forks (Daniel, 2026-07-23)
All five forks are now decided; their implications are folded into the prose above. Recorded here as a scan-line so the decisions don't have to be reconstructed from the body text.
- Persistence key — (a), retire
bank_index. The pool folds into thebanksblob as bank-zero; the legacybank_indexkey is retired after a one-way, lossless migration; the blob is authoritative thereafter. (See persistence and migration.) - Delete drops members; add an evacuate verb. Deleting a named bank drops its member index entries (files untouched, owned by capture/prune). A separate evacuate operation moves all members back to the pool. Workflow: "evacuate then delete" to keep samples; plain delete to drop the grouping and its members. Non-empty delete should confirm and offer evacuate. (See design-direction → evacuate; orphaned-until-prune consequence in precision-invariant implications.)
- Move is the default gesture; copy is the deliberate secondary. A sample lives in one bank at a time; copy is the explicit "in two places" escape hatch. (See design-direction → move is the default.)
- Active bank and shown tab stay distinct; active bank gets an unmistakable indicator. Browsing never retargets capture; the "visually unmistakable" requirement is a settled constraint (placement remains a B4 detail). (See design-direction → active bank and shown tab stay distinct.)
- Named-banks tabs are LICE-drawn; move ships as both menu and drag. (5a) The tab strip is LICE-drawn to match the M5 grid and the Design View segmented switch — not a SWELL-native tab control — with an overflow/scroll affordance built in from the start so the strip scales past the ~8–12-tab breakdown point. (5b) Move ships as both a "move to bank" / "send to bank" menu on the current selection (the precise, MIDI-bindable front-end for the B3 move action) and drag-between-regions (the direct-manipulation accelerator); copy stays the deliberate secondary act via the menu. Drag mis-drop is mitigated by clear drop-target highlighting and is recoverable by design (move is index-only and reversible). (See Fork 5 — settled below.)
Fork 5 — settled (Daniel, 2026-07-23)
Two sub-questions, both B4 panel-build mechanics — neither phase-defining, both shaping the panel's feel. The full options analysis that produced these calls is retained below the decision for the record; the decision is what governs B4.
Decision.
- 5a — LICE-drawn custom tabs, matching the M5 grid aesthetic and the Design View
[ Arrange | Design ]segmented-switch precedent. Overflow/scroll is in scope from the start (chevron or scroll affordance), not deferred — a naive fixed-width LICE strip breaks down at ~8–12 tabs, so the panel must scale from B4 onward. - 5b — both a "move to bank" menu and drag-between-regions. The menu is the precise, MIDI-bindable front-end for the B3 move action; drag is the direct-manipulation accelerator. Copy remains available as the deliberate secondary act, offered on the menu. Drag mis-drop is a designed-for property, not an afterthought: the destination region/tab shows clear drop-target highlighting during a drag, and because move is index-only and move-is-default, a mis-drop is recoverable (the user moves the sample back) rather than destructive — state this as the safety net, not a hope.
Rationale (brief). 5a: visual consistency across one coherent custom-drawn surface plus full cross-platform certainty (we own every pixel, no SWELL tab-parity risk) beats buying keyboard-nav/overflow from an inconsistent native widget; the segmented switch is a partial precedent to extend. Overflow is pulled forward because banks routinely running past a handful is plausible and a backfill after the naive strip ships would be a visible cliff. 5b: the menu carries the precise/bindable path (and is the natural front-end for the already-bindable B3 action, consistent with how the M5 grid and Design View act on the current selection); drag serves users who prefer direct manipulation — the pattern every media manager (Finder, Lightroom, Ableton browser) ships. The mis-drop hazard that argued against drag-only is defused by highlighting + reversibility, so drag is safe as the accelerator.
Verify before building. LICE tab drawing and any SWELL surface used for the menu/drag hit-testing must be verified against the M5 reference and the SWELL/SDK headers before use — even having chosen LICE, confirm what a native fallback would offer, and confirm the drag hit-test doesn't collide with the M5 grid's existing multi-select drag.
Fork 5 — options analysis (retained for the record)
The analysis that produced the decision above. Kept so the reasoning behind the LICE-tabs / both-affordances calls doesn't have to be reconstructed.
5a — Named-banks tab rendering: SWELL-native tabs vs. LICE-drawn custom tabs
The named-banks region needs a tab strip (one tab per named bank). Two ways to draw it.
Option A — SWELL-native tab control (SWELL's tab-control, the cross-platform
Win32 SysTabControl32 analog). Verify against the SWELL headers — SWELL tab-control
availability and behavior-parity across Win/mac/Linux is a known soft spot and must be
checked, not assumed; the SWS extension is the reference for which SWELL controls are
safe to lean on.
- Pros: near-zero custom draw/hit-test code; keyboard navigation (arrow between tabs, focus handling) comes for free on platforms where SWELL implements it; gets native-ish affordances (overflow, tooltips) without hand-rolling.
- Cons: visual inconsistency — a native tab control will not match the M5 LICE grid aesthetic or the Design View segmented switch; it will read as a bolted-on OS widget in an otherwise custom-drawn surface. HiDPI/scaling behavior of SWELL controls is uneven across platforms. Cross-platform parity is the real risk: the control may look and behave three different ways on Win/mac/Linux, and SWELL's tab implementation is less battle-tested than its buttons/edits.
- Scaling: native tab controls handle many tabs via built-in overflow/scroll, so it degrades more gracefully past ~10–15 banks than a naive custom strip would.
Option B — LICE-drawn custom tabs matching the grid aesthetic (the same render
path M5 already uses, and the same approach the Design View segmented mode switch
[ Arrange | Design ] took).
- Pros: visual consistency — tabs match the grid and the existing segmented switch; one coherent custom-drawn surface. Full control over HiDPI (draw at the panel's own scale factor, as M5 already must). Identical look and behavior on all three platforms because we own every pixel — no SWELL-parity risk. Precedent exists in-repo: the Design View segmented switch is already a LICE-drawn multi-segment selector, so the pattern and hit-testing are partly solved.
- Cons: we hand-roll hit-testing, keyboard navigation, and overflow. Keyboard nav (tab/arrow to move between banks) is real work we'd otherwise get free from a native control. Overflow past the visible width needs a scroll or chevron affordance we build ourselves.
- Scaling: a naive fixed-width custom strip breaks down when named banks exceed the region width (~8–12 tabs depending on name length) unless we build scroll/overflow. This is the sharpest cost of B — but the Design View switch only ever has two segments, so its precedent doesn't cover the many-tabs case; we'd extend it.
Framing note: the choice is largely "buy keyboard-nav + overflow from SWELL and pay in visual inconsistency + cross-platform risk" (A) vs. "pay to build keyboard-nav + overflow and get visual consistency + platform certainty" (B). How many named banks we expect in practice is the swing factor: if a user rarely exceeds ~6–8 banks, B's overflow cost is small and its consistency win is large; if banks routinely run into the dozens, A's built-in overflow starts to matter.
5b — Move affordance: drag-between-regions vs. "send to bank" menu vs. both
The move gesture (now the default, per fork 3) needs a surface. Three shapes.
Option A — drag between regions (drag selected samples from the pool grid onto a named-bank tab, or between tabs).
- Pros: spatially intuitive; matches the mental model ("put this there"); direct.
- Cons: precision / mis-drop risk is real on a dense grid — dragging onto the wrong tab or dropping in dead space is easy, and a mis-drop that silently moves a sample to the wrong bank is exactly the "silent wrong-bank" error we're guarding against elsewhere. Not keyboard- or MIDI-bindable — but B3 already specifies the move action is MIDI-bindable, so drag can't be the only surface. Drag also has to interoperate cleanly with the M5 grid's existing multi-select drag behavior (verify there's no gesture collision).
Option B — "send to bank" / "move to bank" menu on the current selection (a context menu or a toolbar dropdown listing banks; the selected samples move to the chosen bank).
- Pros: precise — you pick the destination from a named list, no mis-drop. Maps
directly onto the B3 bindable action (the menu is just a visual front-end for the
same
move → bankIdoperation, so it's consistent with keyboard/MIDI binding). Consistent with how the M5 grid already handles selection (act-on-current-selection, not per-item widgets — the Design View tag/untag already works this way). Scales to many banks (a menu list handles dozens of destinations fine). Cheapest to build in the LICE/SWELL surface (a menu, not a drag-and-drop hit-test system). - Cons: less discoverable than drag for a first-time user; an extra click vs. a direct drag; feels less "direct-manipulation" than dragging.
Option C — both. Menu as the precise, bindable, discoverable-via-right-click primary; drag as the direct-manipulation accelerator for users who prefer it.
- Pros: covers both interaction styles; the menu carries the bindable/precise path while drag serves direct manipulation. This is what most media managers do (Finder, Lightroom, Ableton browser all offer drag and a menu/right-click move).
- Cons: most implementation cost (build both, and make them agree); drag still carries its mis-drop risk even as a secondary path.
Recommendations that fed the decision (now settled above)
These were the product-designer recommendations Daniel weighed; the settled record at the top of Fork 5 is authoritative where it differs (notably 5b, which Daniel took all the way to "both" with overflow and drop-highlighting pulled forward).
- 5a — recommend LICE-drawn custom tabs (Option B). Visual consistency with the M5 grid and the Design View segmented switch is worth the hand-rolled keyboard-nav and overflow, and it removes the cross-platform SWELL-parity risk entirely (we own every pixel on all three platforms). The Design View segmented switch is a partial precedent to extend rather than a from-scratch build. The one thing that would flip me to native tabs: evidence that users routinely run dozens of named banks, where built-in overflow earns its keep. Recommend B; revisit if the expected bank count is high. Verify SWELL tab-control availability against the headers regardless — even choosing B, we should know what the native fallback offers.
- 5b — recommend the "send to bank" menu as primary (Option B), with drag as a later accelerator (toward C). The menu is precise, cheap, scales to many banks, is the natural front-end for the already-bindable B3 move action, and matches how the M5 grid and Design View already act on the current selection. It sidesteps the mis-drop hazard that would otherwise fight our "no silent wrong-bank" stance. Drag is a nice accelerator but should not be the primary or only surface (it can't be MIDI-bound, and B3 requires the action be bindable). Recommend menu-first; add drag in a later B4 pass if it earns the cost. Net: ship B, leave the seam for C.