Skip to content

Integrated Stockfish wasm - #3

Merged
MohamedSayed0573 merged 1 commit into
mainfrom
stockfish
Jul 25, 2026
Merged

Integrated Stockfish wasm#3
MohamedSayed0573 merged 1 commit into
mainfrom
stockfish

Conversation

@MohamedSayed0573

@MohamedSayed0573 MohamedSayed0573 commented Jul 24, 2026

Copy link
Copy Markdown
Owner
  • Integrated Stockfish 18 WebAssembly for computer games through a dedicated Web Worker and UCI command interface.
  • Added useStockfish to manage engine searches, validate and apply AI moves, and handle worker cleanup.
  • Added timer functionality with shared useTimer and Timer components for both players.
  • Added a reusable sidebar displaying move history and game-over details.
  • Refactored computer games to support Stockfish, timers, board orientation, game state tracking, and move history.
  • Updated player games to use the shared timer and sidebar components.

Note

Integrate Stockfish 18 WebAssembly engine into the vs-computer game mode

  • Adds the Stockfish 18 lite wasm binary and JS loader to apps/client/public/stockfish/, and a worker module that exposes the engine via Web Worker.
  • Introduces a useStockfish hook that sends UCI commands to the worker, receives bestmove responses, and applies the engine's move to the chess.js game instance at depth 15.
  • Rewrites computerGame.tsx to randomly assign the player's side, drive engine moves via useStockfish, display per-side countdown timers, and show a sidebar with move history and game-over details.
  • Extracts Timer and SideBar into shared components reused by both computerGame.tsx and playerGame.tsx.
  • Risk: the wasm binary is served as a static public asset with no integrity check; the worker path is hardcoded and will break if the public directory structure changes.
📊 Macroscope summarized f8275de. 7 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

Copilot AI review requested due to automatic review settings July 24, 2026 16:07
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chesslab Ready Ready Preview, Comment Jul 24, 2026 4:09pm

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an embedded Stockfish.js runtime and Worker integration for UCI chess searches. New hooks manage Stockfish moves and per-side countdown timers. Shared Timer and SideBar components provide player labels, clocks, game-over details, and move history. ComputerChessBoard now uses Stockfish instead of random moves, while PlayerGame adopts the shared display components and authenticated player naming.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: integrating Stockfish WebAssembly support.

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.

Copilot AI 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.

Pull request overview

This PR integrates a Stockfish (WASM-backed) engine into the client for computer games and refactors the in-game UI by extracting reusable Timer and Sidebar components.

Changes:

  • Added a Stockfish Web Worker wrapper plus a useStockfish hook to drive engine moves in computerGame.
  • Introduced a shared Timer component and useTimer hook; extracted the move-history/game-over sidebar into chessSidebar.
  • Updated playerGame and computerGame pages to use the new UI components.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/client/src/stockfish/stockfish.ts Creates and exports a shared Stockfish Worker instance.
apps/client/src/pages/playerGame.tsx Switches to shared Timer + SideBar components and uses user name for display.
apps/client/src/pages/computerGame.tsx Reworks computer game flow to use Stockfish + timer + sidebar; adds game-over detection.
apps/client/src/hooks/useTimer.tsx Adds a simple chess clock hook for local games.
apps/client/src/hooks/useStockfish.tsx Adds a hook to initialize Stockfish and apply bestmove replies to a chess.js game.
apps/client/src/components/Timer.tsx New reusable timer display component.
apps/client/src/components/chessSidebar.tsx New reusable sidebar for game-over info and move history.
apps/client/public/stockfish/stockfish-18-lite-single.js Adds the third-party Stockfish.js worker bundle (GPLv3).
Comments suppressed due to low confidence (1)

apps/client/src/hooks/useStockfish.tsx:46

  • This effect depends on onMove, which is commonly passed as an inline function from the caller (e.g. in computerGame.tsx). That causes this effect to re-run on every render, repeatedly re-sending uci/isready/ucinewgame and re-registering the message handler, which can interrupt searches and degrade performance. Prefer keeping this effect stable (and make onMove stable via useCallback or a ref if needed).
	}, [chessGame, onMove]);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +45
stockfish.onmessage = (e) => {
const message = e.data;
if (message.startsWith("bestmove")) {
const parts = message.split(" ");
const bestMove = parts[1];
if (bestMove && bestMove !== "(none)") {
try {
if (chessGame.isGameOver()) return;
chessGame.move({
from: bestMove.substring(0, 2),
to: bestMove.substring(2, 4),
promotion: bestMove.charAt(4) || "q",
});
onMove();
} catch (err) {
console.error("Error applying Stockfish move:", err);
}
}
}
};

return () => {
stockfish.postMessage("stop");
stockfish.onmessage = null;
};
Comment on lines 38 to 42
function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) {
// type narrow targetSquare potentially being null (e.g. if dropped off board)
if (!targetSquare || chessGameRef.current.turn() !== "w") {
if (chessGame.isGameOver()) return false;

if (!targetSquare) {
return false;

const chessboardOptions: ChessboardOptions = {
position: chessPosition,
position: chessPosition!,

useEffect(() => {
if (gameOverInfo) return;
let interval: NodeJS.Timeout | null = null;
Comment on lines +2 to +5
* Stockfish.js 18 (c) 2026, Chess.com, LLC
* https://github.com/nmrugg/stockfish.js
* License: GPLv3
*
Comment on lines 38 to +39
function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) {
// type narrow targetSquare potentially being null (e.g. if dropped off board)
if (!targetSquare || chessGameRef.current.turn() !== "w") {
if (chessGame.isGameOver()) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High pages/computerGame.tsx:38

onPieceDrop doesn't check that chessGame.turn() matches the human player's side, so the player can drag Stockfish's pieces whenever it's the engine's turn (e.g. immediately at game start when side is "b", or while Stockfish is thinking), corrupting the turn sequence. Add a guard like if (chessGame.turn() !== side) return false; at the top of onPieceDrop.

 function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) {
- if (chessGame.isGameOver()) return false;
+ if (chessGame.isGameOver() || chessGame.turn() !== side) return false;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/client/src/pages/computerGame.tsx around lines 38-39:

`onPieceDrop` doesn't check that `chessGame.turn()` matches the human player's `side`, so the player can drag Stockfish's pieces whenever it's the engine's turn (e.g. immediately at game start when `side` is `"b"`, or while Stockfish is thinking), corrupting the turn sequence. Add a guard like `if (chessGame.turn() !== side) return false;` at the top of `onPieceDrop`.

chessGame,
turn,
stockfishSide: side === "w" ? "b" : "w",
onMove: () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium pages/computerGame.tsx:31

The onMove callback passed to useStockfish is created inline on every render, so each timer tick (once per second) re-runs the effect, sends stop to Stockfish, and reinitializes the worker. A go depth 15 search that takes longer than one second is repeatedly canceled before completing, so Stockfish either plays a prematurely selected move or never moves at all. Store the callback in a ref (or memoize it with useCallback) so timer-driven renders don't restart the worker while a search is in progress.

Also found in 1 other location(s)

apps/client/src/hooks/useStockfish.tsx:46

The initialization effect depends on onMove, but the caller supplies a new inline callback on every render. Timer-driven renders therefore run this cleanup while Stockfish is searching, sending stop; UCI requires stop to terminate the search and emit bestmove, so the requested go depth 15 search is repeatedly cut short and the engine plays a prematurely selected move rather than completing the configured depth. Keep the callback in a ref or otherwise avoid restarting/stopping the worker merely because callback identity changed.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/client/src/pages/computerGame.tsx around line 31:

The `onMove` callback passed to `useStockfish` is created inline on every render, so each timer tick (once per second) re-runs the effect, sends `stop` to Stockfish, and reinitializes the worker. A `go depth 15` search that takes longer than one second is repeatedly canceled before completing, so Stockfish either plays a prematurely selected move or never moves at all. Store the callback in a ref (or memoize it with `useCallback`) so timer-driven renders don't restart the worker while a search is in progress.

Also found in 1 other location(s):
- apps/client/src/hooks/useStockfish.tsx:46 -- The initialization effect depends on `onMove`, but the caller supplies a new inline callback on every render. Timer-driven renders therefore run this cleanup while Stockfish is searching, sending `stop`; UCI requires `stop` to terminate the search and emit `bestmove`, so the requested `go depth 15` search is repeatedly cut short and the engine plays a prematurely selected move rather than completing the configured depth. Keep the callback in a ref or otherwise avoid restarting/stopping the worker merely because callback identity changed.

import type { GameOverInfo } from "@chesslab/shared/types";
import { useEffect, useState } from "react";

export default function useTimer({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium hooks/useTimer.tsx:4

The timer clamps the active clock at 0 but never reports timeout to the caller, so when a player's clock reaches 0:00 the game continues indefinitely — the interval keeps firing and the opponent's moves remain legal. This happens because the hook only tracks local display state via setWhiteDisplayTime/setBlackDisplayTime and returns those values, with no callback or state to signal that a flag fell. Consider exposing a timeout event (e.g., an onTimeout callback or a derived timeout value in the return) so the caller can terminate the game and set gameOverInfo when a clock hits zero.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/client/src/hooks/useTimer.tsx around line 4:

The timer clamps the active clock at `0` but never reports timeout to the caller, so when a player's clock reaches `0:00` the game continues indefinitely — the interval keeps firing and the opponent's moves remain legal. This happens because the hook only tracks local display state via `setWhiteDisplayTime`/`setBlackDisplayTime` and returns those values, with no callback or state to signal that a flag fell. Consider exposing a timeout event (e.g., an `onTimeout` callback or a derived `timeout` value in the return) so the caller can terminate the game and set `gameOverInfo` when a clock hits zero.

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

🤖 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 `@apps/client/src/hooks/useStockfish.tsx`:
- Around line 16-46: Stabilize the onMove callback used by the useEffect in
useStockfish.tsx with a ref, updating the ref when the callback changes while
keeping the Stockfish initialization and cleanup effect from depending on
onMove. Invoke the current ref callback when applying a best move so rerenders
do not resend uci/isready/ucinewgame or stop an active search.

In `@apps/client/src/hooks/useTimer.tsx`:
- Around line 18-25: The countdown logic in useTimer must use wall-clock elapsed
time instead of decrementing by fixed intervals, updating both display time and
player-game state from the same timestamp-based calculation. When either side
reaches zero, stop the countdown and expose an explicit timeout win/draw result
through the hook’s consumer-facing API, preserving normal updates while time
remains.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d5f7aeb9-d1ed-4120-95b1-07df0887e1fc

📥 Commits

Reviewing files that changed from the base of the PR and between 136dc86 and f8275de.

⛔ Files ignored due to path filters (1)
  • apps/client/public/stockfish/stockfish-18-lite-single.wasm is excluded by !**/*.wasm
📒 Files selected for processing (8)
  • apps/client/public/stockfish/stockfish-18-lite-single.js
  • apps/client/src/components/Timer.tsx
  • apps/client/src/components/chessSidebar.tsx
  • apps/client/src/hooks/useStockfish.tsx
  • apps/client/src/hooks/useTimer.tsx
  • apps/client/src/pages/computerGame.tsx
  • apps/client/src/pages/playerGame.tsx
  • apps/client/src/stockfish/stockfish.ts

Comment on lines +16 to +46
useEffect(() => {
stockfish.postMessage("uci");
stockfish.postMessage("isready");
stockfish.postMessage("ucinewgame");

stockfish.onmessage = (e) => {
const message = e.data;
if (message.startsWith("bestmove")) {
const parts = message.split(" ");
const bestMove = parts[1];
if (bestMove && bestMove !== "(none)") {
try {
if (chessGame.isGameOver()) return;
chessGame.move({
from: bestMove.substring(0, 2),
to: bestMove.substring(2, 4),
promotion: bestMove.charAt(4) || "q",
});
onMove();
} catch (err) {
console.error("Error applying Stockfish move:", err);
}
}
}
};

return () => {
stockfish.postMessage("stop");
stockfish.onmessage = null;
};
}, [chessGame, onMove]);

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

Init effect re-runs every render and interrupts the engine.

onMove is supplied inline from ComputerChessBoard (onMove: () => {…}), so its identity changes on every render. Because ComputerChessBoard re-renders roughly once per second (the useTimer countdown updates state), this effect re-runs on every tick: it re-sends uci/isready/ucinewgame (resetting engine state mid-game) and its cleanup posts stop, aborting any search that is in flight. The result is unstable Stockfish behavior.

Stabilize the callback with a ref so initialization runs once per game:

🔧 Proposed fix
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
 import stockfish from "../stockfish/stockfish";
 import type { Chess } from "chess.js";
@@
 }) {
+	const onMoveRef = useRef(onMove);
+	onMoveRef.current = onMove;
+
 	useEffect(() => {
 		stockfish.postMessage("uci");
 		stockfish.postMessage("isready");
 		stockfish.postMessage("ucinewgame");
@@
-						onMove();
+						onMoveRef.current();
 					} catch (err) {
 						console.error("Error applying Stockfish move:", err);
 					}
 				}
 			}
 		};
@@
 		return () => {
 			stockfish.postMessage("stop");
 			stockfish.onmessage = null;
 		};
-	}, [chessGame, onMove]);
+	}, [chessGame]);

Alternatively, wrap onMove in useCallback at the ComputerChessBoard call site (Lines 31-36 of computerGame.tsx), but the ref keeps the fix local to the hook.

📝 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
useEffect(() => {
stockfish.postMessage("uci");
stockfish.postMessage("isready");
stockfish.postMessage("ucinewgame");
stockfish.onmessage = (e) => {
const message = e.data;
if (message.startsWith("bestmove")) {
const parts = message.split(" ");
const bestMove = parts[1];
if (bestMove && bestMove !== "(none)") {
try {
if (chessGame.isGameOver()) return;
chessGame.move({
from: bestMove.substring(0, 2),
to: bestMove.substring(2, 4),
promotion: bestMove.charAt(4) || "q",
});
onMove();
} catch (err) {
console.error("Error applying Stockfish move:", err);
}
}
}
};
return () => {
stockfish.postMessage("stop");
stockfish.onmessage = null;
};
}, [chessGame, onMove]);
import { useEffect, useRef } from "react";
import stockfish from "../stockfish/stockfish";
import type { Chess } from "chess.js";
const onMoveRef = useRef(onMove);
onMoveRef.current = onMove;
useEffect(() => {
stockfish.postMessage("uci");
stockfish.postMessage("isready");
stockfish.postMessage("ucinewgame");
stockfish.onmessage = (e) => {
const message = e.data;
if (message.startsWith("bestmove")) {
const parts = message.split(" ");
const bestMove = parts[1];
if (bestMove && bestMove !== "(none)") {
try {
if (chessGame.isGameOver()) return;
chessGame.move({
from: bestMove.substring(0, 2),
to: bestMove.substring(2, 4),
promotion: bestMove.charAt(4) || "q",
});
onMoveRef.current();
} catch (err) {
console.error("Error applying Stockfish move:", err);
}
}
}
};
return () => {
stockfish.postMessage("stop");
stockfish.onmessage = null;
};
}, [chessGame]);
🤖 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 `@apps/client/src/hooks/useStockfish.tsx` around lines 16 - 46, Stabilize the
onMove callback used by the useEffect in useStockfish.tsx with a ref, updating
the ref when the callback changes while keeping the Stockfish initialization and
cleanup effect from depending on onMove. Invoke the current ref callback when
applying a best move so rerenders do not resend uci/isready/ucinewgame or stop
an active search.

Comment on lines +18 to +25
if (turn === "w") {
interval = setInterval(() => {
setWhiteDisplayTime((prev) => Math.max(0, prev - 1000));
}, 1000);
} else {
interval = setInterval(() => {
setBlackDisplayTime((prev) => Math.max(0, prev - 1000));
}, 1000);

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching useTimer:"
fd -a 'useTimer' . || true

echo
echo "Candidate files:"
git ls-files | rg '(^|/)useTimer\.tsx$|useTimer\.(tsx|ts)$' || true

echo
echo "Changed files stat (if available):"
git diff --stat HEAD~1..HEAD 2>/dev/null || git diff --stat 2>/dev/null || true

echo
echo "Search for useTimer/hooks usage:"
rg -n "useTimer|WhiteDisplayTime|BlackDisplayTime|Timer" apps/client/src || true

Repository: MohamedSayed0573/ChessLab

Length of output: 2234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "useTimer.tsx:"
cat -n apps/client/src/hooks/useTimer.tsx

echo
echo "Timer.tsx:"
cat -n apps/client/src/components/Timer.tsx

echo
echo "computerGame.tsx relevant sections:"
cat -n apps/client/src/pages/computerGame.tsx | sed -n '1,120p'

echo
echo "playerGame.tsx relevant sections:"
cat -n apps/client/src/pages/playerGame.tsx | sed -n '120,170p'

echo
echo "Search zero/timeout/expiry handling:"
rg -n "0|timeout|timed|win|draw|time|ended|gameover|game over|onWin|onEnd|isDraw|checkmate" apps/client/src/pages apps/client/src/components apps/client/src/hooks -g '!public/**' | sed -n '1,220p'

Repository: MohamedSayed0573/ChessLab

Length of output: 20108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "playerGame.tsx timer/game state:"
cat -n apps/client/src/pages/playerGame.tsx | sed -n '1,125p'

echo
echo "computerGame.tsx top:"
cat -n apps/client/src/pages/computerGame.tsx | sed -n '13,25p'

echo
echo "Shared types GameOverInfo:"
rg -n "type GameOverInfo|interface GameOverInfo|GameOverInfo" -S . | sed -n '1,80p'

echo
echo "Read-only JavaScript setInterval throttle probe (non-blocking timing):"
node - <<'JS'
async function probe() {
  const start = performance.now();
  let fired = 0;
  const one = new Promise((resolve) => {
    const id = setInterval(() => {
      fired += 1;
      clearInterval(id);
      resolve();
    }, 1000);
  });
  await one;
  const delta = performance.now() - start;
  console.logJSON({ oneFired, elapsedSeconds: Math.round(delta / 1000 * 1000) / 1000 });
}
function consoleJSON(obj) {
  process.stdout.write(JSON.stringify(obj) + "\n");
}
consoleJSON.probe = () => probe();
Object.assign(consoleJSON.probe, console);
probe();
JS

echo
echo "Static check: hook return values and timer props vs game-over logic"
python3 - <<'PY'
from pathlib import Path
files = {
    "hooks": "apps/client/src/hooks/useTimer.tsx",
    "computer": "apps/client/src/pages/computerGame.tsx",
    "player": "apps/client/src/pages/playerGame.tsx",
}
for name, path in files.items():
    s = Path(path).read_text()
    print(f"\n== {name}: {path} ==")
    print("return { whiteDisplayTime, blackDisplayTime }:", "return { whiteDisplayTime, blackDisplayTime }" in s)
    print("onMove sets isDraw:", "isDraw" in s and "setGameOverInfo" in s)
    print("relevance: turns only use display time values passed through Timer")
PY

Repository: MohamedSayed0573/ChessLab

Length of output: 6943


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "packages/shared/src/types.ts GameOverInfo:"
cat -n packages/shared/src/types.ts | sed -n '1,80p'

echo
echo "apps/server/src/sockets/socket.ts game-over handling:"
cat -n apps/server/src/sockets/socket.ts | sed -n '180,200p'

Repository: MohamedSayed0573/ChessLab

Length of output: 1883


Count down against the wall clock and expose timeout results.

setInterval(() => prev - 1000, 1000) can drift under browser throttling, and this hook stops at 0 without emitting a timeout. Make the countdown wall-clock based, ensure the player-game state is also updated the same way, and let the hook/consumer emit an explicit timeout win/draw status for when a side reaches zero.

🤖 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 `@apps/client/src/hooks/useTimer.tsx` around lines 18 - 25, The countdown logic
in useTimer must use wall-clock elapsed time instead of decrementing by fixed
intervals, updating both display time and player-game state from the same
timestamp-based calculation. When either side reaches zero, stop the countdown
and expose an explicit timeout win/draw result through the hook’s
consumer-facing API, preserving normal updates while time remains.

@MohamedSayed0573
MohamedSayed0573 merged commit 8d34d82 into main Jul 25, 2026
5 checks passed
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.

2 participants