Skip to content

feat(reticulum): integrate lrgp-rs Games tab for TTT and Chess - #791

Merged
rinchen merged 2 commits into
mainfrom
lgrp
Aug 5, 2026
Merged

feat(reticulum): integrate lrgp-rs Games tab for TTT and Chess#791
rinchen merged 2 commits into
mainfrom
lgrp

Conversation

@rinchen

@rinchen rinchen commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Wire sibling lrgp-rs into the Reticulum sidecar (LrgpRouter + SQLite store, LXMF 0xFB/0xFD, HTTP /api/v1/games/*, WS games.*) so mesh-client peers can play Tic-Tac-Toe and Chess over LRGP.
  • Add dedicated reticulum:games-* IPC (rate-limited; blocked on generic proxy), hasLrgpGames, and a full Games left-rail tab with challenge entry points from Peers/Chat.
  • Track Ratspeak Games tab parity via update.sh (games-parity) and docs/reticulum-games-parity.md.

Closes #773

Test plan

  • Clone stack includes lrgp-rs (pnpm/clone script with RS_LRGP_REF); sidecar builds with rns-stack + optional lrgp
  • Start Reticulum stack; Games tab appears (Gamepad2) when hasLrgpGames
  • Challenge a peer from Peers or Chat DM; accept/decline; complete a Tic-Tac-Toe and Chess game mesh-client ↔ mesh-client
  • Confirm dedicated games IPC works and generic proxy* rejects /api/v1/games/*
  • WS games.update / games.action_result refresh the Games panel without full poll thrash
  • pnpm run check:pr (or CI) green; i18n keys under gamesPanel.* / tabs.games present

Summary by CodeRabbit

  • New Features

    • Added Reticulum Games support with a dedicated Games tab.
    • Added Tic-Tac-Toe and Chess challenges, interactive boards, session tracking, and game actions.
    • Added challenge controls from chats and peer lists.
    • Added support for accepting, declining, resigning, drawing, resending, reading, and deleting game sessions.
    • Added real-time game updates and action results.
  • Documentation

    • Added setup, interoperability, parity, and troubleshooting guidance for Reticulum Games.
  • Tests

    • Added comprehensive coverage for game boards, sessions, challenges, updates, and integration behavior.

Wire sibling lrgp-rs into the sidecar with dedicated games IPC, a Reticulum Games panel, and Ratspeak parity tracking so mesh-client peers can play over LRGP.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rinchen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 566ee5a6-fb0c-40d4-bfd2-92d834ae5594

📥 Commits

Reviewing files that changed from the base of the PR and between 840d33b and 5267445.

⛔ Files ignored due to path filters (16)
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (22)
  • docs/ci-cd.md
  • reticulum-sidecar/src/api/games.rs
  • reticulum-sidecar/src/stack/games_session.rs
  • reticulum-sidecar/src/stack/live.rs
  • scripts/clone-ratspeak-stack.test.mjs
  • src/main/ipc/reticulum-handlers.ts
  • src/main/ipc/reticulum-proxy-rate-limit.contract.test.ts
  • src/renderer/components/GamesPanel.test.tsx
  • src/renderer/components/GamesPanel.tsx
  • src/renderer/components/games/ChessBoard.test.tsx
  • src/renderer/components/games/ChessBoard.tsx
  • src/renderer/components/games/TicTacToeBoard.test.tsx
  • src/renderer/components/games/TicTacToeBoard.tsx
  • src/renderer/components/reticulum/ReticulumGameChallengeButton.test.tsx
  • src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx
  • src/renderer/lib/reticulum/clearReticulumSessionStores.test.ts
  • src/renderer/lib/reticulum/reticulumGamesSession.test.ts
  • src/renderer/lib/reticulum/reticulumGamesSession.ts
  • src/renderer/stores/reticulumGamesStore.test.ts
  • src/renderer/stores/reticulumGamesStore.ts
  • src/shared/games-types.test.ts
  • src/shared/games-types.ts
📝 Walkthrough

Walkthrough

Added LRGP Tic-Tac-Toe and Chess support for Reticulum. The change covers the lrgp-rs stack, sidecar session handling, REST and IPC APIs, WebSocket events, capability-gated UI, game boards, challenge actions, persistence, and tests.

Changes

LRGP Games integration

Layer / File(s) Summary
Stack setup and documentation
AGENTS.md, docs/*, reticulum-sidecar/Cargo.toml, reticulum-sidecar/README.md, scripts/*
Added lrgp-rs checkout, build configuration, release tracking, parity guidance, troubleshooting, and documentation for the Games feature.
Sidecar game-session stack
reticulum-sidecar/src/stack/*, reticulum-sidecar/src/api/*, src/shared/games-types.ts
Added LRGP app registration, session persistence, LXMF handling, action validation, routing, rollback, resend support, WebSocket events, shared contracts, and REST endpoints.
Dedicated games IPC bridge
src/main/ipc/*, src/preload/index.ts, src/shared/electron-api.types.ts
Added typed games IPC channels with validation and a separate rate limit. Generic proxy routes reject games API paths.
Renderer state and event handling
src/renderer/stores/*, src/renderer/runtime/*, src/renderer/lib/reticulum/*, src/renderer/vitest.electronApiMock.ts
Added games state management, API helpers, metadata readers, WebSocket event handling, session cleanup, and test mocks.
Games UI and capability gating
src/renderer/App.tsx, src/renderer/components/*, src/renderer/lib/*
Added the Games tab, challenge controls, Chess and Tic-Tac-Toe boards, session actions, lazy loading, Reticulum-only capability gating, and UI tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant GamesPanel
  participant ElectronIPC
  participant ReticulumSidecar
  participant GamesSessionManager
  participant LXMF
  Operator->>GamesPanel: Select game or challenge peer
  GamesPanel->>ElectronIPC: Send games action
  ElectronIPC->>ReticulumSidecar: Validate dedicated games request
  ReticulumSidecar->>GamesSessionManager: Prepare and route action
  GamesSessionManager->>LXMF: Sign and send LRGP envelope
  LXMF-->>GamesSessionManager: Deliver message
  GamesSessionManager-->>ElectronIPC: Emit games.update or games.action_result
  ElectronIPC-->>GamesPanel: Update session state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.95% 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 clearly summarizes the integration of lrgp-rs Games support for Tic-Tac-Toe and Chess in Reticulum.
Linked Issues check ✅ Passed The changes satisfy issue #773 by adding lrgp-rs integration, a Games UI, supported games, and Reticulum communication support.
Out of Scope Changes check ✅ Passed The code, tests, documentation, and tooling changes directly support the lrgp-rs Games integration objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lgrp

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.

@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: 5

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (11)
docs/ci-cd.md-156-158 (1)

156-158: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the earlier CI sibling list.

The new text lists rsLXST and lrgp-rs, but the sidecar CI section at Line 79 still lists only rsReticulum, rsLXMF, and rsNomad. Update that section to list all five siblings and their RS_*_REF overrides. Otherwise, the CI and local-parity instructions conflict.

🤖 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 `@docs/ci-cd.md` around lines 156 - 158, Update the sidecar CI sibling list
near the existing rsReticulum/rsLXMF/rsNomad entries to include all five
repositories: rsReticulum, rsLXMF, rsNomad, rsLXST, and lrgp-rs. Add the
corresponding RS_*_REF override documentation for rsLXST and lrgp-rs, matching
the established format so the CI and local-parity instructions agree.
scripts/clone-ratspeak-stack.test.mjs-145-156 (1)

145-156: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the LRGP pin wiring.

The test name includes optional RS_LRGP_REF pin support, but the test only checks that the variable appears in the script. It does not verify that the lrgp-rs ensure_repo call passes this variable. Add a source-contract assertion for the complete call, or execute the LRGP pin path with a local tag fixture.

Suggested source-contract assertion
+    expect(cloneScript).toContain(
+      'ensure_repo "${LRGP_DIR}" \'https://github.com/ratspeak/lrgp-rs.git\' "${RS_LRGP_REF}" \'lrgp-rs\'',
+    );

As per path instructions, use source-contract tests where runtime integration is impractical.

🤖 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 `@scripts/clone-ratspeak-stack.test.mjs` around lines 145 - 156, Extend the
test named “clones lrgp-rs with optional RS_LRGP_REF pin support” to verify that
the lrgp-rs ensure_repo invocation passes the RS_LRGP_REF variable, asserting
the complete call in cloneScript rather than only checking the variable
declaration. Keep the existing local remote behavior and assertions unchanged.

Source: Path instructions

src/renderer/components/games/TicTacToeBoard.tsx-40-41 (1)

40-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a fallback for unknown session statuses.

Line 41 builds the translation key from session.status. GameSession.status is typed as string, and src/shared/games-types.ts Line 100 documents that the sidecar may add new values. If the sidecar reports a status outside GamesSessionStatus, no matching key exists and i18next renders the raw key gamesPanel.status.<value> in the board.

Pass a defaultValue so an unknown status shows readable text.

🌐 Proposed fix for the dynamic status key
   } else if (!isActive) {
-    statusText = t(`gamesPanel.status.${session.status}`);
+    statusText = t(`gamesPanel.status.${session.status}`, {
+      defaultValue: t('gamesPanel.status.unknown'),
+    });
   } else if (isMyTurn) {
🤖 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/renderer/components/games/TicTacToeBoard.tsx` around lines 40 - 41,
Update the statusText assignment in TicTacToeBoard’s inactive-session branch to
provide i18next’s defaultValue when translating the dynamic session.status key.
Use a readable fallback for statuses not present in the gamesPanel.status
translations while preserving existing translations for known statuses.
src/shared/games-types.ts-138-142 (1)

138-142: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Normalize query strings before the games API gate.

assertProxyApiPath returns apiPath unchanged, so /api/v1/games?peer=x bypasses the generic proxy deny gate and uses the shared 300/min proxy bucket instead of reticulum:games*. Apply an equivalent query-stripping/normalization before comparing against the /api/v1/games base in all reticulum:proxyGet/Post/Delete paths.

🤖 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/shared/games-types.ts` around lines 138 - 142, The games API path check
in isGamesApiPath must normalize or strip the query string before comparing
against the base path and GAMES_API_PREFIX. Apply this normalized path
consistently in the reticulum:proxyGet, reticulum:proxyPost, and
reticulum:proxyDelete gating flows so query-bearing games URLs use the
reticulum:games* bucket.
src/main/ipc/reticulum-handlers.ts-386-424 (1)

386-424: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Return a consistent invalid-session-id envelope from the games session handlers.

reticulum:gamesResend, reticulum:gamesMarkRead, reticulum:gamesDeleteSession, and reticulum:gamesSessionDetail throw invalid_session_id via assertGamesSessionId, while reticulum:gamesAction resolves { ok: false, error }. These IPC promises can reject for malformed input; return the same non-rejection error result across the games channels.

🤖 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/main/ipc/reticulum-handlers.ts` around lines 386 - 424, Update the games
session handlers `reticulum:gamesResend`, `reticulum:gamesMarkRead`,
`reticulum:gamesDeleteSession`, and `reticulum:gamesSessionDetail` so
`assertGamesSessionId` failures return the same `{ ok: false, error }` envelope
used by `reticulum:gamesAction` instead of rejecting the IPC promise. Handle
validation before constructing the request path, while preserving existing proxy
failure handling for valid session IDs.
reticulum-sidecar/src/stack/live.rs-3128-3137 (1)

3128-3137: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The resend contract does not match when the envelope is cached.

The doc comment states that resend applies "after a transient send failure". GamesSessionManager::commit_action is the only writer of last_envelope, and it runs only after a successful send. send_game_action calls rollback_action on send failure, which does not cache the envelope. So after a failed send, prepare_resend returns no_previous_action.

Either cache the envelope bytes before the send attempt, or correct the comment to state that resend applies only to the last successfully sent action.

🤖 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 `@reticulum-sidecar/src/stack/live.rs` around lines 3128 - 3137, The
documentation for resend_last_game_action promises retrying transient send
failures, but last_envelope is only cached after successful sends. Either update
the send/rollback flow so the envelope is cached before the send attempt and
remains available after failure, or revise the resend_last_game_action doc
comment to specify that it only resends the last successfully sent action; keep
prepare_resend behavior consistent with the chosen contract.
src/renderer/components/GamesPanel.tsx-161-167 (1)

161-167: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translation keys are built from open-ended sidecar strings without a fallback. GameSession.status is typed as string in src/shared/games-types.ts because the sidecar can add new values, and FEN piece characters are equally unconstrained. When a value has no matching key, i18next renders the raw key path in the UI or in an accessible name, for example gamesPanel.status.cancelled. Pass a defaultValue at each interpolation site.

  • src/renderer/components/GamesPanel.tsx#L161-L167: add { defaultValue: session.app_id } to gamesPanel.apps.${session.app_id} and { defaultValue: session.status } to gamesPanel.status.${session.status}.
  • src/renderer/components/games/ChessBoard.tsx#L92-L92: add { defaultValue: session.status } to gamesPanel.status.${session.status}.
  • src/renderer/components/games/ChessBoard.tsx#L147-L152: add { defaultValue: piece } to gamesPanel.chess.pieceNames.${piece}, so an unexpected FEN character does not leak a key path into the square's accessible name.
🤖 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/renderer/components/GamesPanel.tsx` around lines 161 - 167, Add i18next
defaultValue fallbacks for all open-ended translation interpolations: in
src/renderer/components/GamesPanel.tsx lines 161-167, use session.app_id for
gamesPanel.apps and session.status for gamesPanel.status; in
src/renderer/components/games/ChessBoard.tsx line 92, use session.status for
gamesPanel.status; and in lines 147-152, use piece for
gamesPanel.chess.pieceNames. Preserve the existing translation keys and
rendering behavior when keys exist.
src/renderer/components/GamesPanel.tsx-340-348 (1)

340-348: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Confirm the destructive delete action.

deleteGamesSession permanently removes the session and its history. The resign action in the same component already uses ConfirmModal, but delete runs on a single click next to the resign and resend controls. A misclick loses the session with no undo.

Reuse the existing ConfirmModal pattern for delete.

🛡️ Proposed confirmation for the delete action
-  const [confirmResign, setConfirmResign] = useState(false);
+  const [confirmResign, setConfirmResign] = useState(false);
+  const [confirmDelete, setConfirmDelete] = useState(false);
               <button
                 type="button"
                 className="rounded bg-amber-950/60 px-3 py-1 text-xs font-medium text-amber-200/70 disabled:opacity-50"
                 aria-label={t('gamesPanel.deleteSessionAria')}
                 disabled={actionBusy}
-                onClick={() => void deleteGamesSession(selectedSession.session_id)}
+                onClick={() => {
+                  setConfirmDelete(true);
+                }}
               >
                 {t('gamesPanel.deleteSession')}
               </button>
+      {confirmDelete && selectedSession && (
+        <ConfirmModal
+          title={t('gamesPanel.deleteConfirmTitle')}
+          message={t('gamesPanel.deleteConfirmMessage')}
+          confirmLabel={t('gamesPanel.deleteSession')}
+          danger
+          onCancel={() => {
+            setConfirmDelete(false);
+          }}
+          onConfirm={() => {
+            setConfirmDelete(false);
+            void deleteGamesSession(selectedSession.session_id);
+          }}
+        />
+      )}
     </div>

Add gamesPanel.deleteConfirmTitle and gamesPanel.deleteConfirmMessage to every locale file that defines gamesPanel.resignConfirmTitle.

🤖 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/renderer/components/GamesPanel.tsx` around lines 340 - 348, Update the
delete flow around the GamesPanel delete button and deleteGamesSession to
require confirmation before permanently removing a session. Reuse the existing
ConfirmModal pattern used by the resign action, wiring its confirmation callback
to deleteGamesSession and preserving actionBusy behavior. Add
gamesPanel.deleteConfirmTitle and gamesPanel.deleteConfirmMessage to every
locale that defines gamesPanel.resignConfirmTitle.
src/renderer/components/GamesPanel.test.tsx-50-143 (1)

50-143: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add coverage for the draw-offer branch and the resend button.

GamesPanel.test.tsx does not cover either conditional path in GamesPanel.tsx:

  • draw_offered: true only exercises GAMES_CMD.DRAW_OFFER; cover GAMES_CMD.DRAW_ACCEPT / GAMES_CMD.DRAW_DECLINE by selecting a session with that metadata.
  • The resend button appears only when the selected session has a failed lastActionResult.session_id; cover that by sending an action that resolves { ok: false, session_id: 's1' }.
🤖 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/renderer/components/GamesPanel.test.tsx` around lines 50 - 143, Extend
the GamesPanel tests to cover both missing conditional paths: select a session
with draw_offered metadata and verify the draw accept/decline actions invoke
sendAction with the expected commands, then configure sendAction to resolve {
ok: false, session_id: 's1' } for an action and verify the selected session’s
resend button appears and triggers the resend behavior. Reuse
renderAndSelectSession and the existing session/action helpers.

Source: Coding guidelines

src/renderer/lib/reticulum/clearReticulumSessionStores.test.ts-47-51 (1)

47-51: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Seed a session before asserting that cleanup removes sessions.

Line 48 selects an ID but does not add a session. The length assertion passes even if clear() stops clearing sessions.

Insert a valid s1 session before calling clearReticulumSessionStores().

As per path instructions, **/*.test.ts: Prefer behavioral assertions; skip style-only test nits.`

🤖 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/renderer/lib/reticulum/clearReticulumSessionStores.test.ts` around lines
47 - 51, Update the “clears the games store” test to seed a valid s1 session in
useReticulumGamesStore before calling clearReticulumSessionStores(). Keep the
existing selectedSessionId and sessions length assertions so the test verifies
cleanup removes the seeded session.

Source: Path instructions

src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx-39-41 (1)

39-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent an open challenge menu from sending after disable.

The trigger is disabled at Line 55. If the Reticulum stack stops after the menu opens, the option buttons remain active and can still call sendGamesChallenge().

  • src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx#L39-L41: guard handleChallenge() when disabled is true.
  • src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx#L66-L80: close or hide the menu when disabled, and disable app option buttons.
  • src/renderer/components/reticulum/ReticulumGameChallengeButton.test.tsx#L40-L43: open the menu, rerender with disabled, then verify that no option can dispatch sendAction.
Proposed fix
   useEffect(() => {
     if (!menuOpen) return;
@@
   }, [menuOpen]);
 
+  useEffect(() => {
+    if (disabled && menuOpen) setMenuOpen(false);
+  }, [disabled, menuOpen]);
+
   async function handleChallenge(appId: GamesAppId) {
+    if (disabled) return;
     setMenuOpen(false);
@@
               key={appId}
               type="button"
+              disabled={disabled}

As per coding guidelines, Behavioral changes must ship with a passing test.

🤖 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/renderer/components/reticulum/ReticulumGameChallengeButton.tsx` around
lines 39 - 41, The open challenge menu can still dispatch a game challenge after
the control becomes disabled. In
src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx#L39-L41,
guard handleChallenge so disabled prevents sending; at `#L66-L80`, close or hide
the menu when disabled and disable its app option buttons. In
src/renderer/components/reticulum/ReticulumGameChallengeButton.test.tsx#L40-L43,
open the menu, rerender with disabled, and verify selecting an option does not
dispatch sendAction.

Source: Coding guidelines

🧹 Nitpick comments (3)
reticulum-sidecar/src/stack/games_session.rs (1)

58-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the last_envelope resend cache.

commit_action inserts one envelope per session_id and never evicts. Entries are removed only by delete_session. Sessions accumulate for the process lifetime, so the map and its envelope bytes grow without a limit. Add a cap or evict entries for sessions that the store no longer holds.

Also applies to: 437-444

🤖 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 `@reticulum-sidecar/src/stack/games_session.rs` around lines 58 - 59, Bound the
resend cache used by commit_action by evicting entries for session IDs no longer
retained by the store, or by enforcing a fixed capacity with eviction. Update
last_envelope and the associated insertion path so stale entries cannot
accumulate for the process lifetime, while preserving resend behavior for active
sessions and delete_session cleanup.
src/main/ipc/reticulum-proxy-rate-limit.contract.test.ts (1)

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

Tie the rate-limit assertion to the games limiter.

toMatch(/max:\s*600/) matches any max: 600 in reticulum-handlers.ts. The test passes even if the games limiter uses a different ceiling. Assert the label and the max in one match so the 600 ceiling is bound to reticulum:games.

The negative proxy assertions match only a single-quoted literal path on the same call. A template literal or a variable would evade them. Consider asserting that no proxy invocation mentions /api/v1/games anywhere in the preload source.

♻️ Proposed tightening
-    expect(HANDLERS_SOURCE).toContain("label: 'reticulum:games'");
-    expect(HANDLERS_SOURCE).toMatch(/max:\s*600/);
+    expect(HANDLERS_SOURCE).toMatch(/label:\s*'reticulum:games'[\s\S]{0,200}?max:\s*600/);
@@
-    expect(preload).not.toMatch(/invoke\('reticulum:proxyGet',\s*'\/api\/v1\/games/);
-    expect(preload).not.toMatch(/invoke\('reticulum:proxyPost',\s*'\/api\/v1\/games/);
+    expect(preload).not.toMatch(/invoke\(\s*'reticulum:proxy(Get|Post)'[\s\S]{0,80}?\/api\/v1\/games/);
🤖 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/main/ipc/reticulum-proxy-rate-limit.contract.test.ts` around lines 57 -
68, Strengthen the contract assertions in the test around HANDLERS_SOURCE by
matching the reticulum:games limiter label and max: 600 in one pattern, ensuring
the ceiling is tied to that limiter. Broaden both preload negative assertions to
reject any reticulum:proxyGet or reticulum:proxyPost invocation whose source
contains /api/v1/games, regardless of whether the path uses a literal, template,
or variable.
src/renderer/components/games/ChessBoard.test.tsx (1)

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

Add coverage for the flipped board and the promotion move.

The suite covers the unflipped white path. Two branches introduced in ChessBoard.tsx have no test:

  • flipped (my_color: 'b') reverses rows and columns and remaps actualRankIdx/actualFileIdx at lines 103-104 and 135-136. An index inversion error here is silent.
  • Promotion at lines 118-120 appends q to the UCI string. No test asserts the e7e8q form.
🧪 Proposed additional tests
it('maps clicks to the correct squares when the board is flipped for black', async () => {
  const onMove = vi.fn();
  render(
    <ChessBoard
      session={makeSession({
        metadata: { ...makeSession().metadata, my_color: 'b', turn: 'me' },
      })}
      onMove={onMove}
    />,
  );

  await userEvent.click(screen.getByRole('button', { name: /^e7,/ }));
  await userEvent.click(screen.getByRole('button', { name: /^e5,/ }));

  expect(onMove).toHaveBeenCalledWith('e7e5');
});

it('appends the queen promotion suffix when a pawn reaches the last rank', async () => {
  const onMove = vi.fn();
  render(
    <ChessBoard
      session={makeSession({
        metadata: {
          ...makeSession().metadata,
          fen: '4k3/4P3/8/8/8/8/8/4K3 w - - 0 1',
        },
      })}
      onMove={onMove}
    />,
  );

  await userEvent.click(screen.getByRole('button', { name: /^e7,/ }));
  await userEvent.click(screen.getByRole('button', { name: /^e8,/ }));

  expect(onMove).toHaveBeenCalledWith('e7e8q');
});
🤖 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/renderer/components/games/ChessBoard.test.tsx` around lines 43 - 152, Add
tests in the ChessBoard suite covering both untested branches: render a
black-turn session and verify clicks map to e7e5 on the flipped board, then
render a promotion position and verify e7 to e8 emits the UCI move e7e8q. Reuse
the existing makeSession and onMove patterns without changing production
behavior.

Source: Coding guidelines

🤖 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 `@reticulum-sidecar/src/api/games.rs`:
- Around line 55-61: Extend input validation in games_action to call
reject_oversize for app_id, command, and session_id in addition to dest_hash,
and apply the same bound to the id path parameter in games_session_detail,
games_session_resend, games_session_read, and games_session_delete. Ensure each
oversized external game/session/action input returns the existing structured
rejection response before persistence, caching, or further processing.

In `@reticulum-sidecar/src/stack/live.rs`:
- Around line 328-335: Move the synchronous SQLite work triggered by
games_session_cb.handle_inbound_lxmf out of the router delivery callback
registered in the live stack. Preserve the existing inbound filtering and
message handling, but dispatch the persistence path asynchronously or otherwise
defer it until after deliver_unpacked_lxmf releases router.lock().await,
ensuring persist_session_from_state never runs while the callback holds the
router lock.

In `@src/renderer/components/games/TicTacToeBoard.tsx`:
- Around line 62-79: Update the cell aria-label in TicTacToeBoard’s cells.map
rendering to include both the localized cell position and whether the cell is
empty or occupied, using new corresponding empty and occupied translation keys
in every locale. Preserve the existing index numbering and update
TicTacToeBoard.test.tsx queries to match the expanded accessible names.

In `@src/renderer/lib/reticulum/reticulumGamesSession.ts`:
- Around line 114-121: Update markGamesSessionRead to capture the session’s
revision before awaiting window.electronAPI.reticulum.games.markRead, then clear
unread only when the session still exists and that revision remains current
after the IPC call. Preserve newer games.update changes, and add a test covering
an update received during markRead.

In `@src/renderer/stores/reticulumGamesStore.ts`:
- Around line 11-18: Validate complete external game contracts before mutating
state: update isGameSession/asGameSession in
src/renderer/stores/reticulumGamesStore.ts:11-18 to require every GameSession
field, including finite timestamps and unread values; reject incomplete rows at
src/renderer/stores/reticulumGamesStore.ts:53-55, validate
GamesUpdateEventPayload before reading its session at :80-100, and validate
complete app manifests before storing them at :103-110. Add malformed
valid-session-id fixtures covering missing required fields in
src/renderer/stores/reticulumGamesStore.test.ts:28-32.

---

Other comments:
In `@docs/ci-cd.md`:
- Around line 156-158: Update the sidecar CI sibling list near the existing
rsReticulum/rsLXMF/rsNomad entries to include all five repositories:
rsReticulum, rsLXMF, rsNomad, rsLXST, and lrgp-rs. Add the corresponding
RS_*_REF override documentation for rsLXST and lrgp-rs, matching the established
format so the CI and local-parity instructions agree.

In `@reticulum-sidecar/src/stack/live.rs`:
- Around line 3128-3137: The documentation for resend_last_game_action promises
retrying transient send failures, but last_envelope is only cached after
successful sends. Either update the send/rollback flow so the envelope is cached
before the send attempt and remains available after failure, or revise the
resend_last_game_action doc comment to specify that it only resends the last
successfully sent action; keep prepare_resend behavior consistent with the
chosen contract.

In `@scripts/clone-ratspeak-stack.test.mjs`:
- Around line 145-156: Extend the test named “clones lrgp-rs with optional
RS_LRGP_REF pin support” to verify that the lrgp-rs ensure_repo invocation
passes the RS_LRGP_REF variable, asserting the complete call in cloneScript
rather than only checking the variable declaration. Keep the existing local
remote behavior and assertions unchanged.

In `@src/main/ipc/reticulum-handlers.ts`:
- Around line 386-424: Update the games session handlers
`reticulum:gamesResend`, `reticulum:gamesMarkRead`,
`reticulum:gamesDeleteSession`, and `reticulum:gamesSessionDetail` so
`assertGamesSessionId` failures return the same `{ ok: false, error }` envelope
used by `reticulum:gamesAction` instead of rejecting the IPC promise. Handle
validation before constructing the request path, while preserving existing proxy
failure handling for valid session IDs.

In `@src/renderer/components/games/TicTacToeBoard.tsx`:
- Around line 40-41: Update the statusText assignment in TicTacToeBoard’s
inactive-session branch to provide i18next’s defaultValue when translating the
dynamic session.status key. Use a readable fallback for statuses not present in
the gamesPanel.status translations while preserving existing translations for
known statuses.

In `@src/renderer/components/GamesPanel.test.tsx`:
- Around line 50-143: Extend the GamesPanel tests to cover both missing
conditional paths: select a session with draw_offered metadata and verify the
draw accept/decline actions invoke sendAction with the expected commands, then
configure sendAction to resolve { ok: false, session_id: 's1' } for an action
and verify the selected session’s resend button appears and triggers the resend
behavior. Reuse renderAndSelectSession and the existing session/action helpers.

In `@src/renderer/components/GamesPanel.tsx`:
- Around line 161-167: Add i18next defaultValue fallbacks for all open-ended
translation interpolations: in src/renderer/components/GamesPanel.tsx lines
161-167, use session.app_id for gamesPanel.apps and session.status for
gamesPanel.status; in src/renderer/components/games/ChessBoard.tsx line 92, use
session.status for gamesPanel.status; and in lines 147-152, use piece for
gamesPanel.chess.pieceNames. Preserve the existing translation keys and
rendering behavior when keys exist.
- Around line 340-348: Update the delete flow around the GamesPanel delete
button and deleteGamesSession to require confirmation before permanently
removing a session. Reuse the existing ConfirmModal pattern used by the resign
action, wiring its confirmation callback to deleteGamesSession and preserving
actionBusy behavior. Add gamesPanel.deleteConfirmTitle and
gamesPanel.deleteConfirmMessage to every locale that defines
gamesPanel.resignConfirmTitle.

In `@src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx`:
- Around line 39-41: The open challenge menu can still dispatch a game challenge
after the control becomes disabled. In
src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx#L39-L41,
guard handleChallenge so disabled prevents sending; at `#L66-L80`, close or hide
the menu when disabled and disable its app option buttons. In
src/renderer/components/reticulum/ReticulumGameChallengeButton.test.tsx#L40-L43,
open the menu, rerender with disabled, and verify selecting an option does not
dispatch sendAction.

In `@src/renderer/lib/reticulum/clearReticulumSessionStores.test.ts`:
- Around line 47-51: Update the “clears the games store” test to seed a valid s1
session in useReticulumGamesStore before calling clearReticulumSessionStores().
Keep the existing selectedSessionId and sessions length assertions so the test
verifies cleanup removes the seeded session.

In `@src/shared/games-types.ts`:
- Around line 138-142: The games API path check in isGamesApiPath must normalize
or strip the query string before comparing against the base path and
GAMES_API_PREFIX. Apply this normalized path consistently in the
reticulum:proxyGet, reticulum:proxyPost, and reticulum:proxyDelete gating flows
so query-bearing games URLs use the reticulum:games* bucket.

---

Nitpick comments:
In `@reticulum-sidecar/src/stack/games_session.rs`:
- Around line 58-59: Bound the resend cache used by commit_action by evicting
entries for session IDs no longer retained by the store, or by enforcing a fixed
capacity with eviction. Update last_envelope and the associated insertion path
so stale entries cannot accumulate for the process lifetime, while preserving
resend behavior for active sessions and delete_session cleanup.

In `@src/main/ipc/reticulum-proxy-rate-limit.contract.test.ts`:
- Around line 57-68: Strengthen the contract assertions in the test around
HANDLERS_SOURCE by matching the reticulum:games limiter label and max: 600 in
one pattern, ensuring the ceiling is tied to that limiter. Broaden both preload
negative assertions to reject any reticulum:proxyGet or reticulum:proxyPost
invocation whose source contains /api/v1/games, regardless of whether the path
uses a literal, template, or variable.

In `@src/renderer/components/games/ChessBoard.test.tsx`:
- Around line 43-152: Add tests in the ChessBoard suite covering both untested
branches: render a black-turn session and verify clicks map to e7e5 on the
flipped board, then render a promotion position and verify e7 to e8 emits the
UCI move e7e8q. Reuse the existing makeSession and onMove patterns without
changing production behavior.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 50b9c5e3-c1b2-41d3-9a2c-90a7c8025d53

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4a750 and 840d33b.

⛔ Files ignored due to path filters (17)
  • reticulum-sidecar/Cargo.lock is excluded by !**/*.lock
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (55)
  • AGENTS.md
  • docs/ci-cd.md
  • docs/development-environment.md
  • docs/reticulum-games-parity.md
  • docs/reticulum-sidecar-ipc.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • reticulum-sidecar/Cargo.toml
  • reticulum-sidecar/README.md
  • reticulum-sidecar/src/api/games.rs
  • reticulum-sidecar/src/api/mod.rs
  • reticulum-sidecar/src/api/system.rs
  • reticulum-sidecar/src/stack/games_session.rs
  • reticulum-sidecar/src/stack/live.rs
  • reticulum-sidecar/src/stack/mod.rs
  • scripts/clone-ratspeak-stack.sh
  • scripts/clone-ratspeak-stack.test.mjs
  • scripts/i18n-unused-keys.mjs
  • scripts/update.sh
  • scripts/update.test.mjs
  • src/main/index.contract.test.ts
  • src/main/ipc/reticulum-handlers.ts
  • src/main/ipc/reticulum-proxy-rate-limit.contract.test.ts
  • src/preload/index.ts
  • src/renderer/App.tsx
  • src/renderer/components/ChatPanel.tsx
  • src/renderer/components/GamesPanel.test.tsx
  • src/renderer/components/GamesPanel.tsx
  • src/renderer/components/ReticulumPeerListPanel.tsx
  • src/renderer/components/games/ChessBoard.test.tsx
  • src/renderer/components/games/ChessBoard.tsx
  • src/renderer/components/games/TicTacToeBoard.test.tsx
  • src/renderer/components/games/TicTacToeBoard.tsx
  • src/renderer/components/reticulum/ReticulumGameChallengeButton.test.tsx
  • src/renderer/components/reticulum/ReticulumGameChallengeButton.tsx
  • src/renderer/lazyTabPanels.ts
  • src/renderer/lib/appTabMappings.test.ts
  • src/renderer/lib/appTabMappings.ts
  • src/renderer/lib/icons/tabIcons.test.tsx
  • src/renderer/lib/icons/tabIcons.tsx
  • src/renderer/lib/radio/BaseRadioProvider.ts
  • src/renderer/lib/radio/protocol-capabilities.test.ts
  • src/renderer/lib/reticulum/clearReticulumSessionStores.test.ts
  • src/renderer/lib/reticulum/clearReticulumSessionStores.ts
  • src/renderer/lib/reticulum/reticulumGamesMetadata.ts
  • src/renderer/lib/reticulum/reticulumGamesSession.ts
  • src/renderer/lib/tabSlotIds.ts
  • src/renderer/runtime/useReticulumRuntime.games.test.ts
  • src/renderer/runtime/useReticulumRuntime.ts
  • src/renderer/stores/reticulumGamesStore.test.ts
  • src/renderer/stores/reticulumGamesStore.ts
  • src/renderer/vitest.electronApiMock.ts
  • src/shared/electron-api.types.ts
  • src/shared/games-types.test.ts
  • src/shared/games-types.ts
💤 Files with no reviewable changes (1)
  • reticulum-sidecar/src/api/system.rs

Comment thread reticulum-sidecar/src/api/games.rs
Comment on lines 328 to +335
router.register_delivery_callback(move |msg| {
if !msg.incoming {
return;
}
let sender_hex = hex::encode(msg.source_hash);
if games_session_cb.handle_inbound_lxmf(&msg.fields, &sender_hex, &msg.content) {
return;
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect where the LXMF router invokes the delivery callback and whether the router lock is held.
set -euo pipefail
fd -t f -e rs . | xargs -r rg -n -C 8 'delivery_callback'

Repository: Colorado-Mesh/mesh-client

Length of output: 4372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== live.rs delivery callback section =="
sed -n '300,360p' reticulum-sidecar/src/stack/live.rs

echo "== live.rs wrapper section around 2190-2220 =="
sed -n '2160,2235p' reticulum-sidecar/src/stack/live.rs

echo "== locate handle_inbound_lxmf definition and LrgpStore store accesses =="
rg -n "handle_inbound_lxmf|persist_session_from_state|session_detail|struct LrgpStore|impl LrgpStore|async fn .*LrgpStore|pub async fn|pub fn" reticulum-sidecar/src -g '*.rs'

echo "== lxmf delivery callback lock section =="
sed -n '330,365p' reticulum-sidecar/src/stack/lxmf_delivery.rs

Repository: Colorado-Mesh/mesh-client

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== GamesSessionManager and handle_inbound_lxmf files =="
rg -n "struct GamesSessionManager|impl GamesSessionManager|fn spawn|handle_inbound_lxmf|persist_session_from_state|session_detail|LrgpStore|sq" reticulum-sidecar/src -g '*.rs' | head -n 240

echo "== candidate file excerpts =="
for f in $(rg -l "handle_inbound_lxmf|GamesSessionManager" reticulum-sidecar/src -g '*.rs'); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,260p' "$f"
done

Repository: Colorado-Mesh/mesh-client

Length of output: 24486


Keep SQLite work off the router delivery callback.

deliver_unpacked_lxmf holds router.lock().await while calling the delivery callback, and that callback calls games_session_cb.handle_inbound_lxmf(...) synchronously. Inbound LRGP messages then wait on synchronous SQLite storage work if handle_inbound_lxmf reaches persist_session_from_state. Move the relevant store update out of the callback path, or at least move that call itself off the async path.

🤖 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 `@reticulum-sidecar/src/stack/live.rs` around lines 328 - 335, Move the
synchronous SQLite work triggered by games_session_cb.handle_inbound_lxmf out of
the router delivery callback registered in the live stack. Preserve the existing
inbound filtering and message handling, but dispatch the persistence path
asynchronously or otherwise defer it until after deliver_unpacked_lxmf releases
router.lock().await, ensuring persist_session_from_state never runs while the
callback holds the router lock.

Comment thread src/renderer/components/games/TicTacToeBoard.tsx
Comment thread src/renderer/lib/reticulum/reticulumGamesSession.ts
Comment thread src/renderer/stores/reticulumGamesStore.ts
Defer inbound LRGP SQLite persist off the LXMF router lock, tighten games input validation and store contracts, and close Games UI races around mark-read, delete confirm, and disabled challenges.
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.

Add lrgp-rs (games)

1 participant