Skip to content

Fix shell art and fullscreen - #6520

Merged
matthewevans merged 2 commits into
mainfrom
ship/native-p2p-engine
Jul 22, 2026
Merged

Fix shell art and fullscreen#6520
matthewevans merged 2 commits into
mainfrom
ship/native-p2p-engine

Conversation

@matthewevans

@matthewevans matthewevans commented Jul 22, 2026

Copy link
Copy Markdown
Member

Allows Scryfall card art in the Tauri CSP and uses Tauri's native window fullscreen API.

Summary by CodeRabbit

  • New Features

    • Added fullscreen support for the desktop app, including synchronized fullscreen state and toggling.
    • Enabled approved card image sources for improved image loading.
  • Bug Fixes

    • Improved connection error handling during game setup, including clearer recoverable and non-recoverable errors.
    • Ensured pending setup actions are properly resolved when a connection closes.
  • Accessibility

    • Improved keyboard focus visibility for the preferences slider.

@matthewevans
matthewevans enabled auto-merge July 22, 2026 22:41
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR improves native pregame error propagation, adds Tauri fullscreen support, permits fullscreen and Scryfall image access in desktop configuration, and updates slider focus-visible styling.

Changes

Native pregame and desktop client updates

Layer / File(s) Summary
Native pregame lifecycle errors
client/src/adapter/ws-adapter.ts, client/src/adapter/__tests__/ws-adapter.test.ts
Initialization waiters now reject on disposal with recoverable WS_CLOSED errors, while illegal decks produce non-recoverable DECK_REJECTED errors covered by tests.
Fullscreen and focus behavior
client/src/components/chrome/FullscreenButton.tsx, client/src/components/settings/PreferencesModal.tsx
Fullscreen state and toggling support both browser and Tauri APIs, and the multiplier slider uses explicit visible focus outlines.
Desktop permissions and image policy
client/src-tauri/capabilities/default.json, client/src-tauri/tauri.conf.json
Tauri fullscreen permission is added to capabilities, and CSP allows local, data, and Scryfall images.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: parthmishra

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main themes of the PR: fullscreen behavior and card-art loading.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ship/native-p2p-engine

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
client/src-tauri/capabilities/default.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

client/src-tauri/tauri.conf.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
client/src/adapter/ws-adapter.ts (1)

1329-1343: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

DECK_REJECTED branch is now unreachable — deck-not-legal errors get the wrong code.

rejectInitialization(...) at Line 1331 sets this.initReject = null whenever an init promise is pending. The subsequent data.message.includes("Deck not legal") && this.initReject guard is therefore always false, so initialize() now rejects with actionRejectionError(...) (ACTION_REJECTED, recoverable=true) instead of DECK_REJECTED (recoverable=false). Any caller that branches on the deck-rejected code/recoverability will misclassify the failure.

Reorder so the deck-specific rejection wins before the generic init rejection:

🐛 Proposed reordering
       case "Error": {
         const data = msg.data as { message: string };
-        this.rejectInitialization(actionRejectionError(data.message));
         this.rejectPregameMutation(actionRejectionError(data.message));
         this.rejectAbandon(actionRejectionError(data.message));
         this.emit({ type: "error", message: data.message });
         if (data.message.includes("Deck not legal") && this.initReject) {
           this.initReject(
             new AdapterError("DECK_REJECTED", data.message, false),
           );
           this.initResolve = null;
           this.initReject = null;
+          this.rejectInitialization(actionRejectionError(data.message));
+          break;
         }
+        this.rejectInitialization(actionRejectionError(data.message));
         break;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/adapter/ws-adapter.ts` around lines 1329 - 1343, Reorder the Error
handling in the message-processing switch so the “Deck not legal” case invokes
initReject with AdapterError("DECK_REJECTED", ..., false) before
rejectInitialization can clear it. Preserve the existing generic
rejectInitialization, pregame, abandon, and error-event behavior for all other
messages.
🧹 Nitpick comments (1)
client/src/providers/GameProvider.tsx (1)

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

Native-P2P-options resolution is duplicated across GameProvider.tsx and multiplayerStore.ts. Both independently call ensureNativeEngine(nativeEngineKey), build the same { expectedServerVersion } shape, and warn-and-fall-back-to-WASM on failure — one shared helper would keep the two hosting entry points from drifting.

  • client/src/providers/GameProvider.tsx#L793-L841: extract the ensureNativeEngine/nativeP2P construction (and its WASM-fallback catch) into a shared helper (e.g. resolveNativeP2POptions) importable from both files.
  • client/src/stores/multiplayerStore.ts#L1062-L1078: replace this block's inline ensureNativeEngine/nativeP2P construction with the same shared helper.
🤖 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 `@client/src/providers/GameProvider.tsx` around lines 793 - 841, Extract the
duplicated native-P2P resolution logic into a shared resolveNativeP2POptions
helper, including ensureNativeEngine, expectedServerVersion construction, and
warning-based WASM fallback. Update client/src/providers/GameProvider.tsx lines
793-841 and client/src/stores/multiplayerStore.ts lines 1062-1078 to call the
helper, preserving the existing native-resume failure behavior and passing each
caller’s nativeEngineKey/context as needed.
🤖 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 `@client/src/adapter/ws-adapter.ts`:
- Around line 706-714: Update dispose() to reject all pending lifecycle waiters
by invoking rejectInitialization(...) (or explicitly rejecting the pregame and
gameStarted waiters) before clearing initResolve/initReject. Ensure
initializePregame() and waitForGameStarted() settle even when disposal occurs
before this.ws exists, while preserving the existing mutation and abandonment
rejections.

In `@client/src/components/chrome/FullscreenButton.tsx`:
- Around line 38-54: Update the toggle callback in FullscreenButton’s toggle
function to catch rejected Tauri and browser fullscreen operations, including
import and API failures. Preserve the existing fullscreen state when any
operation fails, and ensure the promise invoked by the onClick handler does not
produce an unhandled rejection.
- Around line 38-45: Update the Tauri branch of the FullscreenButton state
management around toggle to subscribe to the current window’s fullscreen state
changes and update isFullscreen whenever external Tauri changes occur. Reuse the
Tauri window API as the source of truth, clean up the listener on unmount or
dependency changes, and preserve the existing browser fullscreenchange handling.

In `@client/src/components/settings/PreferencesModal.tsx`:
- Line 1240: Update the range input styling near the PreferencesModal control to
preserve a visible focus fallback in forced-colors mode: remove
focus-visible:outline-none or replace it with a focus-visible outline and
suitable offset. Keep the existing focus-visible ring styling intact.

---

Outside diff comments:
In `@client/src/adapter/ws-adapter.ts`:
- Around line 1329-1343: Reorder the Error handling in the message-processing
switch so the “Deck not legal” case invokes initReject with
AdapterError("DECK_REJECTED", ..., false) before rejectInitialization can clear
it. Preserve the existing generic rejectInitialization, pregame, abandon, and
error-event behavior for all other messages.

---

Nitpick comments:
In `@client/src/providers/GameProvider.tsx`:
- Around line 793-841: Extract the duplicated native-P2P resolution logic into a
shared resolveNativeP2POptions helper, including ensureNativeEngine,
expectedServerVersion construction, and warning-based WASM fallback. Update
client/src/providers/GameProvider.tsx lines 793-841 and
client/src/stores/multiplayerStore.ts lines 1062-1078 to call the helper,
preserving the existing native-resume failure behavior and passing each caller’s
nativeEngineKey/context as needed.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e3c29c2-ed76-441b-945e-9b409cb27c29

📥 Commits

Reviewing files that changed from the base of the PR and between ddf2cf2 and eadffd4.

📒 Files selected for processing (30)
  • client/src-tauri/capabilities/default.json
  • client/src-tauri/tauri.conf.json
  • client/src/adapter/__tests__/ws-adapter.test.ts
  • client/src/adapter/p2p-adapter.ts
  • client/src/adapter/ws-adapter.ts
  • client/src/components/chrome/FullscreenButton.tsx
  • client/src/components/menu/MenuActionTile.tsx
  • client/src/components/settings/PreferencesModal.tsx
  • client/src/components/settings/__tests__/PreferencesModal.priorityPassing.test.tsx
  • client/src/i18n/locales/de/settings.json
  • client/src/i18n/locales/en/settings.json
  • client/src/i18n/locales/es/settings.json
  • client/src/i18n/locales/fr/settings.json
  • client/src/i18n/locales/it/settings.json
  • client/src/i18n/locales/pl/settings.json
  • client/src/i18n/locales/pt/settings.json
  • client/src/providers/GameProvider.tsx
  • client/src/services/gamePersistence.ts
  • client/src/stores/multiplayerStore.ts
  • client/src/stores/preferencesStore.ts
  • crates/lobby-broker/src/protocol.rs
  • crates/phase-server/src/main.rs
  • crates/server-core/src/client_message_wire_guard.rs
  • crates/server-core/src/lib.rs
  • crates/server-core/src/persist.rs
  • crates/server-core/src/protocol.rs
  • crates/server-core/src/session.rs
  • fixtures/adapter-contract/game_started.json
  • fixtures/adapter-contract/state_update.json
  • scripts/check-protocol-version.mjs

Comment thread client/src/adapter/ws-adapter.ts
Comment thread client/src/components/chrome/FullscreenButton.tsx Outdated
Comment thread client/src/components/chrome/FullscreenButton.tsx
Comment thread client/src/components/settings/PreferencesModal.tsx Outdated
@matthewevans
matthewevans disabled auto-merge July 22, 2026 22:55
@matthewevans
matthewevans enabled auto-merge July 22, 2026 23:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
client/src/adapter/ws-adapter.ts (2)

438-443: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop sockets that finish connecting after disposal.

If dispose() runs while openPhaseSocket() is pending, this.ws is still null. Once the await resumes, this method still installs the socket, starts pinging, and sends the setup frame—leaking a native connection and potentially creating/attaching a session after teardown. Check this.disposed immediately after the await, close socket, and return.

As per path instructions, check async races, including state updates after unmount.

🤖 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 `@client/src/adapter/ws-adapter.ts` around lines 438 - 443, Update attachSocket
so it checks this.disposed immediately after openPhaseSocket resolves; if
disposed, close the newly opened socket and return before assigning it, starting
pinging, or sending the setup frame. Preserve the existing setup flow when
disposal has not occurred.

Source: Path instructions


520-541: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject all lifecycle waiters when the socket closes.

initializePregame() and waitForGameStarted() can be pending together, but this else if chain rejects only pregameReject; gameStartedReject then remains pending forever. Replace the branch with one rejectInitialization(new AdapterError(...)) call so every active lifecycle latch settles.

As per path instructions, check async races, including state updates after unmount.

🤖 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 `@client/src/adapter/ws-adapter.ts` around lines 520 - 541, Update the
socket-close handling around initializePregame and waitForGameStarted to call
rejectInitialization once with the WS_CLOSED AdapterError instead of using the
mutually exclusive initReject/pregameReject/gameStartedReject chain. Ensure
rejectInitialization settles every active lifecycle waiter and clears their
state, including concurrent waiters, while preserving safe behavior if closure
races with unmount or other state updates.

Source: Path instructions

🤖 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 `@client/src/components/chrome/FullscreenButton.tsx`:
- Around line 39-55: Update the fullscreen synchronization effect around
syncFullscreen and the onResized registration so syncFullscreen handles its own
errors, including resize-triggered rejections. When onResized resolves,
immediately invoke the returned unsubscribe if active is already false;
otherwise retain it for normal cleanup. Preserve the existing active guard and
cleanup behavior in the effect.

---

Outside diff comments:
In `@client/src/adapter/ws-adapter.ts`:
- Around line 438-443: Update attachSocket so it checks this.disposed
immediately after openPhaseSocket resolves; if disposed, close the newly opened
socket and return before assigning it, starting pinging, or sending the setup
frame. Preserve the existing setup flow when disposal has not occurred.
- Around line 520-541: Update the socket-close handling around initializePregame
and waitForGameStarted to call rejectInitialization once with the WS_CLOSED
AdapterError instead of using the mutually exclusive
initReject/pregameReject/gameStartedReject chain. Ensure rejectInitialization
settles every active lifecycle waiter and clears their state, including
concurrent waiters, while preserving safe behavior if closure races with unmount
or other state updates.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b92ec05-6c3a-4df1-b16c-ab26256e90f1

📥 Commits

Reviewing files that changed from the base of the PR and between eadffd4 and fdd7858.

📒 Files selected for processing (4)
  • client/src/adapter/__tests__/ws-adapter.test.ts
  • client/src/adapter/ws-adapter.ts
  • client/src/components/chrome/FullscreenButton.tsx
  • client/src/components/settings/PreferencesModal.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/src/components/settings/PreferencesModal.tsx

Comment on lines +39 to +55
const syncFullscreen = async () => {
const fullscreen = await appWindow.isFullscreen();
if (active) setIsFullscreen(fullscreen);
};

await syncFullscreen();
unlisten = await appWindow.onResized(() => {
void syncFullscreen();
});
} catch (error) {
console.warn("[phase.rs] Could not synchronize Tauri fullscreen state.", error);
}
})();

return () => {
active = false;
unlisten?.();

Copy link
Copy Markdown
Contributor

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

Close the asynchronously registered listener and catch later sync failures.

If cleanup runs before onResized() resolves, unlisten is assigned afterward and never called. Also, resize-triggered syncFullscreen() rejections bypass the outer try/catch. Dispose immediately when inactive and handle errors inside syncFullscreen.

As per path instructions, client/src/** requires unmount cleanup for subscriptions and calls out async races after unmount.

🤖 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 `@client/src/components/chrome/FullscreenButton.tsx` around lines 39 - 55,
Update the fullscreen synchronization effect around syncFullscreen and the
onResized registration so syncFullscreen handles its own errors, including
resize-triggered rejections. When onResized resolves, immediately invoke the
returned unsubscribe if active is already false; otherwise retain it for normal
cleanup. Preserve the existing active guard and cleanup behavior in the effect.

Source: Path instructions

@matthewevans
matthewevans force-pushed the ship/native-p2p-engine branch from fdd7858 to 48fac68 Compare July 22, 2026 23:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
client/src/adapter/ws-adapter.ts (2)

693-699: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancel the in-flight handshake when disposing.

dispose() can run before openPhaseSocket() resolves, when this.ws is still null. The pending attachSocket() then continues, installs a socket, starts pinging, and sends the setup frame after disposal. Abort or retain-and-close the pending transport, and return from attachSocket() if disposed before assigning this.ws.

As per path instructions, check “async races (state updates after unmount, two reconnects at once).”

🤖 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 `@client/src/adapter/ws-adapter.ts` around lines 693 - 699, Update dispose and
the attachSocket/openPhaseSocket flow to cancel or retain-and-close any
transport that resolves after disposal. In attachSocket, check the disposed
state immediately after the pending transport resolves and before assigning
this.ws, starting pinging, or sending the setup frame; close the resolved
transport and return when disposed, while preserving normal initialization
otherwise.

Source: Path instructions


524-541: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject every native lifecycle waiter on socket close.

When initializePregame() and waitForGameStarted() are both pending, this if/else if chain rejects only pregameReject; gameStarted remains pending forever. Use rejectInitialization(...) here, as dispose() already does, and add a close-path regression test.

Suggested change
-      if (this.initReject) {
-        // ...
-      } else if (this.pregameReject) {
-        // ...
-      } else if (this.gameStartedReject) {
-        // ...
+      if (this.initReject || this.pregameReject || this.gameStartedReject) {
+        this.rejectInitialization(
+          new AdapterError("WS_CLOSED", "Connection closed before initialization completed", true),
+        );
       } else if (this.snapshot !== null || this.playerToken !== null) {

As per path instructions, check “async races.”

🤖 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 `@client/src/adapter/ws-adapter.ts` around lines 524 - 541, Update the
socket-close handling in the adapter lifecycle flow to call the existing
rejectInitialization(...) helper instead of branching across initReject,
pregameReject, and gameStartedReject, ensuring every pending native lifecycle
waiter is rejected and cleared. Add a regression test covering simultaneous
initializePregame() and waitForGameStarted() pending promises when the socket
closes, including the async-race behavior.

Source: Path instructions

🤖 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 `@client/src/adapter/ws-adapter.ts`:
- Around line 1332-1335: Replace the message-text check in the initialization
rejection handling with a typed wire-protocol rejection/error discriminant,
using the enum value for illegal decks to create DECK_REJECTED and preserving
actionRejectionError for other typed values. Update the associated test to send
the typed discriminant instead of relying on “Deck not legal” text, and follow
the protocol’s typed-enum convention throughout.

---

Outside diff comments:
In `@client/src/adapter/ws-adapter.ts`:
- Around line 693-699: Update dispose and the attachSocket/openPhaseSocket flow
to cancel or retain-and-close any transport that resolves after disposal. In
attachSocket, check the disposed state immediately after the pending transport
resolves and before assigning this.ws, starting pinging, or sending the setup
frame; close the resolved transport and return when disposed, while preserving
normal initialization otherwise.
- Around line 524-541: Update the socket-close handling in the adapter lifecycle
flow to call the existing rejectInitialization(...) helper instead of branching
across initReject, pregameReject, and gameStartedReject, ensuring every pending
native lifecycle waiter is rejected and cleared. Add a regression test covering
simultaneous initializePregame() and waitForGameStarted() pending promises when
the socket closes, including the async-race behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f0deb98-869a-46fa-93f8-c55ba10fdb3d

📥 Commits

Reviewing files that changed from the base of the PR and between fdd7858 and 48fac68.

📒 Files selected for processing (6)
  • client/src-tauri/capabilities/default.json
  • client/src-tauri/tauri.conf.json
  • client/src/adapter/__tests__/ws-adapter.test.ts
  • client/src/adapter/ws-adapter.ts
  • client/src/components/chrome/FullscreenButton.tsx
  • client/src/components/settings/PreferencesModal.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/src-tauri/capabilities/default.json
  • client/src/components/chrome/FullscreenButton.tsx

Comment on lines +1332 to +1335
const initializationError = data.message.includes("Deck not legal")
? new AdapterError("DECK_REJECTED", data.message, false)
: actionRejectionError(data.message);
this.rejectInitialization(initializationError);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not derive a protocol error code from message text.

includes("Deck not legal") is brittle: server wording changes can turn a fatal illegal-deck failure into a recoverable ACTION_REJECTED, or misclassify an unrelated error. Add a typed rejection/error discriminant to the wire protocol and map that enum to DECK_REJECTED; update this test to send the typed value.

As per path instructions, use “typed enums over stringly/bool data.”

🤖 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 `@client/src/adapter/ws-adapter.ts` around lines 1332 - 1335, Replace the
message-text check in the initialization rejection handling with a typed
wire-protocol rejection/error discriminant, using the enum value for illegal
decks to create DECK_REJECTED and preserving actionRejectionError for other
typed values. Update the associated test to send the typed discriminant instead
of relying on “Deck not legal” text, and follow the protocol’s typed-enum
convention throughout.

Source: Path instructions

@matthewevans
matthewevans added this pull request to the merge queue Jul 22, 2026
Merged via the queue into main with commit b7054dd Jul 22, 2026
15 checks passed
@matthewevans
matthewevans deleted the ship/native-p2p-engine branch July 22, 2026 23:56
jsdevninja pushed a commit to jsdevninja/phase that referenced this pull request Jul 24, 2026
* Fix shell art and fullscreen

* Fix shell review findings

---------

Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant