Skip to content

Feat/winusb phase2a usbdk - #1

Merged
Troughie merged 80 commits into
mainfrom
feat/winusb-phase2a-usbdk
Jul 20, 2026
Merged

Feat/winusb phase2a usbdk#1
Troughie merged 80 commits into
mainfrom
feat/winusb-phase2a-usbdk

Conversation

@Troughie

@Troughie Troughie commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added After Effects macros, including shortcut, JSX script, preset, and expression support.
    • Added an After Effects panel for installing, monitoring, and running saved scripts and expressions.
    • Added macro keyboard dedicate/release controls with device status and recovery handling.
    • Added Force Quit macros and settings backup export/import.
    • Added application and audio-session icons, plus richer volume adjustment notifications.
  • Bug Fixes

    • Improved keyboard state cleanup and recovery when devices disconnect or readers restart.
    • Improved installed-app and audio-session detection and display reliability.
  • Documentation

    • Updated setup, usage, troubleshooting, and After Effects integration guidance.

Troughie and others added 30 commits July 10, 2026 11:29
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the pre-existing uncommitted working-tree state handed to Phase 1
(Interception removal, keyboard.ipc/main/preload edits, usb dependency,
asInvoker execution level) as a clean baseline so subsequent task commits
stay atomic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the Raw Input + low-level hook reader. keyboard:list now enumerates
USB keyboards via node-usb; keyboard:select opens the selected device with the
WinUSB reader and forwards assigned keys over the unchanged keyboard:event IPC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… errors

Composite keyboards expose extra interrupt IN endpoints (consumer/vendor) that
raise LIBUSB_ERROR_IO when polled; claiming every interface spammed errors and
the tear-down-on-error path caused reopen/"Polling is not active" storms.
Now only the HID boot-keyboard interface is claimed, endpoint errors are
recovered by restarting the poll, and the IPC layer logs instead of tearing down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wdi-simple build from libwdi proved too brittle; UsbDk ships a signed MSI and
captures by VID/PID from code, with node-usb useUsbDkBackend() support.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User plays kernel-anti-cheat games; empirically verify whether UsbDk's loaded
driver is flagged before building the integration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The packaged app intermittently got a "stuck key" that blocked all other macro
keys until restart (dev was fine). Root cause, confirmed via the debug log:
App.tsx and KeyboardSelector both call loadDevices() on mount, firing
keyboard:select twice. findByIds() returns a singleton libusb Device per
VID/PID, so the two selects opened two readers on the same handle; the second's
poll collided with the first's teardown ("Can't close device with a pending
request" -> LIBUSB_ERROR_NOT_FOUND), killing the poll. It only surfaced in the
packaged build, whose faster startup races the two selects close together.

- keyboard.ipc: make keyboard:select idempotent (activeDeviceKey) and serialize
  every reader open/close (opChain), awaiting teardown before each open so a
  close never races its own open. Self-heal re-open uses the same queue.
- hid-keyboard: add a watchdog so a poll error whose 'end' never fires escalates
  to a full re-open instead of wedging the poll forever.
- main: backgroundThrottling=false so the hidden tray window keeps running macros.
- flush held keys to the renderer on reader teardown so nothing stays
  stuck-highlighted (preload onFlush + keyboardStore.clearKeys + App.tsx).

Committed together with the in-progress WinUSB macro-keyboard work this fix
builds on (wdi-simple.exe driver-swap flow, debug file logger). Also refreshes
README to the WinUSB flow and ignores resources build artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Troughie and others added 27 commits July 20, 2026 15:02
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep aeExpressions required (mirrors aeScripts), seed its default in main.ts,
and widen AeScriptEditor's scriptType prop so the renderer tree compiles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a portable JSON backup so a user can restore their full MacroDeck
config (macros across all profiles, app settings, and the AE script/
expression libraries) after reinstalling — no manual re-setup per key.

- settings-transfer.ts: pure buildExportBundle/parseImportBundle with a
  versioned, identifying header and strict validation (unit-tested).
- main.ts: settings:export / settings:import IPC with save/open dialogs;
  import validates before a wholesale store replace, then re-syncs the AE
  panel files and tray menu.
- preload.ts: backup.export / backup.import bridge.
- TitleBar: Export/Import buttons in the settings dropdown; import reloads
  every store from disk and re-syncs macro keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hint

The target select, name input and Save button were crammed into one flex
row, so in the narrow settings panel the Save button overflowed off-screen
(had to scroll sideways) and the name field was hidden. Save is disabled
until both expression and name are filled, so users who only typed the
expression couldn't tell why it wouldn't save.

Stack the name row and a full-width Save button vertically (like Test
Expression), and show a hint when a name is still needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erflow

The target select kept flex-shrink-0, so its long labels (e.g. "Selected
property") pushed the name input off the right edge, forcing a horizontal
scroll. Give both flex-1 min-w-0 so they split the row evenly and shrink
together in the narrow panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking a saved expression's name (or its new pencil button) now loads it
into the editor in edit mode: Save becomes "Update Expression", a Cancel
button appears, and saving patches the existing record instead of creating
a duplicate. The delete (trash) button is now always visible in the
expression list rather than only on row hover, matching the custom-JSX list
behavior the user expected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AE's built-in "Create Shapes from Text" command is disabled when more than
one layer is selected, forcing users to convert text layers one at a time.
This preset snapshots the selected text layers, then isolates and runs the
command on each in turn under a single undo group — one click converts the
whole selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decompose-text tools keep the full source string on every single-character
layer and reveal one glyph via a range selector. "Create Shapes from Text"
ignores that selector and outlines the entire string, so a layer named "y"
ends up containing B/o/u/n/c/y groups.

After converting, keep only the glyph group whose name matches the layer
name and remove the rest. Guarded by an exact name match so normal
multi-character text layers are left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Name-matching alone can't disambiguate duplicate characters: decomposing
"Hello" gives the second "l" layer two groups both named "l", and keeping
"every group named l" left both. Now read the decompose range selector on
the text layer (before conversion) to learn which character index it
reveals, and keep the glyph group at that ordinal position. Falls back to
name matching when the character is unique, and leaves the layer untouched
when a letter repeats and no index is available — never deletes the wrong
glyph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change replaces Interception-based keyboard capture with WinUSB HID reading and driver dedication, adds a resident After Effects CEP panel with file-based execution and saved libraries, introduces Force Quit and AE macros, adds settings backup import/export, enriches app/audio metadata, and updates packaging, documentation, tests, and UI flows.

Changes

MacroDeck platform

Layer / File(s) Summary
WinUSB keyboard capture and dedication
electron/native/*, electron/ipc/keyboard.ipc.ts, src/stores/keyboardStore.ts, src/components/KeyboardSelector/*
USB keyboards are enumerated, identified, named, dedicated through WinUSB, read through HID boot reports, and restored to Windows HID handling.
After Effects CEP bridge
ae-panel/*, electron/ipc/ae-bridge.ts, electron/ipc/ae-install.ts, electron/ipc/ae.ipc.ts
A resident CEP panel polls bridge files, executes JSX in After Effects, writes responses and heartbeats, and exposes installation and status controls.
AE macros and saved content
src/components/MacroSettings/ae/*, src/stores/ae*Store.ts, electron/main.ts, electron/ipc/macro.ipc.ts
Shortcut, preset, custom JSX, and expression-based AE commands are modeled, edited, persisted, compiled, synchronized to the panel, and executed.
System services and settings transfer
electron/ipc/apps.ipc.ts, electron/ipc/audio.ipc.ts, electron/ipc/settings-transfer.ts, electron/main.ts, resources/AudioSessions.cs
App and audio discovery gain metadata enrichment, audio mutations return structured values, and settings can be exported and validated on import.
Renderer and packaging integration
src/types/*, src/components/*, electron/preload.ts, package.json, tsconfig*.json, vitest.config.ts
Renderer contracts and controls reflect the new flows, tests are configured, WinUSB dependencies are added, and the CEP panel is packaged as an extra resource.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and points to the WinUSB/UsbDk phase-2 work reflected in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/winusb-phase2a-usbdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Troughie
Troughie merged commit 3ec3f84 into main Jul 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
resources/AudioSessions.cs (1)

120-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Per-session commands report { ok: true } even when the target session doesn't exist. All three per-session branches default their result fields to zero/false and never track whether a matching session was found, so a missing/exited target is indistinguishable from a real success. Fix by recording a matched flag in the session loop and exiting non-zero (or emitting ok: false) when no session matches target.

  • resources/AudioSessions.cs#L120-L155: in AdjustVolume, set a matched flag when the target session is found; emit failure when it stays false instead of { ok:true, previousValue:0, currentValue:0 }.
  • resources/AudioSessions.cs#L171-L200: in SetVolume, apply the same matched guard before emitting { ok:true, ..., currentValue:clampedValue }.
  • resources/AudioSessions.cs#L216-L247: in ToggleMute, apply the same matched guard before emitting { ok:true, isMuted:false, volume:0 }.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/AudioSessions.cs` around lines 120 - 155, Track whether a target
session was found in AdjustVolume, SetVolume, and ToggleMute by setting a
matched flag inside each session loop. For resources/AudioSessions.cs lines
120-155, 171-200, and 216-247, emit failure or exit non-zero when matched
remains false; only return the existing success payloads after a matching
session is processed.
🧹 Nitpick comments (4)
src/App.tsx (1)

132-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

flex-0 is not a valid Tailwind utility.

Tailwind's flex shorthand utilities are flex-1, flex-auto, flex-initial, flex-noneflex-0 generates no CSS. It's also redundant alongside flex-1 here. Drop it (or use flex-[0]/flex-none if flex: 0 was actually intended).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` at line 132, Update the container div in App.tsx by removing the
invalid and redundant flex-0 class from its className, while preserving the
existing flex-1 and other layout utilities.
src/components/MacroSettings/AppLaunchSettings.tsx (1)

30-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove leftover debug console.log.

This logs the full installed-apps payload (including base64 icon data URLs) to the console on every load. Drop it before merge.

♻️ Proposed cleanup
       const result = await electronAPI.apps.getInstalled();
-      console.log(result);
-
       setApps(result);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/MacroSettings/AppLaunchSettings.tsx` around lines 30 - 31,
Remove the leftover console.log(result) statement from the app-loading flow in
AppLaunchSettings, leaving the installed-apps payload handling unchanged.
src/components/MacroSettings/MacroSettings.tsx (1)

215-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

h-500 is not a valid Tailwind class.

Numeric height utilities follow Tailwind's spacing scale (…h-96); 500 isn't in it, so h-500 generates no CSS and the intended fixed height is silently dropped. Use an arbitrary value if a fixed height is wanted: h-[500px]. Note it also competes with the existing flex-1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/MacroSettings/MacroSettings.tsx` at line 215, Replace the
invalid h-500 utility on the scrollable container with the appropriate valid
fixed-height class, using an arbitrary value such as h-[500px] if 500px is
required. Review the existing flex-1 on the same element and remove or retain it
according to the intended height-versus-flex behavior.
src/components/MacroSettings/AeCommandSettings.tsx (1)

21-29: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add rejection handling to the polling and detect IPC calls.

panelStatus() is polled every 2s with no .catch; a rejecting IPC call produces an unhandled rejection on every tick. The detect() call on Line 106 has the same gap and additionally dereferences r.found without guarding against an undefined result.

Proposed hardening
     const poll = () => {
-      electronAPI?.ae.panelStatus().then((s: PanelStatus) => { if (active) setStatus(s); });
+      electronAPI?.ae.panelStatus()
+        .then((s: PanelStatus) => { if (active) setStatus(s); })
+        .catch(() => {});
     };
   useEffect(() => {
-    electronAPI?.ae.detect().then((r: any) => setAeFound(r.found));
+    electronAPI?.ae.detect()
+      .then((r: any) => setAeFound(!!r?.found))
+      .catch(() => {});
   }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/MacroSettings/AeCommandSettings.tsx` around lines 21 - 29,
Update the polling effect around panelStatus to handle rejected IPC promises
without unhandled rejections, while preserving the active-state guard before
updating status. Also update the detect() call near the existing detection flow
to handle rejection and guard the response before accessing r.found, treating an
absent result as not found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/debug-log.ts`:
- Around line 15-16: Add an asynchronous 'error' listener to the write stream
created in the debug-log setup, ensuring stream failures are handled without
propagating as unhandled exceptions or crashing the Electron main process. Keep
the existing synchronous setup behavior unchanged and make the handler safely
ignore or report the stream error without relying on the logger being
initialized.
- Around line 30-36: Update the console interception in the debug-log setup to
prevent raw HID report bytes emitted by electron/native/hid-keyboard.ts from
being persisted, while preserving normal console output and existing log levels.
Ensure the filtering targets the HID report logging path before write(level,
args) persists its arguments.

In `@electron/ipc/ae-bridge.ts`:
- Around line 119-142: Serialize executeViaPanel calls with an in-process queue
or mutex so each invocation waits for the previous request to resolve or time
out before calling writeRequest. Preserve the existing per-request polling,
success, script-error, panel-not-open, and timeout behavior while ensuring
concurrent callers cannot overwrite the shared request/response slot.

In `@electron/ipc/ae-install.ts`:
- Around line 57-77: The install flow in install must avoid deleting the
existing panel before a replacement is ready. Copy the source into a temporary
sibling directory, then replace dest only after copyDirRecursive completes
successfully, cleaning up the temporary directory on failure while preserving
the previous installation.

In `@electron/ipc/ae.ipc.ts`:
- Line 19: Reorder the AE_YEARS array into strictly descending year order by
moving '2026' to the front, ahead of '2025', while preserving all existing year
entries.

In `@electron/ipc/apps.ipc.ts`:
- Line 244: Update the PowerShell app-discovery command around Add-App to
replace the PowerShell 7 null-coalescing expression on $item.Publisher with a
PowerShell 5.1-compatible null check, preserving the empty-string fallback when
Publisher is null. Apply the same compatibility fix to any corresponding
$publisher expression in the generated script.
- Around line 41-330: Fix the embedded PS_SCRIPT so JavaScript preserves every
PowerShell regex backslash, either by escaping the backslashes in the template
literal or by loading the script from a separate .ps1 file. Also replace the
unsupported null-coalescing expressions in PS_SCRIPT, including the Add-App
calls using ??, with Windows PowerShell 5.1-compatible null/default handling so
the script parses and scans successfully.

In `@electron/ipc/keyboard.ipc.ts`:
- Around line 190-208: Update the keyboard:dedicate handler to await
stopReader() after validating the device and driver-tool availability, but
before calling dedicate(), so any active reader or retry loop is released before
the driver swap. Keep the existing dedication result handling and
startReader(deviceId, true) behavior unchanged.

In `@electron/native/appvolume.ts`:
- Line 180: Update the per-session "set-volume" branch to clamp the requested
percentage to [0,100] before computing and emitting currentValue, matching the
clamped value applied by SAVSetVol. Reuse that clamped value for the setter
input and JSON response while preserving previousValue behavior.

In `@electron/native/usb-enum.ts`:
- Around line 48-53: Eliminate the per-device getDriverService calls in the
enumeration block by fetching DEVPKEY_Device_Service in bulk within
loadPnpDevices, storing the result on each PnpDeviceInfo, and using that cached
service value to assign dev.driverState. Preserve the existing WinUSB
case-insensitive matching and normal-state fallback when the service is absent.

In `@electron/native/winusb-driver.ts`:
- Around line 200-201: Optimize device queries by passing the wildcard directly
to Get-PnpDevice via -InstanceId instead of filtering a parameterless query.
Apply this in electron/native/winusb-driver.ts at lines 200-201 and 226-227,
preserving Select-Object -First 1; at line 289, preserve the array wrapper
around the filtered result. Apply the same -InstanceId 'USB\VID_*' change in
electron/native/device-names.ts lines 73-74, retaining the existing exclusion of
'*MI_*'.

In `@resources/README.md`:
- Around line 44-46: Update the fenced code block containing the wdi-simple-x64
invocation in the README to specify a language identifier, using a shell fence
as requested, without changing the command itself.

In `@src/components/MacroSettings/MultimediaSettings.tsx`:
- Line 70: Replace unsupported Tailwind utilities at all affected sites: update
the container in src/components/MacroSettings/MultimediaSettings.tsx at lines
70-70 to use max-h-[200px], and update the h-500 and flex-0 usages in
src/App.tsx at lines 132-132 and src/components/MacroSettings/MacroSettings.tsx
at lines 215-215 to use h-[500px] and flex-none (or remove flex-0 if redundant).
Ensure the intended sizing and flex behavior remain unchanged.

---

Outside diff comments:
In `@resources/AudioSessions.cs`:
- Around line 120-155: Track whether a target session was found in AdjustVolume,
SetVolume, and ToggleMute by setting a matched flag inside each session loop.
For resources/AudioSessions.cs lines 120-155, 171-200, and 216-247, emit failure
or exit non-zero when matched remains false; only return the existing success
payloads after a matching session is processed.

---

Nitpick comments:
In `@src/App.tsx`:
- Line 132: Update the container div in App.tsx by removing the invalid and
redundant flex-0 class from its className, while preserving the existing flex-1
and other layout utilities.

In `@src/components/MacroSettings/AeCommandSettings.tsx`:
- Around line 21-29: Update the polling effect around panelStatus to handle
rejected IPC promises without unhandled rejections, while preserving the
active-state guard before updating status. Also update the detect() call near
the existing detection flow to handle rejection and guard the response before
accessing r.found, treating an absent result as not found.

In `@src/components/MacroSettings/AppLaunchSettings.tsx`:
- Around line 30-31: Remove the leftover console.log(result) statement from the
app-loading flow in AppLaunchSettings, leaving the installed-apps payload
handling unchanged.

In `@src/components/MacroSettings/MacroSettings.tsx`:
- Line 215: Replace the invalid h-500 utility on the scrollable container with
the appropriate valid fixed-height class, using an arbitrary value such as
h-[500px] if 500px is required. Review the existing flex-1 on the same element
and remove or retain it according to the intended height-versus-flex behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e0ad69d-1ede-4702-9303-d743e0932df4

📥 Commits

Reviewing files that changed from the base of the PR and between e3a95d0 and 8e566cb.

⛔ Files ignored due to path filters (15)
  • Interception/command line installer/install-interception.exe is excluded by !**/*.exe
  • Interception/library/x64/interception.dll is excluded by !**/*.dll
  • Interception/library/x86/interception.dll is excluded by !**/*.dll
  • Interception/licenses/commercial-usage/Interception API.pdf is excluded by !**/*.pdf
  • Interception/licenses/commercial-usage/Interception.pdf is excluded by !**/*.pdf
  • Interception/samples/x86/axes.exe is excluded by !**/*.exe
  • Interception/samples/x86/cadstop.exe is excluded by !**/*.exe
  • Interception/samples/x86/caps2esc.exe is excluded by !**/*.exe
  • Interception/samples/x86/hardwareid.exe is excluded by !**/*.exe
  • Interception/samples/x86/identify.exe is excluded by !**/*.exe
  • Interception/samples/x86/interception.dll is excluded by !**/*.dll
  • Interception/samples/x86/mathpointer.exe is excluded by !**/*.exe
  • Interception/samples/x86/x2y.exe is excluded by !**/*.exe
  • package-lock.json is excluded by !**/package-lock.json
  • resources/wdi-simple.exe is excluded by !**/*.exe
📒 Files selected for processing (99)
  • .claudeignore
  • .gitignore
  • Interception/library/interception.h
  • Interception/library/x64/interception.lib
  • Interception/library/x86/interception.lib
  • Interception/licenses/non-commercial-usage/LGPL 3.0.txt
  • README.md
  • ae-panel/.debug
  • ae-panel/CSXS/manifest.xml
  • ae-panel/index.html
  • ae-panel/js/CSInterface.js
  • ae-panel/js/main.js
  • ae-panel/jsx/runner.jsx
  • docs/superpowers/plans/2026-07-10-winusb-macro-keyboard-phase1-reading.md
  • docs/superpowers/plans/2026-07-10-winusb-phase2a-spike-guide.md
  • docs/superpowers/plans/2026-07-10-winusb-phase2a-usbdk-spike-guide.md
  • docs/superpowers/plans/2026-07-14-ae-macros.md
  • docs/superpowers/plans/2026-07-15-ae-cep-panel.md
  • docs/superpowers/plans/2026-07-20-ae-expression-library.md
  • docs/superpowers/plans/2026-07-20-ae-panel-script-list.md
  • docs/superpowers/specs/2026-07-10-winusb-macro-keyboard-design.md
  • docs/superpowers/specs/2026-07-11-single-toggle-macro-keyboard-design.md
  • docs/superpowers/specs/2026-07-14-ae-macros-design.md
  • docs/superpowers/specs/2026-07-15-ae-cep-panel-design.md
  • docs/superpowers/specs/2026-07-20-ae-expression-library-design.md
  • docs/superpowers/specs/2026-07-20-ae-panel-script-list-design.md
  • electron/debug-log.ts
  • electron/ipc/ae-bridge.test.ts
  • electron/ipc/ae-bridge.ts
  • electron/ipc/ae-install.test.ts
  • electron/ipc/ae-install.ts
  • electron/ipc/ae.ipc.ts
  • electron/ipc/apps.ipc.ts
  • electron/ipc/audio.ipc.ts
  • electron/ipc/keyboard.ipc.ts
  • electron/ipc/macro.ipc.ts
  • electron/ipc/settings-transfer.test.ts
  • electron/ipc/settings-transfer.ts
  • electron/ipc/system.ipc.ts
  • electron/main.ts
  • electron/native/appvolume.ts
  • electron/native/boot-report.test.ts
  • electron/native/boot-report.ts
  • electron/native/device-names.test.ts
  • electron/native/device-names.ts
  • electron/native/hid-keyboard.ts
  • electron/native/hid-usage-map.test.ts
  • electron/native/hid-usage-map.ts
  • electron/native/usb-device-id.test.ts
  • electron/native/usb-device-id.ts
  • electron/native/usb-enum.ts
  • electron/native/winusb-driver.test.ts
  • electron/native/winusb-driver.ts
  • electron/notification-window.ts
  • electron/preload.ts
  • installer.nsh
  • package.json
  • resources/AudioSessions.cs
  • resources/README.md
  • resources/obj/AudioSessions.csproj.nuget.dgspec.json
  • resources/obj/AudioSessions.csproj.nuget.g.props
  • resources/obj/AudioSessions.csproj.nuget.g.targets
  • resources/obj/Debug/net6.0-windows/win-x64/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.AssemblyInfo.cs
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.AssemblyInfoInputs.cache
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.GeneratedMSBuildEditorConfig.editorconfig
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.GlobalUsings.g.cs
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.assets.cache
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.csproj.AssemblyReference.cache
  • resources/obj/project.assets.json
  • resources/obj/project.nuget.cache
  • src/App.tsx
  • src/components/KeyboardSelector/KeyboardSelector.tsx
  • src/components/MacroList/AssignedMacroList.tsx
  • src/components/MacroList/MacroTypeCard.tsx
  • src/components/MacroSettings/AeCommandSettings.tsx
  • src/components/MacroSettings/AppLaunchSettings.tsx
  • src/components/MacroSettings/ForceQuitSettings.tsx
  • src/components/MacroSettings/MacroSettings.tsx
  • src/components/MacroSettings/MultimediaSettings.tsx
  • src/components/MacroSettings/ae/AeScriptEditor.tsx
  • src/components/MacroSettings/ae/AeShortcutPicker.tsx
  • src/components/MacroSettings/ae/aePresets.test.ts
  • src/components/MacroSettings/ae/aePresets.ts
  • src/components/MacroSettings/ae/aeShortcuts.test.ts
  • src/components/MacroSettings/ae/aeShortcuts.ts
  • src/components/MacroSettings/ae/compileExpression.test.ts
  • src/components/MacroSettings/ae/compileExpression.ts
  • src/components/TitleBar/TitleBar.tsx
  • src/components/Toast/NotificationApp.tsx
  • src/notification-main.tsx
  • src/stores/aeExpressionStore.ts
  • src/stores/aeScriptStore.ts
  • src/stores/keyboardStore.ts
  • src/stores/macroStore.ts
  • src/types/macro.types.ts
  • tsconfig.electron.json
  • tsconfig.json
  • vitest.config.ts
💤 Files with no reviewable changes (14)
  • resources/obj/project.assets.json
  • resources/obj/AudioSessions.csproj.nuget.g.targets
  • resources/obj/AudioSessions.csproj.nuget.dgspec.json
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.GlobalUsings.g.cs
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.AssemblyInfoInputs.cache
  • resources/obj/Debug/net6.0-windows/win-x64/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs
  • resources/obj/AudioSessions.csproj.nuget.g.props
  • resources/obj/project.nuget.cache
  • Interception/licenses/non-commercial-usage/LGPL 3.0.txt
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.AssemblyInfo.cs
  • installer.nsh
  • resources/obj/Debug/net6.0-windows/win-x64/AudioSessions.GeneratedMSBuildEditorConfig.editorconfig
  • Interception/library/interception.h
  • electron/ipc/system.ipc.ts

Comment thread electron/debug-log.ts
Comment on lines +15 to +16
const file = path.join(app.getPath('userData'), 'macrodeck-debug.log');
const stream = fs.createWriteStream(file, { flags: 'w' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing error handler on the write stream can crash the app.

The try/catch around setup only covers synchronous errors. If the stream itself emits an async 'error' event later (disk full, permission denied, path issue) with no listener attached, Node throws an unhandled exception, crashing the Electron main process — the opposite of "logging must never break startup."

🛡️ Proposed fix
     const stream = fs.createWriteStream(file, { flags: 'w' });
+    stream.on('error', () => { /* never let logging crash the app */ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const file = path.join(app.getPath('userData'), 'macrodeck-debug.log');
const stream = fs.createWriteStream(file, { flags: 'w' });
const file = path.join(app.getPath('userData'), 'macrodeck-debug.log');
const stream = fs.createWriteStream(file, { flags: 'w' });
stream.on('error', () => { /* never let logging crash the app */ });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/debug-log.ts` around lines 15 - 16, Add an asynchronous 'error'
listener to the write stream created in the debug-log setup, ensuring stream
failures are handled without propagating as unhandled exceptions or crashing the
Electron main process. Keep the existing synchronous setup behavior unchanged
and make the handler safely ignore or report the stream error without relying on
the logger being initialized.

Comment thread electron/debug-log.ts
Comment on lines +30 to +36
(['log', 'warn', 'error'] as const).forEach((level) => {
const orig = console[level].bind(console);
console[level] = (...args: unknown[]) => {
write(level, args);
orig(...args);
};
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether HID/keyboard modules log potentially sensitive per-key data.
rg -nP --type=ts -C2 'console\.(log|warn|error)\(' electron/native/hid-keyboard.ts electron/ipc/keyboard.ipc.ts

Repository: Troughie/MacroDeck

Length of output: 7687


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== electron/native/hid-keyboard.ts (outline) ==\n'
ast-grep outline electron/native/hid-keyboard.ts --view expanded || true

printf '\n== electron/native/hid-keyboard.ts (relevant slices) ==\n'
sed -n '1,240p' electron/native/hid-keyboard.ts

printf '\n== electron/ipc/keyboard.ipc.ts (relevant slices) ==\n'
sed -n '1,260p' electron/ipc/keyboard.ipc.ts

Repository: Troughie/MacroDeck

Length of output: 19000


Remove HID report bytes from the persistent debug log
electron/native/hid-keyboard.ts logs the first 8 bytes of incoming boot reports for the first 30 packets, and debug-log.ts persists all console.log/warn/error output to disk. Those bytes can include modifier state and keycodes, so raw keystroke data may be written in plaintext.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/debug-log.ts` around lines 30 - 36, Update the console interception
in the debug-log setup to prevent raw HID report bytes emitted by
electron/native/hid-keyboard.ts from being persisted, while preserving normal
console output and existing log levels. Ensure the filtering targets the HID
report logging path before write(level, args) persists its arguments.

Comment thread electron/ipc/ae-bridge.ts
Comment on lines +119 to +142
export async function executeViaPanel(jsx: string, opts: ExecuteViaPanelOptions = {}): Promise<void> {
const now = opts.now ?? Date.now;
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
const pollIntervalMs = opts.pollIntervalMs ?? 50;
const timeoutMs = opts.timeoutMs ?? 5000;

if (!isPanelAlive(now())) {
throw new Error(PANEL_NOT_OPEN); // NO CLI fallback (by design)
}

const id = genRequestId(now());
writeRequest(id, jsx, now());

const deadline = now() + timeoutMs;
while (now() < deadline) {
const res = readResponse();
if (res && res.id === id) {
if (res.ok) return;
throw new Error(res.error || 'After Effects script error');
}
await sleep(pollIntervalMs);
}
throw new Error('After Effects not responding (timeout).');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Concurrent executeViaPanel calls race on the shared request.json/response.json.

There's only one request/response slot per bridge dir. If a second call writes request.json while an earlier call is still polling for its own id, the panel (single-slot poller) only ever executes the newer script; the first call silently times out after 5s instead of failing fast or being queued behind the in-flight one. Given AE macros can be triggered rapidly from a macro keyboard, this is a plausible failure mode that surfaces as spurious "not responding" errors.

Consider serializing calls (an in-process queue/mutex) so a new request is only written once the previous one has resolved or timed out.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/ae-bridge.ts` around lines 119 - 142, Serialize executeViaPanel
calls with an in-process queue or mutex so each invocation waits for the
previous request to resolve or time out before calling writeRequest. Preserve
the existing per-request polling, success, script-error, panel-not-open, and
timeout behavior while ensuring concurrent callers cannot overwrite the shared
request/response slot.

Comment on lines +57 to +77
export async function install(source: SourceEnv): Promise<{ ok: boolean; error?: string }> {
try {
// ① Enable unsigned panels for CEP 11 (AE 2024) and CEP 12 (AE 2025+).
// HKCU only — no admin rights needed.
for (const csxs of ['CSXS.11', 'CSXS.12']) {
await execFileAsync('reg', [
'add', `HKCU\\Software\\Adobe\\${csxs}`,
'/v', 'PlayerDebugMode', '/t', 'REG_SZ', '/d', '1', '/f',
], { timeout: 5000, windowsHide: true } as any);
}
// ② Copy panel into the CEP extensions folder (overwrite to support upgrades).
const src = panelSourceDir(source);
if (!fs.existsSync(src)) return { ok: false, error: `Panel source not found: ${src}` };
const dest = panelDestDir();
fs.rmSync(dest, { recursive: true, force: true });
copyDirRecursive(src, dest);
return { ok: true };
} catch (err: any) {
return { ok: false, error: err?.message?.slice(0, 200) ?? 'Install failed' };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Install upgrade path is destructive with no rollback on partial failure.

fs.rmSync(dest, ...) wipes the existing panel before copyDirRecursive runs. If the copy fails partway (permission issue, file lock, disk error), the previous working install is already gone and the new one is incomplete — the extension is left broken with no automatic recovery.

Consider copying into a temporary sibling directory first, then atomically renaming it over dest only after the copy fully succeeds.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/ae-install.ts` around lines 57 - 77, The install flow in install
must avoid deleting the existing panel before a replacement is ready. Copy the
source into a temporary sibling directory, then replace dest only after
copyDirRecursive completes successfully, cleaning up the temporary directory on
failure while preserving the previous installation.

Comment thread electron/ipc/ae.ipc.ts

// ─── AE Path Detection ────────────────────────────────────────────────────────

const AE_YEARS = ['2025', '2024', '2023', '2022', '2021', '2020', '2026'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

AE_YEARS ordering puts 2026 after 2020, preferring older AE installs.

The list is otherwise strictly descending (2025→2020) but '2026' is appended at the end instead of the front. With multiple AE versions installed side by side, findAeExeByFilesystem will return the 2020 install before checking 2026.

🛠️ Proposed fix
-const AE_YEARS = ['2025', '2024', '2023', '2022', '2021', '2020', '2026'];
+const AE_YEARS = ['2026', '2025', '2024', '2023', '2022', '2021', '2020'];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AE_YEARS = ['2025', '2024', '2023', '2022', '2021', '2020', '2026'];
const AE_YEARS = ['2026', '2025', '2024', '2023', '2022', '2021', '2020'];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/ae.ipc.ts` at line 19, Reorder the AE_YEARS array into strictly
descending year order by moving '2026' to the front, ahead of '2025', while
preserving all existing year entries.

case "set-volume": float sv=float.Parse(args[2])/100f; foreach(var p in savs) SAVSetVol(p,sv); Console.WriteLine("OK"); break;
case "volume-up": float d=args.Length>2?float.Parse(args[2]):10f; float c=SAVGetVol(savs[0]); foreach(var p in savs) SAVSetVol(p,Math.Min(1f,c+d/100f)); Console.WriteLine("OK"); break;
case "volume-down": float dd=args.Length>2?float.Parse(args[2]):10f; float dc=SAVGetVol(savs[0]); foreach(var p in savs) SAVSetVol(p,Math.Max(0f,dc-dd/100f)); Console.WriteLine("OK"); break;
case "set-volume": { float prev=(float)Math.Round(SAVGetVol(savs[0])*100); float sv=float.Parse(args[2])/100f; foreach(var p in savs) SAVSetVol(p,sv); Console.WriteLine("{\\"ok\\":true,\\"previousValue\\":" + (int)prev + ",\\"currentValue\\":" + (int)Math.Round(float.Parse(args[2])) + "}"); break; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Per-session set-volume reports an unclamped currentValue.

SAVSetVol clamps the applied level to [0,1], but currentValue is emitted from float.Parse(args[2]) without clamping — unlike the master branch (line 168) which clamps to [0,100]. A caller passing a value outside 0..100 (e.g. macro.ipc's executeVolumeAdjust, which forwards settings.setValue unclamped) will surface a currentValue that doesn't match the actual volume.

🐛 Proposed fix
-                    case "set-volume": { float prev=(float)Math.Round(SAVGetVol(savs[0])*100); float sv=float.Parse(args[2])/100f; foreach(var p in savs) SAVSetVol(p,sv); Console.WriteLine("{\\"ok\\":true,\\"previousValue\\":" + (int)prev + ",\\"currentValue\\":" + (int)Math.Round(float.Parse(args[2])) + "}"); break; }
+                    case "set-volume": { float prev=(float)Math.Round(SAVGetVol(savs[0])*100); float cv=Math.Max(0f,Math.Min(100f,float.Parse(args[2]))); float sv=cv/100f; foreach(var p in savs) SAVSetVol(p,sv); Console.WriteLine("{\\"ok\\":true,\\"previousValue\\":" + (int)prev + ",\\"currentValue\\":" + (int)Math.Round(cv) + "}"); break; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "set-volume": { float prev=(float)Math.Round(SAVGetVol(savs[0])*100); float sv=float.Parse(args[2])/100f; foreach(var p in savs) SAVSetVol(p,sv); Console.WriteLine("{\\"ok\\":true,\\"previousValue\\":" + (int)prev + ",\\"currentValue\\":" + (int)Math.Round(float.Parse(args[2])) + "}"); break; }
case "set-volume": { float prev=(float)Math.Round(SAVGetVol(savs[0])*100); float cv=Math.Max(0f,Math.Min(100f,float.Parse(args[2]))); float sv=cv/100f; foreach(var p in savs) SAVSetVol(p,sv); Console.WriteLine("{\\"ok\\":true,\\"previousValue\\":" + (int)prev + ",\\"currentValue\\":" + (int)Math.Round(cv) + "}"); break; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/appvolume.ts` at line 180, Update the per-session
"set-volume" branch to clamp the requested percentage to [0,100] before
computing and emitting currentValue, matching the clamped value applied by
SAVSetVol. Reuse that clamped value for the setter input and JSON response while
preserving previousValue behavior.

Comment on lines +48 to +53
// Detect which keyboards are already bound to WinUSB (dedicated). Runs in
// parallel; a null service leaves the device as 'normal'.
await Promise.all(result.map(async (dev) => {
const service = await getDriverService(dev.vendorId, dev.productId);
dev.driverState = service && /^winusb$/i.test(service) ? 'dedicated' : 'normal';
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid N+1 PowerShell executions during enumeration.

Calling getDriverService for each device spawns a new powershell.exe process per keyboard, creating an N+1 bottleneck that will cause a multi-second delay when opening the device selector.

Consider fetching the service state in bulk. You can update loadPnpDevices in device-names.ts to retrieve the DEVPKEY_Device_Service property for all nodes, store it on PnpDeviceInfo, and then check that cached state here instead of spawning new PowerShell processes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/usb-enum.ts` around lines 48 - 53, Eliminate the per-device
getDriverService calls in the enumeration block by fetching
DEVPKEY_Device_Service in bulk within loadPnpDevices, storing the result on each
PnpDeviceInfo, and using that cached service value to assign dev.driverState.
Preserve the existing WinUSB case-insensitive matching and normal-state fallback
when the service is absent.

Comment on lines +200 to +201
`$d = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | ` +
`Where-Object { $_.InstanceId -like '${like}' } | Select-Object -First 1;` +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Optimize Get-PnpDevice performance.

Piping a parameterless Get-PnpDevice to Where-Object causes PowerShell to enumerate every device on the system before filtering, which is notoriously slow (taking ~1-2 seconds per call). You can eliminate this latency by pushing the wildcard filter directly into the cmdlet using the -InstanceId parameter.

  • electron/native/winusb-driver.ts#L200-L201: replace the pipeline with $d = Get-PnpDevice -InstanceId '${like}' -PresentOnly -ErrorAction SilentlyContinue | Select-Object -First 1;
  • electron/native/winusb-driver.ts#L226-L227: replace the pipeline with $d = Get-PnpDevice -InstanceId '${like}' -PresentOnly -ErrorAction SilentlyContinue | Select-Object -First 1;
  • electron/native/winusb-driver.ts#L289-L289: replace the pipeline with $devs = @(Get-PnpDevice -InstanceId '${like}' -PresentOnly -ErrorAction SilentlyContinue)
  • electron/native/device-names.ts#L73-L74: replace the pipeline with $nodes += Get-PnpDevice -InstanceId 'USB\\VID_*' -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.InstanceId -notlike '*MI_*' }
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

📍 Affects 2 files
  • electron/native/winusb-driver.ts#L200-L201 (this comment)
  • electron/native/winusb-driver.ts#L226-L227
  • electron/native/winusb-driver.ts#L289-L289
  • electron/native/device-names.ts#L73-L74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/winusb-driver.ts` around lines 200 - 201, Optimize device
queries by passing the wildcard directly to Get-PnpDevice via -InstanceId
instead of filtering a parameterless query. Apply this in
electron/native/winusb-driver.ts at lines 200-201 and 226-227, preserving
Select-Object -First 1; at line 289, preserve the array wrapper around the
filtered result. Apply the same -InstanceId 'USB\VID_*' change in
electron/native/device-names.ts lines 73-74, retaining the existing exclusion of
'*MI_*'.

Comment thread resources/README.md
Comment on lines +44 to +46
```
wdi-simple-x64.exe --vid 0xXXXX --pid 0xXXXX --type 0 --name "MacroDeck Keyboard (WinUSB)"
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block. markdownlint flags MD040 here; use a shell fence for the invocation example.

✏️ Proposed fix
-```
+```text
 wdi-simple-x64.exe --vid 0xXXXX --pid 0xXXXX --type 0 --name "MacroDeck Keyboard (WinUSB)"
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 44-44: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/README.md` around lines 44 - 46, Update the fenced code block
containing the wdi-simple-x64 invocation in the README to specify a language
identifier, using a shell fence as requested, without changing the command
itself.

Source: Linters/SAST tools

<div>
<label className="text-text-secondary text-xs font-medium mb-1.5 block">Target</label>
<div className="space-y-1 max-h-28 overflow-y-auto">
<div className="space-y-1 max-h-50 overflow-y-auto">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Tailwind/config files ---'
git ls-files | rg '(^|/)(tailwind\.config|tailwind\.config\.[jt]s|postcss\.config|vite\.config|src/App\.tsx|src/components/MacroSettings/MultimediaSettings\.tsx|src/components/MacroSettings/MacroSettings\.tsx)$'

printf '\n%s\n' '--- Relevant snippets ---'
for f in src/App.tsx src/components/MacroSettings/MultimediaSettings.tsx src/components/MacroSettings/MacroSettings.tsx; do
  echo "### $f"
  sed -n '1,260p' "$f" | nl -ba | rg -n 'flex-0|h-500|max-h-50|overflow-hidden|overflow-y-auto|flex-1|flex-none|max-h-|h-'
  echo
done

printf '%s\n' '--- Tailwind config content (if present) ---'
for f in $(git ls-files | rg '(^|/)(tailwind\.config|tailwind\.config\.[jt]s)$'); do
  echo "### $f"
  cat "$f"
  echo
done

printf '%s\n' '--- Search for custom scale/extensions that could make these classes valid ---'
rg -n --hidden --glob '!.git' --glob '!node_modules' 'flex-0|h-500|max-h-50|theme:\s*{|extend:\s*{|spacing:|height:|maxHeight:' .

Repository: Troughie/MacroDeck

Length of output: 396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- tailwind.config.js ---'
cat -n tailwind.config.js

echo
echo '--- relevant class usages ---'
for f in src/App.tsx src/components/MacroSettings/MultimediaSettings.tsx src/components/MacroSettings/MacroSettings.tsx; do
  echo "### $f"
  sed -n '1,260p' "$f" | grep -nE 'flex-0|h-500|max-h-50|overflow-hidden|overflow-y-auto|flex-1'
  echo
done

Repository: Troughie/MacroDeck

Length of output: 3439


🌐 Web query:

Tailwind CSS default spacing scale max-h-50 h-500 flex-0 utilities

💡 Result:

In modern Tailwind CSS (v4+), the spacing scale has transitioned to a generative system based on a central --spacing unit (defaulting to 0.25rem/4px) [1]. This means that utilities like h-{number} or max-h-{number} are no longer limited to a fixed set of predefined keys [1]. Instead, you can use virtually any numeric value (e.g., h-50, h-500), and Tailwind will calculate the result by multiplying your input by the base spacing unit [1][2][3]. Regarding the specific utilities mentioned: 1. flex-0: This utility is a standard part of Tailwind CSS. It sets the flex-basis property to 0, typically applied via the flex-none (flex: none) or by using custom arbitrary values like flex- if a specific shorthand is required [4]. 2. max-h-50 and h-500: In versions using the new generative spacing system, these utilities will generate CSS such as max-height: calc(var(--spacing) * 50) and height: calc(var(--spacing) * 500) respectively [1][2][3]. In older versions of Tailwind (v3 and earlier), these specific classes were not part of the default theme and would not function unless explicitly added to your tailwind.config.js or used via arbitrary value syntax (e.g., max-h-[50px], h-[500px]) [5][4][6][7]. If you are using a newer version of Tailwind and find these utilities are not working, ensure your configuration is not locking the theme to a legacy, restricted spacing scale [1]. If you are on an older version, you should use the arbitrary value syntax (e.g., h-[500px]) to achieve these specific dimensions [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package.json ---'
cat -n package.json | sed -n '1,220p'

echo
echo '--- lockfile/package manager hints ---'
git ls-files | rg '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$' || true

echo
echo '--- Tailwind references ---'
rg -n '"tailwindcss"|tailwindcss' package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lockb 2>/dev/null || true

Repository: Troughie/MacroDeck

Length of output: 4847


Replace unsupported Tailwind utilities Tailwind 3.4.19 here won’t generate max-h-50, h-500, or flex-0, and tailwind.config.js doesn’t extend spacing to add them. Those classes are dropped, so the intended sizing/scroll cap never applies. Use arbitrary values like max-h-[200px] and h-[500px], and replace flex-0 with flex-none or remove it if it’s redundant.

📍 Affects 3 files
  • src/components/MacroSettings/MultimediaSettings.tsx#L70-L70 (this comment)
  • src/App.tsx#L132-L132
  • src/components/MacroSettings/MacroSettings.tsx#L215-L215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/MacroSettings/MultimediaSettings.tsx` at line 70, Replace
unsupported Tailwind utilities at all affected sites: update the container in
src/components/MacroSettings/MultimediaSettings.tsx at lines 70-70 to use
max-h-[200px], and update the h-500 and flex-0 usages in src/App.tsx at lines
132-132 and src/components/MacroSettings/MacroSettings.tsx at lines 215-215 to
use h-[500px] and flex-none (or remove flex-0 if redundant). Ensure the intended
sizing and flex behavior remain unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant