Problem: a builder can be selected while rendering on no key
Row-1 selection pages by a hardcoded constant, not by how many builder keys exist. ROW1_WINDOW_SIZE = 4 (apps/streamdeck/src/store.ts:28), builderWindowStart() returns floor(cursor.builder / 4) * 4, and windowedBuilder(slotIndex) returns builders()[windowStart + slotIndex]. The page step is fixed at four and is independent of how many BuilderAction keys the user actually placed.
The consequence is a correctness bug, not a sizing preference. Place three builder keys instead of four and every builder at index ≡ 3 (mod 4) renders on no key at all — while the Select dial still walks onto it, so it becomes the selected builder, drives Row 2's entire palette and both review dials, and shows no accent ring anywhere on the board. The reviewer is acting on a builder the deck never displays. (There is no gap only while the fleet is no larger than the number of placed slots.)
It is also wrong for most hardware: Mini is 3×2, Standard 5×3, XL 8×4, and the SD+ keypad 4×2, so a constant of four fits exactly one device.
Why now
The trigger is concrete rather than hypothetical: #1463 adds an Open Architect key that the owner intends to place on a Row-1 key (safe, because its Main mode is selection-independent), which leaves three builder slots against a fixed page of four — the exact configuration that hides every fourth builder. The fix belongs to Row-1 windowing, not to the architect key, so #1463 ships with the fixed-4 behaviour documented and this issue carries the change.
Feasibility (verified against @elgato/streamdeck@2.1.0)
There is no profile-structure API — plugin/profiles.d.ts states verbatim that plugins "may only switch to profiles distributed with the plugin, as defined within the manifest, and cannot access user-defined profiles", and switchToProfile is the only profile call. So the layout cannot be read directly.
It can be derived from the lifecycle, which is the workable path: every visible BuilderAction fires willAppear carrying KeyAction.coordinates ({column, row}) and its device, and streamDeck.devices exposes Device.size ({columns, rows}) and type.
Proposed shape
- Window size = the number of currently visible
BuilderAction instances, replacing the constant.
- Slot order derived by sorting visible instances by
(row, column), which could retire the manual slot PI field or keep it as an override.
- Recompute on
willAppear / willDisappear, debounced so page-load settling does not thrash, and skipping instances whose coordinates are undefined (multi-action).
- Semantics are per visible page, which is correct: the window should match what is on screen.
Scope
store.ts (window math), actions.ts (BuilderAction lifecycle and slot resolution), builder-action.html (PI), cursor paging, and tests. Pairs naturally with the larger-profile thread (#1381). Protocol: PIR — the payoff is on hardware, across different key counts and at least one non-SD+ device if available.
Credit
Researched and proposed by the pir-1463 builder, surfaced by the owner during that lane's plan-approval discussion; SDK claims and the windowing math independently verified against source before filing.
Problem: a builder can be selected while rendering on no key
Row-1 selection pages by a hardcoded constant, not by how many builder keys exist.
ROW1_WINDOW_SIZE = 4(apps/streamdeck/src/store.ts:28),builderWindowStart()returnsfloor(cursor.builder / 4) * 4, andwindowedBuilder(slotIndex)returnsbuilders()[windowStart + slotIndex]. The page step is fixed at four and is independent of how manyBuilderActionkeys the user actually placed.The consequence is a correctness bug, not a sizing preference. Place three builder keys instead of four and every builder at index ≡ 3 (mod 4) renders on no key at all — while the Select dial still walks onto it, so it becomes the selected builder, drives Row 2's entire palette and both review dials, and shows no accent ring anywhere on the board. The reviewer is acting on a builder the deck never displays. (There is no gap only while the fleet is no larger than the number of placed slots.)
It is also wrong for most hardware: Mini is 3×2, Standard 5×3, XL 8×4, and the SD+ keypad 4×2, so a constant of four fits exactly one device.
Why now
The trigger is concrete rather than hypothetical: #1463 adds an Open Architect key that the owner intends to place on a Row-1 key (safe, because its Main mode is selection-independent), which leaves three builder slots against a fixed page of four — the exact configuration that hides every fourth builder. The fix belongs to Row-1 windowing, not to the architect key, so #1463 ships with the fixed-4 behaviour documented and this issue carries the change.
Feasibility (verified against
@elgato/streamdeck@2.1.0)There is no profile-structure API —
plugin/profiles.d.tsstates verbatim that plugins "may only switch to profiles distributed with the plugin, as defined within the manifest, and cannot access user-defined profiles", andswitchToProfileis the only profile call. So the layout cannot be read directly.It can be derived from the lifecycle, which is the workable path: every visible
BuilderActionfireswillAppearcarryingKeyAction.coordinates({column, row}) and its device, andstreamDeck.devicesexposesDevice.size({columns, rows}) and type.Proposed shape
BuilderActioninstances, replacing the constant.(row, column), which could retire the manualslotPI field or keep it as an override.willAppear/willDisappear, debounced so page-load settling does not thrash, and skipping instances whose coordinates are undefined (multi-action).Scope
store.ts(window math),actions.ts(BuilderActionlifecycle and slot resolution),builder-action.html(PI), cursor paging, and tests. Pairs naturally with the larger-profile thread (#1381). Protocol: PIR — the payoff is on hardware, across different key counts and at least one non-SD+ device if available.Credit
Researched and proposed by the pir-1463 builder, surfaced by the owner during that lane's plan-approval discussion; SDK claims and the windowing math independently verified against source before filing.