Skip to content

Conversation

@ryanbarlow97
Copy link
Contributor

Description:

I've made it so that pretty much everything is a configurable hotkey.

image image image image image image image

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

w.o.n

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Centralizes and exposes default keybinds, adds composite keyboard/mouse/wheel keybind support, updates UI capture/validation and help/unit displays to use dynamic keybinds, reloads keybinds at runtime, and adds many new translation keys for the expanded keybind surface.

Changes

Cohort / File(s) Summary
Translations
resources/lang/en.json
Added many new keybind labels and descriptions (performance overlay keybind, reset graphics, auto-upgrade, attack modifier, alternate move/zoom, attack ratio scroll, close view, keybind_reserved_error). Updated main.user_setting.performance_overlay_desc.
Default keybinds module
src/client/Keybinds.ts
New exported KeybindMap type and getDefaultKeybinds() returning platform-aware defaults (MetaLeft+MouseLeft on macOS, ControlLeft+MouseLeft otherwise).
Input processing & runtime
src/client/InputHandler.ts, tests/InputHandler.test.ts, tests/setup.ts
Added parsed keybind cache and reload on settings change, generalized matching helpers for keyboard/pointer/wheel, attack-modifier gating, pointer/wheel keybind handling, runtime event emissions updated; tests adjusted and in-memory localStorage polyfill added.
Keybind capture UI
src/client/components/baseComponents/setting/SettingKeybind.ts
Capture UI now supports modifiers + primary + pointer + wheel, improved commit/reset flow, ignoreClick handling, and emits structured change events with formatted display values.
Settings modal & validation
src/client/UserSettingModal.ts
Replaced hardcoded defaults with getDefaultKeybinds(), added parsing/validation for reserved browser shortcuts, centralized error messages, new actions added to settings UI, and emits settings.keybinds.changed on updates.
Help & display
src/client/HelpModal.ts
Merged defaults with saved keybinds, improved multi-key/mouse/scroll label rendering, replaced many hard-coded visuals with dynamic render pipeline using parsed keybinds.
Key formatting utilities
src/client/Utils.ts
Extended formatKeyForDisplay() to render combos (+), modifiers, mouse buttons, scroll actions, and numpad keys for consistent hotkey labels.
Unit UI hotkeys
src/client/graphics/layers/UnitDisplay.ts
Added private getKeybindValue(action: string) and switched hotkey lookups to use formatKeyForDisplay() so displayed hotkeys reflect configured keybinds.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Settings/Help/Keybind Capture
    participant Store as LocalStorage/Settings
    participant Input as InputHandler
    participant Parser as Keybind Parser
    participant Game as EventBus/Game

    User->>UI: Define or change keybind (keyboard / mouse / wheel)
    UI->>Store: Save keybinds to localStorage
    Store-->>Input: settings.keybinds.changed
    Input->>Parser: Load merged config (getDefaultKeybinds + saved)
    Parser-->>Input: ParsedKeybinds cached

    User->>Input: Keyboard / Pointer / Wheel event
    Input->>Parser: Match event against parsed keybinds
    alt Matching action found
        Input->>Game: Emit action event (e.g., AutoUpgradeEvent, AttackRatioEvent, ToggleOverlay)
    else No match
        Input->>Game: Ignore or forward raw input
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

"Keys, clicks, scrolls in tidy rows,
Defaults blend with what the user chose,
Mouse and wheel join keyboard tunes,
Browser warnings hush reserved moons,
Settings speak — the input system knows."

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Make hotkeys great again' is vague and generic, using a colloquial phrase that doesn't clearly convey the specific technical changes in the changeset. Consider a more descriptive title like 'Make UI actions configurable via hotkeys with keybind settings UI' to clearly communicate the main feature added.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly relates to the changeset, explaining that hotkeys are now configurable with multiple screenshots and confirmation of testing and translation work.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/client/InputHandler.ts (1)

425-431: Remove debug logging.

Line 426 has a console.log that logs all key events. This should be removed before merging.

🔧 Proposed fix
       if (this.matchesKeybind(e, this.keybinds.swapDirection)) {
         e.preventDefault();
         const nextDirection = !this.uiState.rocketDirectionUp;
         this.eventBus.emit(new SwapRocketDirectionEvent(nextDirection));
       }

-      // Toggle performance overlay keybind
-      console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
       if (this.matchesKeybind(e, this.keybinds.togglePerformanceOverlay)) {
         e.preventDefault();
-        console.log("TogglePerformanceOverlayEvent");
         this.eventBus.emit(new TogglePerformanceOverlayEvent());
       }
🤖 Fix all issues with AI agents
In `@src/client/HelpModal.ts`:
- Line 3: The code in HelpModal that reads stored keybinds (used alongside
getDefaultKeybinds) currently drops entries when the stored value is a string[];
fix this by normalizing any array values before filtering: detect
Array.isArray(value) and convert to a single string (e.g., value.join(" ") or
another delimiter you use for display) then proceed with the existing
filter/format logic so no saved keybinds are ignored; update the read/normalize
logic in the HelpModal code path that merges stored keybinds with
getDefaultKeybinds so arrays are handled consistently.

In `@src/client/UserSettingModal.ts`:
- Around line 184-213: In showKeybindError, don't split the translated string by
displayKey (this breaks translations when displayKey is short); instead pass a
unique placeholder token into translateText (e.g., replace displayKey with
"__KEY_TOKEN__" in the translation args) and then replace that token in the
resulting message with the styled <span> for displayKey (use string replace or
split on the token) so you inject the formatted key without risk of accidental
matches; update the code paths around messageKey, displayKey, and translateText
accordingly.
🧹 Nitpick comments (3)
src/client/components/baseComponents/setting/SettingKeybind.ts (1)

274-310: Consider extracting shared modifier logic.

getPointerModifiers and getWheelModifiers have nearly identical code. A shared helper would reduce duplication.

♻️ Proposed refactor
+  private getEventModifiers(e: { ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean }): string[] {
+    const modifiers = new Set(this.activeModifiers);
+    if (e.ctrlKey) modifiers.add("ControlLeft");
+    if (e.shiftKey) modifiers.add("ShiftLeft");
+    if (e.altKey) modifiers.add("AltLeft");
+    if (e.metaKey) modifiers.add("MetaLeft");
+    return this.orderModifiers(Array.from(modifiers));
+  }
+
   private getPointerModifiers(e: PointerEvent): string[] {
-    const modifiers = new Set(this.activeModifiers);
-
-    if (e.ctrlKey) {
-      modifiers.add("ControlLeft");
-    }
-    if (e.shiftKey) {
-      modifiers.add("ShiftLeft");
-    }
-    if (e.altKey) {
-      modifiers.add("AltLeft");
-    }
-    if (e.metaKey) {
-      modifiers.add("MetaLeft");
-    }
-
-    return this.orderModifiers(Array.from(modifiers));
+    return this.getEventModifiers(e);
   }

   private getWheelModifiers(e: WheelEvent): string[] {
-    const modifiers = new Set(this.activeModifiers);
-
-    if (e.ctrlKey) {
-      modifiers.add("ControlLeft");
-    }
-    if (e.shiftKey) {
-      modifiers.add("ShiftLeft");
-    }
-    if (e.altKey) {
-      modifiers.add("AltLeft");
-    }
-    if (e.metaKey) {
-      modifiers.add("MetaLeft");
-    }
-
-    return this.orderModifiers(Array.from(modifiers));
+    return this.getEventModifiers(e);
   }
src/client/graphics/layers/UnitDisplay.ts (1)

101-114: Good helper for handling legacy formats, but the array check is unnecessary.

The getKeybindValue method correctly handles both legacy string values and the newer {value, key} object format. However, the array check on line 108 is overly defensive—keybinds are always stored as strings ({ value: string, key: string }), never as arrays. The code works fine, but this array handling can be removed to simplify the method.

src/client/InputHandler.ts (1)

275-276: Add guards for window access in moveInterval callback.

The code accesses window.innerWidth and window.innerHeight at lines 275–276 inside the moveInterval callback without checking if window is defined. While tests pass with jsdom, this code will fail if initialize() is called in non-browser environments (SSR, Node.js test runners without DOM).

Add a guard at the start of the interval callback:

this.moveInterval = setInterval(() => {
  if (typeof window === "undefined") return;
  let deltaX = 0;
  let deltaY = 0;
  // ... rest of callback

Alternatively, consider moving the zoom calculation outside the interval or ensuring initialize() only runs in browser contexts.

@ryanbarlow97 ryanbarlow97 marked this pull request as draft January 18, 2026 01:17
@ryanbarlow97 ryanbarlow97 added Refactor Code cleanup, technical debt, refactoring, and architecture improvements. Bugfix Fixes a bug labels Jan 18, 2026
@ryanbarlow97 ryanbarlow97 added this to the v30 milestone Jan 18, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/InputHandler.ts`:
- Around line 480-497: The onScroll handler currently uses deltaX when deltaY is
0, causing pure horizontal scroll to be treated as zoom/ratio; change onScroll
in InputHandler to detect horizontal-only scroll (deltaY === 0 && deltaX !== 0)
and do not run the ratio/zoom keybind logic or emit ZoomEvent (i.e., return
early or let the pan handler handle it), while preserving existing
matchesWheelKeybind checks for mixed/vertical scroll; ensure you reference
onScroll, matchesWheelKeybind, and the ZoomEvent emission when making the
change.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/InputHandler.ts`:
- Around line 503-518: onAttackRatioScroll currently treats horizontal-only
wheel events as valid input, causing Shift+horizontal trackpad swipes to change
attack ratio; add the same horizontal-only guard used in onScroll so the handler
returns early when deltaY === 0 && deltaX !== 0. Update the method
onAttackRatioScroll to compute scrollValue but first check if event.deltaY === 0
&& event.deltaX !== 0 and return if true, then proceed to determine direction,
lookup this.keybinds.attackRatioScrollDown/Up, call this.matchesWheelKeybind and
emit new AttackRatioEvent as before.
- Around line 328-336: The keyup handler contains unreachable checks using
matchesKeybind against keybinds.attackRatioScrollDown and attackRatioScrollUp
(which have primary keys like "ScrollDown"/"ScrollUp")—keyboard events never
produce those codes; remove the two if-blocks that call matchesKeybind and emit
AttackRatioEvent from the keyup handler (or alternatively, if you intended
keyboard alternatives, change those keybind definitions to use real keyboard
codes and keep the checks). Keep the existing wheel handling that uses
matchesWheelKeybind (used in the wheel handlers) as-is.
🧹 Nitpick comments (2)
src/client/InputHandler.ts (2)

480-501: Minor: deltaX fallback is now unreachable.

After the early return on line 481-483, when deltaY === 0 at line 485, deltaX must also be zero (otherwise we'd have returned). The ternary is now effectively dead logic.

This could be simplified:

-    const scrollValue = event.deltaY === 0 ? event.deltaX : event.deltaY;
-    if (scrollValue === 0) return;
+    if (event.deltaY === 0) return;

658-684: Consider extracting "Null" to a constant.

Line 659 checks for the magic string "Null" as a disabled marker. For clarity and to prevent typos elsewhere, extract to a constant:

const KEYBIND_DISABLED = "Null";

This is a minor readability improvement.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/InputHandler.ts`:
- Line 21: The MOUSE_CODES set in InputHandler.ts is missing "MouseRight",
causing matchesPointerKeybind() to reject parsed.primary === "MouseRight" even
though getPointerCode() returns "MouseRight"; update the MOUSE_CODES constant
(the Set named MOUSE_CODES) to include "MouseRight" so matchesPointerKeybind()
will find the pointer code and correctly match right-click keybinds (ensure you
keep "MouseLeft" and "MouseMiddle" as well).
🧹 Nitpick comments (2)
src/client/InputHandler.ts (2)

6-6: Prefer KeybindMap for typed keybinds.

This avoids stringly-typed access and keeps key names as a typed union.

♻️ Proposed refactor
-import { getDefaultKeybinds } from "./Keybinds";
+import { getDefaultKeybinds, KeybindMap } from "./Keybinds";

-  private keybinds: Record<string, string> = {};
+  private keybinds: KeybindMap = getDefaultKeybinds();

   private reloadKeybindsFromStorage() {
-    let saved: Record<string, string> = {};
+    let saved: Partial<KeybindMap> = {};
     try {
       const parsed = JSON.parse(
         localStorage.getItem("settings.keybinds") ?? "{}",
       );
@@
-      ) as Record<string, string>;
+      ) as Partial<KeybindMap>;
     } catch (e) {
       console.warn("Invalid keybinds JSON:", e);
     }

Also applies to: 171-172, 598-631


652-677: Reject keybinds with multiple primary keys.

Right now, "KeyA+KeyB" silently keeps only the last primary. That makes broken keybinds hard to spot. Consider treating this as invalid to avoid surprises.

🔧 Suggested guard
   const modifiers: string[] = [];
   let primary: string | null = null;
   for (const part of parts) {
     if (MODIFIER_CODES.has(part)) {
       modifiers.push(part);
     } else {
-      primary = part;
+      if (primary) return null; // multiple primaries -> invalid
+      primary = part;
     }
   }

@ryanbarlow97 ryanbarlow97 marked this pull request as ready for review January 18, 2026 15:54
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 18, 2026
@ryanbarlow97 ryanbarlow97 requested a review from a team as a code owner January 22, 2026 01:47
@ryanbarlow97 ryanbarlow97 marked this pull request as draft January 22, 2026 22:20
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/HelpModal.ts`:
- Around line 2-6: There are duplicate imports causing TS2300; consolidate the
duplicated import groups by merging the lit/decorators.js imports into one
statement that exports customElement, query and state, and merge the
../client/Utils imports into one statement that exports getDefaultKeybinds,
translateText and TUTORIAL_VIDEO_URL (so replace the two separate import blocks
with a single import from "lit/decorators.js" containing customElement, query,
state and a single import from "../client/Utils" containing getDefaultKeybinds,
translateText, TUTORIAL_VIDEO_URL).
- Around line 37-46: The code that normalizes stored keybind arrays in HelpModal
(the branch handling typeof v === "object" with "value" and the Array.isArray(v)
branch) joins arrays with a space, which mismatches getKeyLabel's expectation of
"+"-delimited composites; change both join calls to use "+" as the delimiter so
array-based keybinds (e.g., ["ShiftLeft","KeyR"]) become "ShiftLeft+KeyR" and
are parsed correctly by getKeyLabel.
🧹 Nitpick comments (2)
src/client/HelpModal.ts (2)

348-368: Two rows share the same action label for ratio change.

Lines 348–358 (keyboard) and 359–368 (scroll wheel) both display help_modal.action_ratio_change. Users might wonder why there are two identical-looking rows. Consider adding a note like "(Alternate)" to the scroll variant, or combining them into one row.


35-35: Redundant cast after type guard.

isKeybindObject already narrows v to { value: string }, so (v as any).value can be simplified to v.value.

♻️ Tiny cleanup
-            if (this.isKeybindObject(v)) return [k, (v as any).value];
+            if (this.isKeybindObject(v)) return [k, v.value];

Comment on lines +2 to 6
import { customElement, state } from "lit/decorators.js";
import { getDefaultKeybinds } from "../client/Keybinds";
import { translateText } from "../client/Utils";
import { customElement, query, state } from "lit/decorators.js";
import { translateText, TUTORIAL_VIDEO_URL } from "../client/Utils";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Duplicate imports break the build.

Lines 2–4 duplicate the imports already on lines 5–6. The CI pipeline fails with TS2300: Duplicate identifier for customElement, state, and translateText. Merge both groups into single import statements and keep query and TUTORIAL_VIDEO_URL which lines 2–4 dropped.

🐛 Fix: merge into two clean imports
-import { customElement, state } from "lit/decorators.js";
-import { getDefaultKeybinds } from "../client/Keybinds";
-import { translateText } from "../client/Utils";
-import { customElement, query, state } from "lit/decorators.js";
-import { translateText, TUTORIAL_VIDEO_URL } from "../client/Utils";
+import { customElement, query, state } from "lit/decorators.js";
+import { getDefaultKeybinds } from "../client/Keybinds";
+import { translateText, TUTORIAL_VIDEO_URL } from "../client/Utils";
📝 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
import { customElement, state } from "lit/decorators.js";
import { getDefaultKeybinds } from "../client/Keybinds";
import { translateText } from "../client/Utils";
import { customElement, query, state } from "lit/decorators.js";
import { translateText, TUTORIAL_VIDEO_URL } from "../client/Utils";
import { customElement, query, state } from "lit/decorators.js";
import { getDefaultKeybinds } from "../client/Keybinds";
import { translateText, TUTORIAL_VIDEO_URL } from "../client/Utils";
🧰 Tools
🪛 Biome (2.3.13)

[error] 5-5: Shouldn't redeclare 'customElement'. Consider to delete it or rename it.

'customElement' is defined here:

(lint/suspicious/noRedeclare)


[error] 5-5: Shouldn't redeclare 'state'. Consider to delete it or rename it.

'state' is defined here:

(lint/suspicious/noRedeclare)


[error] 6-6: Shouldn't redeclare 'translateText'. Consider to delete it or rename it.

'translateText' is defined here:

(lint/suspicious/noRedeclare)

🪛 GitHub Actions: 🧪 CI

[error] 2-2: Duplicate identifier 'customElement'.

🪛 GitHub Actions: 🚀 Deploy

[error] 2-6: Duplicate identifier 'customElement'. Duplicate identifier 'state'. Duplicate identifier 'translateText'.


[error] 2-6: TS2300: Duplicate identifier errors detected during TypeScript compilation in HelpModal.ts.

🤖 Prompt for AI Agents
In `@src/client/HelpModal.ts` around lines 2 - 6, There are duplicate imports
causing TS2300; consolidate the duplicated import groups by merging the
lit/decorators.js imports into one statement that exports customElement, query
and state, and merge the ../client/Utils imports into one statement that exports
getDefaultKeybinds, translateText and TUTORIAL_VIDEO_URL (so replace the two
separate import blocks with a single import from "lit/decorators.js" containing
customElement, query, state and a single import from "../client/Utils"
containing getDefaultKeybinds, translateText, TUTORIAL_VIDEO_URL).

Comment on lines +37 to +46
if (
typeof v === "object" &&
v !== null &&
"value" in (v as any) &&
Array.isArray((v as any).value)
) {
return [k, (v as any).value.join(" ")];
}

if (Array.isArray(v)) return [k, v.join(" ")];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src/client && head -100 HelpModal.ts | tail -80

Repository: openfrontio/OpenFrontIO

Length of output: 2247


🏁 Script executed:

# Find the full HelpModal.ts file and check its size
wc -l src/client/HelpModal.ts

Repository: openfrontio/OpenFrontIO

Length of output: 95


🏁 Script executed:

# Look for getKeyLabel function
rg "getKeyLabel" src/client/HelpModal.ts -A 15

Repository: openfrontio/OpenFrontIO

Length of output: 1194


🏁 Script executed:

# Look for getDefaultKeybinds
rg "getDefaultKeybinds\|DEFAULT_KEYBINDS" src/client/ -A 10

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Check the full getKeybinds method
rg "getKeybinds\(\)" src/client/HelpModal.ts -A 40

Repository: openfrontio/OpenFrontIO

Length of output: 1938


🏁 Script executed:

# Search for getDefaultKeybinds function definition
rg "function getDefaultKeybinds|const getDefaultKeybinds|export.*getDefaultKeybinds" src/client/ -A 20

Repository: openfrontio/OpenFrontIO

Length of output: 222


🏁 Script executed:

# Look in the entire src/client directory for DEFAULT_KEYBINDS or getDefaultKeybinds
rg "getDefaultKeybinds" src/ -B 2 -A 25

Repository: openfrontio/OpenFrontIO

Length of output: 8863


🏁 Script executed:

# Check the Keybinds.ts file to see DEFAULT_KEYBINDS
cat -n src/client/Keybinds.ts | head -60

Repository: openfrontio/OpenFrontIO

Length of output: 1751


Array values joined with space, but getKeyLabel splits on +.

When a stored keybind value is an array (e.g. ["ShiftLeft", "KeyR"]), it gets joined with " " (space). But getKeyLabel splits composite keys on "+" and all default keybinds use "+" as the composite delimiter (e.g. "ShiftLeft+KeyR"). This means array-based composite keybinds will render as an unrecognized label like "ShiftLeft KeyR" instead of "⇧ Shift + R".

Join arrays with "+" to match the default format and parsing logic.

Fix: use "+" as the join delimiter
             Array.isArray((v as any).value)
            ) {
-              return [k, (v as any).value.join(" ")];
+              return [k, (v as any).value.join("+")];
             }

-            if (Array.isArray(v)) return [k, v.join(" ")];
+            if (Array.isArray(v)) return [k, v.join("+")];
📝 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
if (
typeof v === "object" &&
v !== null &&
"value" in (v as any) &&
Array.isArray((v as any).value)
) {
return [k, (v as any).value.join(" ")];
}
if (Array.isArray(v)) return [k, v.join(" ")];
if (
typeof v === "object" &&
v !== null &&
"value" in (v as any) &&
Array.isArray((v as any).value)
) {
return [k, (v as any).value.join("+")];
}
if (Array.isArray(v)) return [k, v.join("+")];
🤖 Prompt for AI Agents
In `@src/client/HelpModal.ts` around lines 37 - 46, The code that normalizes
stored keybind arrays in HelpModal (the branch handling typeof v === "object"
with "value" and the Array.isArray(v) branch) joins arrays with a space, which
mismatches getKeyLabel's expectation of "+"-delimited composites; change both
join calls to use "+" as the delimiter so array-based keybinds (e.g.,
["ShiftLeft","KeyR"]) become "ShiftLeft+KeyR" and are parsed correctly by
getKeyLabel.

@wiki-me
Copy link

wiki-me commented Feb 10, 2026

What about "retaliate"? will that now have a key? because it would be really useful to default it to "r", i don't see anything about this.

@ryanbarlow97
Copy link
Contributor Author

ryanbarlow97 commented Feb 10, 2026

What about "retaliate"? will that now have a key? because it would be really useful to default it to "r", i don't see anything about this.

this PR is more to make it that everything is changeable - that would be a separate PR

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

Labels

Bugfix Fixes a bug Refactor Code cleanup, technical debt, refactoring, and architecture improvements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants