GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

TOOLDECK_PLAN.md



RELEASE

planning/2.8.0/plans/TOOLDECK_PLAN.md

sha256 96d371adb1050416 · 30447 bytes · original held in the private archive

# TOOL DECK + QUICK-DRAW HOLSTER — Implementation Plan > **Protocol 8, Stage 1 (Opus diagnose & plan).** No code was edited to produce this. > Branch: `dev` (reset to `a247fa2`). Target unit: the command-cluster overhaul — > **Option B "Tool Deck"** + the redesigned **"Quick-Draw Holster"** D-PAD, plus a new > native state field `state.padBindings` (Protocol 4). > Design source of truth: `planning/mockups/command-cluster-final.html` (+ `-notes.md`, > screenshots `command-cluster-final-412-deck.png` / `-dpad.png` / `-dpad-bound.png` / > `-desktop.png`). Owner has signed off on this mockup. --- ## 1. Objective Replace the current always-present (desktop) / collapsible (mobile) **COMMAND CLUSTER** sub-panel — the `.d-pad` block, `#macroTarget` input, and the seven bracket-buttons (`[THREAT]`/`[VATS]`/`[TRADE]`/`[LOOT]`/`[CONSULT]`/`V.A.T.S. CALCULATOR`/`> TERMLINK CONSOLE`) — with: 1. **Tool Deck** — zero standing footprint. A new dashed **◈ launcher key** in the composer toolbar raises an in-glass bottom-sheet DECK: one shared TARGET/ITEM/TOPIC field + six keycap-chip tool rows (THREAT · VATS · TRADE · LOOT · CONSULT · CALC). Scrim-tap / ✕ / Esc dismiss. **TERMLINK and CROSSROADS are out of the deck** (retired from the cluster; both remain typable native commands). 2. **Quick-Draw Holster** — the redesigned D-PAD: four labelled **gear-vector sockets** (▲◄►▼) at the deck foot, each showing its direction glyph, an LED (lit = bound), and the bound gear's name. Tap fires it (`[PAD: DIR]`); a **BIND ▸** key arms a two-tap holster flow reusing the deck's target field (`[BIND: gear, DIR]`). 3. **`state.padBindings`** — a NEW native, player-controlled state field so the socket labels have a real source of truth (today the bindings live nowhere durable — see §3). The tools' invocation wiring is **reused, never forked** (Protocol 22). --- ## 2. Confirmed current-state trace (Protocol 27 — reproduce before fixing) ### 2.1 The cluster markup `index.html` lines **2536–2660**: `<details class="sub-panel uplink-tray" data-sub-id="uplinkCommandTray">` containing `.tactical-dashboard` → `.d-pad` (4 arrows) + `.macro-actions` (`#macroTarget`) + `.macro-buttons` (the 7 buttons). It sits inside `.col-right > .chat-panel`, AFTER the `.transcript-card` and the hidden `#imageInput`. ### 2.2 How each tool is invoked today | Button | Handler (`index.html`) | Path | |---|---|---| | ▲◄►▼ | `macroCommand('[PAD: DIR]')` | → `transmitMessage()` → `_routeNativeCommand()` **no match** → **AI** | | `[THREAT]` | `macroCommand('[THREAT]')` | → router `'[THREAT]'` → `renderThreat(target)` (native, sysModal) | | `[VATS]` | `macroCommand('[VATS SIM]')` | → router `'[VATS SIM]'` → `showVATSOverlay()` (native) | | `[TRADE]` | `expandPanelForCategory('trade')` | native panel (INV tab) | | `[LOOT]` | `renderLoot()` | native salvage terminal | | `[CONSULT]` | `macroCommand('[CONSULT]')` | → router `'[CONSULT]'` → `renderConsult(topic)` (native) | | `V.A.T.S. CALCULATOR` | `showVATSOverlay()` | native overlay | | `> TERMLINK CONSOLE` | `macroCommand('[TERMLINK]')` | → router `'[TERMLINK]'` → `showTermlinkConsole()` | `macroCommand(actionStr)` (`ui-core.js:4207`): reads `#macroTarget`, appends it to `actionStr` **unless** the command contains `PAD:`, injects `> {finalCmd}` into `#chatInput`, calls `transmitMessage()`. **Only caller is the cluster** (no other JS references — verified). ### 2.3 CRITICAL: how `[PAD:]` / `[BIND:]` work today - **Neither is in `NATIVE_COMMAND_ROUTER`** (`api.js:1065–1105`) and **neither appears anywhere in `getSystemDirective()`** (grep of `api.js` for `PAD`/`BIND` → zero directive hits). - So `[PAD: UP]` and `[BIND: X, DIR]` fall straight through `_routeNativeCommand()` to the **Gemini AI as raw text**, with **no schema, no directive, no persisted binding**. The only "memory" of a binding is whatever survives in the rolling chat history. Fable's finding confirmed: **bindings are NOT native state.** They are effectively unsupported — the four arrows are anonymous and the BIND command is undiscoverable (only surfaced in the `[FEATURES]` help list, `ui-core.js:3404–3405`). - Historical note: `state.js:1863` still deletes a legacy `s.macros` field ("D-Pad handles this natively") — the D-Pad never actually got a native store. `padBindings` finally gives it one. This is the architectural justification for the new field: **the player, not the AI, owns their quick-slots** (player-authority principle; Protocol 24). --- ## 3. Acceptance criteria (Protocol 26 — Definition of Done) Verified on the **real rendered artifact** at 360 / 412 / ≥1000px, deck exercised live: 1. At rest, the UPLINK view shows **only** the Overseer scope + transcript-card (with the ◈ key in the composer toolbar). No cluster below the composer. `scrollWidth === innerWidth` at 360. 2. ◈ raises the deck (slide-up); scrim + ✕ + Esc + scrim-tap all close it; `aria-expanded` toggles on ◈. Focus moves into `#deckTarget` on open, back to ◈ on close. 3. Each tool row launches its **existing** handler and closes the deck: THREAT/CONSULT read `#deckTarget`; VATS/CALC → `showVATSOverlay()`; TRADE → `expandPanelForCategory('trade')`; LOOT → `renderLoot(deckTarget)`. 4. Holster renders 4 sockets from `state.padBindings`: bound = LED lit + uppercased gear name; empty = dimmed `EMPTY`, LED dark. Correct `aria-label` per socket. 5. **Deploy:** tapping a **bound** socket fires `_nativePadFire(dir)` → transmits a resolved action (`Deploy <gear>`) to the Director and closes the deck. Tapping an **empty** socket shows a hint and makes **no** AI call. 6. **Bind:** BIND ▸ arms (sockets dashed-pulse, hint line live); typing gear in `#deckTarget` then tapping a socket writes `state.padBindings[dir]`, persists, re-renders the socket (LED lights, name shows), logs a sys line, disarms. Tapping a socket with an empty target field shows "type gear first" and does **not** clobber the existing binding. 7. Typed `[BIND: gear, DIR]` and `[PAD: DIR]` (either input mode) route natively to the same `_nativePadBind` / `_nativePadFire` — the AI never sees them. 8. An AI response is **incapable** of altering `state.padBindings` (not in schema, not mapped in `autoImportState`). A cloud/file save **round-trips** padBindings (sanitize→migrate→apply). 9. An existing save with no `padBindings` migrates to the all-empty default (sockets read EMPTY). 10. PWA intact (installable / offline / service-worker); reduced-motion neutralizes the deck slide; desktop two-column shell unchanged; full gate (2028+ → new count) green in both runners. --- ## 4. The new native state field — `state.padBindings` (Protocol 4) **Shape:** `padBindings: { up: <string|null>, down: <string|null>, left: <string|null>, right: <string|null> }` — a fixed 4-key map, direction (lowercase) → gear name (trimmed string) or `null` when empty. Chosen over an array so a direction is a stable named slot; extra/unknown keys are dropped on sanitize. Protocol-4 checklist, all in the **same commit**: - [ ] **Default** in `let state = {…}` (`state.js:~1474`, near the other v2.x fields): `padBindings: { up: null, down: null, left: null, right: null },` (also captured by the existing `window._defaultState` snapshot at `state.js:1479`, so `wipeTerminal()` resets it — no extra work). - [ ] **Migration** in `migrateState()` (`state.js:~1863`, before `delete s.macros`): ```js if (!s.padBindings || typeof s.padBindings !== 'object' || Array.isArray(s.padBindings)) s.padBindings = { up: null, down: null, left: null, right: null }; ['up','down','left','right'].forEach(d => { const v = s.padBindings[d]; s.padBindings[d] = (typeof v === 'string' && v.trim()) ? v.trim() : null; }); // drop any stray non-direction keys Object.keys(s.padBindings).forEach(k => { if (!['up','down','left','right'].includes(k)) delete s.padBindings[k]; }); ``` - [ ] **Sanitize** in `sanitizeImportedContainer()`'s `_sanitizeState` (`api.js:~486`, alongside the `lincolnItems` block — array/object typed field, born-compliant): rebuild the 4-key map, coercing each to a trimmed string or `null`, ignoring extras. This is the **serialized-whole cloud-pull / file-import path** (Protocol 34). - [ ] **autoImportState** (`api.js:540`): **deliberately NOT mapped.** Add an explicit comment documenting that `padBindings` is player-authored and never imported from an AI response (Protocol 24). Because `autoImportState` uses explicit field mapping (never a recursive key transform), simply not referencing `parsed.padBindings` means the AI can never write it. A Protocol-14 test asserts this (see §8). - [ ] **AI contract / `getSystemDirective()`**: **no change** — see §5. - [ ] **Cloud-sync path (declare in commit):** *serialized-whole.* `padBindings` lives inside the `state` object, so it rides the additive `robco_v8` save automatically and returns through `sanitizeImportedContainer()` → `migrateState()` on pull. No dedicated Firestore doc. Guarded by a push→sanitize→migrate→apply round-trip test. - [ ] **Render** (`renderHolster()` in `ui-render.js`) + call on deck-open and after each bind. - [ ] **Panel/UI:** the field surfaces only inside the deck's holster (no standalone panel). - [ ] **`CACHE_NAME` bump** (Protocol 1) — served files change (`index.html`, `css/`, `js/`). - [ ] **`test.html`** KNOWN_KEYS tripwire gains `padBindings` (Protocol 40) + a round-trip assert. - [ ] Lint, format, both runners at parity, docs (Protocol 2/2a). --- ## 5. AI-contract reconciliation (native is authoritative — Protocols 24, 14, 30) **Decision: bindings are fully native; the AI is blind to `padBindings`.** - `getSystemDirective()` is **unchanged** — `padBindings` is NOT added to the state schema. It keeps the token budget lean (Protocol 28), and since the AI already had **zero** directive support for PAD/BIND, nothing is lost. The AI cannot author or clobber a binding. - The old AI-driven `[BIND:]` / `[PAD:]` "commands" are **retired in favor of native handling** (same pattern as the VATS/THREAT/LOOT "still-AI" retirements): both become intercepts in `_routeNativeCommand()` (regex, mirroring `[WAIT:]` at `api.js:1218`), so they run deterministically offline and never reach Gemini. There is therefore **no conflict and no clobber path** — there is only one writer of `padBindings` (the native bind flow), reachable three ways that all funnel to `_nativePadBind`: the socket BIND flow, typed `[BIND:]`, and (read side) `_nativePadFire`. - Firing a bound vector (`_nativePadFire`) still hands a **resolved** action (`Deploy <gear>`) to the Director for narrative resolution — the app now knows the gear name natively instead of the AI having to remember it. This is strictly better than today and preserves the gameplay (using a stimpak / readying a weapon is AI-narrated; there is no native consumption engine to build). - No auth/redirect concerns (Protocol 30 untouched). *Optional (flagged, not planned):* a one-line directive telling the AI "quick-draw bindings are a native player system — do not compute or narrate them" could be added for belt-and-suspenders, but is omitted to save tokens since the AI has no schema hook to write them anyway. See §11. --- ## 6. File-by-file changes (exact selectors + rationale) ### 6.1 `index.html` - **Add the ◈ deck-key** to `.composer-toolbar` (between `+` at line 2494 and `#modePill`), reusing `.composer-icon-btn` (32×32, ≥28px ✓) + a new `.deck-key` modifier: ```html <button class="composer-icon-btn deck-key" id="deckKey" onclick="toggleToolDeck()" aria-label="Open the native tool deck" aria-expanded="false">◈</button> ``` (inline `onclick` → resolved by Suite 59; the rest of the deck is `addEventListener`-wired.) - **Delete** the entire `<details class="sub-panel uplink-tray" …>` block (lines **2536–2660**), removing `.tactical-dashboard`, `.d-pad`, `#macroTarget`, all seven `.macro-buttons` (incl. `#consultBtn`, `#vatsCalcBtn`, `#termlinkBtn`). - **Add the deck + scrim markup** as direct children of `.glass-frame` (after `.main-grid` closes, ~line 2663) so the sheet overlays the whole screen and is anchored to a stable `position:relative; overflow:hidden` container (see §7 "anchor" rationale). Mirror the mockup markup (lines 477–519): `.deck-scrim[hidden]`, `.tool-deck[role=dialog][hidden]` with `.deck-grip`, `.deck-head` (◈ TOOL DECK … + `.dx` ✕), `#deckTarget` input (16px), six `.tool-row` buttons carrying `data-tool` + `.tool-chip` + description `<span>` + optional `.uses-target`, then `.holster` (head + `.bind-key` + `.holster-rack` of four `.socket[data-dir]` buttons + `.holster-hint[aria-live=polite]`). Socket/tool labels are populated by `renderHolster()`; static gear names are NOT hardcoded. ### 6.2 `css/terminal.css` - Port the mockup's deck/holster CSS: `.deck-key` (dashed + `.open`), `.deck-scrim`, `.tool-deck` (+ `@keyframes deckRaise` as a plain **`animation:`** so the global `prefers-reduced-motion` block at Suite 94 neutralizes it — Protocol UI-9, no bespoke carve-out), `.deck-grip`, `.deck-head`/`.dx`, `.deck-target`, `.tool-row`/`.tool-chip`/`.uses-target`, `.holster`/ `.holster-head`/`.bind-key`(+`.armed`)/`.holster-rack`/`.socket`(+`.bound`)/`.socket-led`/ `.socket-gear`/`.holster.binding` (+`@keyframes socketPulse`)/`.holster-hint`. - **Anchor rules:** `.tool-deck`/`.deck-scrim` `position:absolute` inside `.glass-frame` (`position:relative; overflow:hidden` already at `css/terminal.css:597`). Colours use `--bezel-wire` (amber) tokens per Protocol 38 — no game literal. Mobile = full-width sheet (`left/right/bottom:6px`); a `@media (min-width:1000px)` rule may right-anchor it over the Director column (polish, optional). - **Remove** the now-dead `.tactical-dashboard`, `.d-pad`, `.d-pad button`, `.d-pad-row`, `.macro-buttons button`, `.uplink-tray` rules (lines ~242, ~1033–1043, ~1661–1706) — or keep `.macro-buttons` only if any survivor uses it (none will; verify). - Sockets are ~76×46 (≥28px ✓); `#deckTarget` is 16px (no focus-zoom, Protocol 17). ### 6.3 `js/state.js` - Default + migration for `padBindings` (§4). ### 6.4 `js/api.js` - `sanitizeImportedContainer._sanitizeState`: add `padBindings` coercion (§4). - `autoImportState`: documenting comment only — NOT mapped (§4/§5). - `_routeNativeCommand()` (`api.js:1203`): add two regex intercepts **before** the `[WAIT:]` block, mirroring it: ```js const bindM = userText.match(/\[BIND:\s*(.+?)\s*,\s*(UP|DOWN|LEFT|RIGHT)\s*\]/i); if (bindM) { _nativePadBind(bindM[1], bindM[2]); return true; } const padM = userText.match(/\[PAD:\s*(UP|DOWN|LEFT|RIGHT)\s*\]/i); if (padM) { _nativePadFire(padM[1]); return true; } ``` (BIND before PAD; both return true so the AI is never called.) ### 6.5 `js/ui-core.js` - **`toggleToolDeck()` / `openToolDeck()` / `closeToolDeck()`** — bespoke in-glass sheet controller (the deck is a screen-local bottom-sheet, structurally distinct from the centered `#sysModal`; NOT a duplicate modal manager — Protocol 22/23). Handles `hidden` toggling on `#toolDeck`/`.deck-scrim`, `.open`/`aria-expanded` on `#deckKey`, focus into `#deckTarget` on open and back to `#deckKey` on close, Esc + scrim-tap close, and disarming bind mode on close. `openToolDeck()` calls `renderHolster()`. - **`_wireToolDeck()`** — boot function (addEventListener wiring for rows, sockets, BIND ▸, ✕, scrim, Esc). Add its call alongside `_wireModeHint()` / `_wireComposerAutoGrow()` (`ui-core.js:1698–1699`) inside that boot phase; register it in the phase list so Suite 132's slim-composition contract stays honest. - Tool-row dispatch (`data-tool` → existing handler, then `closeToolDeck()`): `THREAT`→`macroCommand('[THREAT]')`, `VATS SIM`→`showVATSOverlay()`, `TRADE`→ `expandPanelForCategory('trade')` (+`switchTab('inv')`), `LOOT`→`renderLoot(deckTargetVal)`, `CONSULT`→`macroCommand('[CONSULT]')`, `VATS CALC`→`showVATSOverlay()`. - **Repoint `macroCommand`** to read the deck target: change `document.getElementById('macroTarget')` → `'deckTarget'`, and drop the now-dead `!actionStr.includes('PAD:')` branch (D-Pad no longer uses `macroCommand`). Keep the helper (reused by the THREAT/CONSULT rows — Protocol 22). - **`_nativePadBind(gear, dir)`**: normalize `dir`→lowercase, validate ∈ {up,down,left,right} (invalid → sys hint, return); `gear=String(gear||'').trim()` (empty → hint "type gear first", no clobber, return); `state.padBindings[dir]=gear`; `saveState()`; `renderHolster()`; `appendToChat('> ▸ [BIND: '+gear.toUpperCase()+', '+dir.toUpperCase()+'] — VECTOR HOLSTERED','sys',true)`; optionally `_logEvent('gear', …)` / `RobcoEvents.emit('gear.bind', …)` (aligns with the U8 auto-log expansion — flagged optional). - **`_nativePadFire(dir)`**: normalize/validate; `gear=state.padBindings[dir]`; empty → `appendToChat('> ▸ SOCKET EMPTY — HOLSTER GEAR FIRST (BIND ▸)','sys',true)`, **no AI**, return; bound → `transmitMessage('Deploy '+gear)` (the additive override param — one clean user line, reaches the Director). The socket UI tap calls `closeToolDeck()` then `_nativePadFire(dir)`. - **Remove** the `uplinkCommandTray` desktop-open exception in `_wirePanelPersistence()` (`ui-core.js:1545–1555`) — the tray no longer exists. - Update `COMMAND_REGISTRY` entries (`ui-core.js:3404–3405`) to describe the native offline Quick-Draw Holster (`[BIND: X, DIR]` / `[PAD: DIR]` — "Offline"). ### 6.6 `js/ui-render.js` - **`renderHolster()`**: for each of ▲(up)/◄(left)/►(right)/▼(down), read `state.padBindings[dir]`; set the socket's `.socket-gear` text (uppercased gear name or `EMPTY`), toggle `.bound` (LED), and set a literal `aria-label` (`Fire gear vector up — Stimpak` / `Gear vector up — empty socket`). `escapeHtml` gear names (XSS — a bound name can be arbitrary player text). Idempotent; safe to call when the deck is closed (elements exist but hidden). ### 6.7 `tests/test.html` (Protocol 40) - Add `padBindings` to the `KNOWN_KEYS` tripwire set; add a runtime assertion that a v8 container with `padBindings` survives `sanitizeImportedContainer`→`migrateState` (4 keys, string|null), and that a payload with a bogus `padBindings` shape normalizes to the empty default. No new `section()` unless needed → `Suites: N` marker likely unchanged (verify). --- ## 7. Protocol-8 plan-audit — every entry path, state, edge **Deck lifecycle** - Closed at rest (default) → ◈ opens → renders holster from live state → focus `#deckTarget`. - Close via ✕ / Esc / scrim-tap → focus returns to ◈, bind mode disarmed, `aria-expanded=false`. - Re-open after a bind reflects the new socket (renderHolster on open). ✓ - **Anchor rationale:** deck is `position:absolute` inside `.glass-frame` (stable `position:relative; overflow:hidden`). Rejected `.chat-panel` (has `overflow-y:auto` on desktop AND mobile-uplink → an absolute `bottom:0` child scrolls with content / is mis-placed) and `.col-right` (`overflow:hidden`/`clip` → clips the sheet). `.glass-frame` is inside the `.casing` that ambient states filter/transform, but the deck is `absolute` (contained by glass-frame), **not** `fixed`, so it does NOT degrade under idle/night/shutdown (the Suite 160 fixed-bezel caveat does not apply here). ✓ **Each tool launch** - THREAT/CONSULT with a `#deckTarget` value → resolved command → native modal; empty target → the native tool's own no-arg behavior (unchanged). VATS/CALC ignore target (router already ignores it). TRADE opens the barter panel (must also `switchTab('inv')`). LOOT pre-fills its search with the target. Every launch closes the deck first. ✓ **Bind flow** - Arm (BIND ▸) → sockets dashed-pulse, hint shows, focus `#deckTarget`. - Pick direction (tap socket) with non-empty target → write + persist + re-render + log + disarm. - Empty target → "type gear first" hint, **no clobber**, stay armed. ✓ - Re-bind an already-bound socket → overwrites (expected). ✓ - Deck closed mid-arm → disarm. ✓ - **Unbind:** the mockup has no unbind control → **v1 ships without unbind** (re-bind to replace). Flagged in §12. **Empty vs bound socket / deploying empty** - Bound tap → deploy (resolved AI action) + close deck. Empty tap → hint, no AI, deck stays open (so the player can BIND). ✓ **Typed command path vs native (no conflict/clobber)** - `[BIND: gear, dir]` / `[PAD: dir]` in EITHER input mode → `_routeNativeCommand` regex → native helpers; AI never sees them. `_nativePadBind` is the single writer. ✓ - Double-echo note: a *typed* `[PAD: UP]` shows `> [PAD: UP]` (outer echo) then `> Deploy <gear>` (fire) — acceptable audit trail for the power-user path; the *socket* path (primary) is one clean line. Flagged. ✓ **Mobile vs desktop** - Mobile: ◈ only reachable on the UPLINK subsystem (the composer lives in `.col-right`, hidden on other subsystems by `body:not([data-subsystem='uplink']) .col-right{display:none}`). Deck fills the screen. Correct — the deck is an UPLINK-context feature; other subsystems show the carrier-strip. Desktop: ◈ always reachable; deck overlays the full glass-frame. ✓ **Reduced-motion (Protocol UI-9)** - `@keyframes deckRaise` + `socketPulse` are plain `animation:` → zeroed by the global `prefers-reduced-motion` block → instant resting frame. No bespoke carve-out. ✓ **Migration / brand-new vs existing save** - New campaign: default all-null (sockets EMPTY). Existing save w/o `padBindings`: migrateState adds the default. Malformed padBindings (array/string/extra keys): normalized. ✓ **AI determinism / clobber** - AI response can't touch `padBindings` (not in schema, not mapped). Cloud/file round-trip preserved via sanitize. ✓ **PWA-intact** - No SW/manifest structural change beyond `CACHE_NAME`. Deck is CSS+DOM chrome inside the app shell; installable/offline/standalone unaffected. ✓ **Reused handlers unbroken** - `macroCommand` retargeted to `#deckTarget`; `renderThreat`/`renderConsult`/`showVATSOverlay`/ `expandPanelForCategory`/`renderLoot` unchanged. `showTermlinkConsole` + `[TERMLINK]`/ `[CROSSROADS]` remain typable (only the on-screen `#termlinkBtn` is removed with the cluster). ✓ --- ## 8. Tests (both runners at parity — Protocols 13, 14, 15, 2a) ### NEW **Suite 164** — "Command-cluster overhaul: Tool Deck + Quick-Draw Holster + padBindings" State field (Protocol 4/14/34): - `state.js` default `padBindings {up,down,left,right}` all null; `window._defaultState` includes it. - `migrateState` adds the default when absent; normalizes array/string/extra-key inputs. - `sanitizeImportedContainer` coerces `padBindings` (behavioral: rebuild 4 keys, string|null, drop extras). - **Protocol 14:** `autoImportState` with a `padBindings` in the parsed response does **NOT** alter `state.padBindings` (player-authority regression). - **Round-trip (Protocol 34):** push (serialize) → sanitize → migrate → apply preserves bindings. Native commands: - `[BIND: gear, DIR]` intercepted by `_routeNativeCommand` → `_nativePadBind` writes state, no AI. - `[PAD: DIR]` intercepted → `_nativePadFire`; empty socket → no transmit + hint; bound → resolved transmit. - `_nativePadBind` validates direction + rejects empty gear (no clobber). Markup/DOM: - composer `#deckKey` (◈) present, `aria-label`, `.deck-key`, ≥28px. - `#toolDeck`[role=dialog], `.deck-scrim`, `#deckTarget` (16px), six `.tool-row[data-tool]` with `.tool-chip`, `.holster` with four `.socket[data-dir=UP/LEFT/RIGHT/DOWN]`, `.bind-key`, hint. - deck rows map to existing handlers (assert dispatch table / onclick-free rows wired in `_wireToolDeck`). - **Removed:** no `.tactical-dashboard`, `.d-pad`, `#macroTarget`, `uplinkCommandTray`, `#termlinkBtn`, and no `macroCommand('[PAD: …]')` anywhere. CSS/motion: - `.tool-deck`/`.socket`/`.tool-chip`/`.holster` rules present; deck raise is an `animation` (reduced-motion-safe); `--bezel-wire` used (Protocol 38, no `--robco-blue`). Wiring: - `_wireToolDeck`, `toggleToolDeck`, `_nativePadFire`, `_nativePadBind`, `renderHolster` defined; `_wireToolDeck` called from `window.onload` phase list. ### UPDATE existing suites (same commit) - **"Critical Feature Presence"** (`tests/robco-diagnostics.js:1377`, lines 1430–1439): replace the four `macroCommand('[PAD: …]')` D-pad assertions + THREAT/VATS macro-button assertions with the deck-row equivalents. - **106.16** — `[TRADE]` → the deck row calls `expandPanelForCategory('trade')` (update selector). - **108.14** — `#consultBtn`/`macroCommand('[CONSULT]')` → the deck CONSULT row. - **111.11** — VISUAL UPLOAD wording lives in the composer already (verify still passes; the ◈ key addition shouldn't affect it). - **123.9** — drop the `#termlinkBtn` DOM requirement (button removed with the cluster); keep the `[TERMLINK]` router + `COMMAND_REGISTRY` + console-CSS checks. TERMLINK stays typable. - **113** — confirm `[PAD]`/`[BIND]` (regex, like `[WAIT:]`, not router-object tokens) don't break the FEATURES/router consistency assertions; update the RETIRED-macro list if needed. - **162.16a/16b/17/18a/18b** — remove/rewrite (uplink tray, `.tactical-dashboard`, `.d-pad`, desktop-open exception, TERMLINK-CONSOLE-position all gone). **162.20** — extend to assert the ◈ `#deckKey` is present in the composer toolbar. - **Suite 59** (inline-handler integrity) — the new `onclick="toggleToolDeck()"` on `#deckKey` must resolve to a real definition (it will). - **Protocol 2a count sync** across: `CLAUDE.md`, `RULES.md`, `README.md`, `ARCHITECTURE.md`, `CHANGELOG.md` `[Unreleased]` header, both runners' per-suite comments, and `test.html` `Suites:` marker (only if a section is added). Run the `Select-String` drift scan. --- ## 9. Protocol touchpoints - **P1** CACHE_NAME bump (served files change). **P2/2a** docs + counts. **P4** new state field (full checklist §4). **P10/17** render at 360/412/desktop, ≥28px targets, 16px inputs, no overflow. **P13** regression tests for the fixed/changed behavior. **P14** AI-contract test (padBindings not AI-writable). **P15** runner parity. **P19** batch → one push. **P20** static guards for the new CSS/markup contracts. **P22** reuse existing tool handlers + `macroCommand`; bespoke deck is a distinct surface, not a second modal manager. **P23** layering respected (render renders, state owns state, api routes). **P24** AI never authoritative for bindings. **P25** owner-approved redesign (signed-off mockup) — reframes the UPLINK cluster, same subsystem, no tap-count increase for the tools (2 taps: ◈ + row), real labels ride along as chips + descriptions. **P34** additive serialized-whole cloud path. **P38** `--bezel-wire` / no game literal. **P39** UTF-8 (◈ ▲◄►▼ ✕ — use Edit tool / Node, never PowerShell writes). **P40** test.html KNOWN_KEYS. **P41** cleanup sweep. **UI-1/UI-3/UI-5** buttons not spans, literal aria-labels. **UI-6** the deck is an ephemeral overlay, not a persisted view-mode, so no MetaStore key (documented). **UI-9** deck slide as a plain animation (reduced-motion-free). --- ## 10. Verification steps (Stage 2, before "done") 1. `npm run lint` + `npm run format` clean. 2. Both runners green + at parity + new total synced everywhere. 3. `tests/render-check.mjs` (Playwright 360/412) — no horizontal overflow, no focus-zoom. 4. Live render at **360 / 412 / ≥1000px** (Protocol 10): resting UPLINK = transcript only + ◈; open deck; launch each of the 6 tools (confirm the right native surface opens + deck closes); fire a bound socket (Director responds to `Deploy <gear>`); fire an empty socket (hint, no AI); BIND flow (arm → type → tap → LED lights, name shows, persists across reload); typed `[BIND: mentats, down]` then `[PAD: down]`. Confirm `scrollWidth === innerWidth` at 360. 5. Reduced-motion emulation → deck appears instantly. 6. Reload → migrated default holds; a bound socket survives reload (serialized-whole). 7. PWA smoke (installable/offline unaffected). --- ## 11. Docs / version (Protocol 2/2a) - **APP_VERSION:** MINOR bump (new user-facing feature/panel) per Protocol 2 — auto, no ask. - **CACHE_NAME:** bump `-rN` (served files change). - **CHANGELOG `[Unreleased]`** (plain English, Protocol 21): Added — the Tool Deck launcher + the Quick-Draw Holster (visible gear vectors); Changed — the command cluster no longer sits under the chat; Under the Hood — bindings are now real saved state. - **ARCHITECTURE.md / README.md** — update the UPLINK/command-cluster description, the state-field list (`padBindings`), and the Native-Input-Path Audit table (padBindings now has a native setter). --- ## 12. Open questions (for the owner — NOT blocking Stage-1) 1. **Unbind:** the mockup ships no un-holster control (re-bind to replace). OK for v1, or add an explicit clear (e.g. BIND ▸ + empty field + socket, or a long-press)? *Plan assumes no unbind.* 2. **Deploy verb:** `_nativePadFire` sends `Deploy <gear>` to the Director. Prefer a different phrasing (`Use <gear>` / `Ready <gear>`)? *Plan uses "Deploy".* 3. **TERMLINK console redundancy:** the Tool Deck now overlaps `showTermlinkConsole()` (both launch native subsystems). Plan keeps TERMLINK typable but removes its on-screen button. Retire the console entirely in a later consolidation (Protocol 22)? *Out of scope here — flagged.* 4. **Bind validation:** the plan allows binding *any* typed gear name (player may bind gear not yet held), matching the mockup. Restrict to inventory items instead? *Plan: no restriction.* 5. **Directive belt-and-suspenders:** add a one-line "bindings are native, don't compute" note to `getSystemDirective()`? *Plan: omit (token budget; AI has no schema hook anyway).*
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).