Skip to content

feat: connection state overlay for web app#197

Merged
zvadaadam merged 5 commits into
mainfrom
zvadaadam/web-connection-overlay
Mar 22, 2026
Merged

feat: connection state overlay for web app#197
zvadaadam merged 5 commits into
mainfrom
zvadaadam/web-connection-overlay

Conversation

@zvadaadam

@zvadaadam zvadaadam commented Mar 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a features/connection module with a Zustand state machine tracking WebSocket health through 4 progressive states: CONNECTED → GRACE_PERIOD (2s silence) → RECONNECTING (thin amber banner) → DISCONNECTED (30s+, full banner with retry, content dimmed to 60%)
  • ConnectionBanner slides down from top of content area with amber accent line, escalates copy from "Reconnecting..." to "Connection lost — Your agents are still running" with a Retry button
  • ConnectionOrb replaces BackendStatusIndicator in sidebar footer — always-visible green/amber dot with breathing pulse animation and "Offline" label
  • ServerOfflinePage with warm SVG illustration replaces inline error JSX in both DesktopShell and ServerLayout, with relay-aware copy variant ("Desktop app not detected" vs "Your computer isn't connected")
  • useSendMessage onError now emits sendAttemptFailed to immediately escalate connection state when a user action is blocked mid-flight

Test plan

  • Kill the backend while the web app is open — verify 2s grace period, then "Reconnecting..." bar appears, then at 30s escalates to "Connection lost" with Retry button and dimmed content
  • Restart backend — verify banner slides away and content opacity restores
  • Send a message while disconnected — verify immediate escalation to "Message queued — reconnecting"
  • Load web app with backend stopped — verify ServerOfflinePage with illustration, "Try again" button, and auto-reconnect on backend start
  • Load web app in relay mode with desktop app stopped — verify relay variant copy
  • Check sidebar footer — ConnectionOrb shows green when connected, amber pulse when reconnecting
  • Test with prefers-reduced-motion enabled — verify animations are disabled/reduced

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Connection banner with reconnection/disconnected states, queued-message notice, and retry action
    • Sidebar connection orb showing live connection health
    • Full-page server offline screen with variant-specific copy and retry
    • Connection illustration asset used in offline UI
  • Improvements

    • Immediate escalation on send failures to surface offline state
    • Timed connection-state transitions, visual dimming when disconnected, and reduced-motion accessibility enhancements

Add a connection state machine (features/connection) that tracks WebSocket
health through 4 progressive states: CONNECTED → GRACE_PERIOD (2s) →
RECONNECTING (show thin banner) → DISCONNECTED (30s+, show full banner,
dim content). Includes ConnectionBanner at top of content area,
ConnectionOrb in sidebar footer replacing BackendStatusIndicator,
ServerOfflinePage with warm illustration for first-load/server-off states,
and send-attempt-failed escalation from useSendMessage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 09874114ce65

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@zvadaadam has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 27 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Lite

Run ID: ed0a4fd3-3733-4997-b3b5-18ac7ac0395b

📥 Commits

Reviewing files that changed from the base of the PR and between f0badd2 and 0cc9b2a.

📒 Files selected for processing (4)
  • apps/web/src/features/connection/store/connectionStore.ts
  • apps/web/src/features/connection/ui/ConnectionBanner.tsx
  • apps/web/src/features/connection/ui/ServerOfflinePage.tsx
  • apps/web/src/features/session/api/session.queries.ts
📝 Walkthrough

Walkthrough

Introduce a Zustand connection state machine, a small event bus, init hook, UI (ConnectionBanner, ConnectionOrb, ServerOfflinePage, ConnectionIllustration), and integrations that surface WebSocket send failures and connection lifecycle into layouts and session send-error handling.

Changes

Cohort / File(s) Summary
Documentation
\.claude/agent-memory/code-reviewer/MEMORY.md
Added “Connection State Machine (features/connection)” docs describing lifecycle, guard/re-entry rules, event bus, WS error matching, and UI animation guidance.
State Management
apps/web/src/features/connection/store/connectionStore.ts, apps/web/src/features/connection/lib/connectionEvents.ts
Added a Zustand store implementing states (connectedgrace_periodreconnectingdisconnected), timers/escalation, sendAttemptFailed handling, and a Set-based emit/onSendAttemptFailed event API.
Connection Hooks
apps/web/src/features/connection/hooks/useConnectionState.ts, apps/web/src/features/connection/hooks/useConnectionStateInit.ts
Added UI-facing useConnectionState and useConnectionStateInit (one-time effect) that subscribe to WS connection changes and send-failed events and drive the store.
UI Components
apps/web/src/features/connection/ui/ConnectionBanner.tsx, apps/web/src/features/connection/ui/ConnectionOrb.tsx, apps/web/src/features/connection/ui/ServerOfflinePage.tsx, apps/web/src/features/connection/ui/ConnectionIllustration.tsx
New ConnectionBanner (reconnecting/disconnected, retry), ConnectionOrb (sidebar health indicator), full-page ServerOfflinePage (desktop/relay variants), and SVG ConnectionIllustration.
Feature Exports
apps/web/src/features/connection/index.ts
New barrel export exposing hooks, types, UI components, and emitSendAttemptFailed.
Layout & Shell Integration
apps/web/src/app/layouts/MainContent.tsx, apps/web/src/app/layouts/MainLayout.tsx, apps/web/src/app/shells/DesktopShell.tsx, apps/web/src/app/shells/ServerLayout.tsx
Rendered ConnectionBanner and apply disconnected opacity in MainContent; initialized connection subscriptions in MainLayout; replaced inline unreachable UI with ServerOfflinePage in DesktopShell and ServerLayout.
Sidebar & Session
apps/web/src/features/sidebar/ui/SidebarFooter.tsx, apps/web/src/features/session/api/session.queries.ts
Replaced BackendStatusIndicator with ConnectionOrb in sidebar footer; session send-error handler now calls emitSendAttemptFailed() when WS errors match “not connected” or “disconnected”.

Sequence Diagram

sequenceDiagram
    participant Client as User / UI
    participant WS as WebSocket Layer
    participant Hooks as Connection Hooks
    participant Store as Connection Store
    participant Session as Session API

    Client->>WS: network interruption / send message
    WS->>Session: send error ("not connected"/"disconnected")
    Session->>Hooks: emitSendAttemptFailed()
    Hooks->>Store: markSendAttemptFailed()
    Store->>Store: transition -> "disconnected" (escalation)
    Store->>Client: notify state changes
    Client->>Client: show ConnectionBanner / update ConnectionOrb

    Client->>Client: Click "Retry now"
    Client->>Store: retry()
    Store->>WS: forceReconnect() / notifyConnectionChange(false)
    WS->>Hooks: onConnectionChange(true)
    Hooks->>Store: onConnected()
    Store->>Client: hide banner, orb -> green
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A twitch, a hop, the banner blooms,
Timers tick through grace and glooms,
A muted dot, an amber sigh—
Click retry; we bound back high,
The orb turns green beneath blue rooms.


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

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

🧹 Nitpick comments (2)
apps/web/src/features/connection/hooks/useConnectionState.ts (1)

13-18: Extract granular hooks to prevent unnecessary re-renders of components that only need one field.

Line 13–18 bundles three independent fields (state, sendAttemptFailed, retry). ConnectionOrb and MainContent subscribe to all three but only use state, causing unnecessary re-renders when the other fields change. Extract individual hooks so each component subscribes only to what it needs.

♻️ Suggested refactor
 export { type ConnectionState };

-export function useConnectionState() {
-  const state = useConnectionStore((s) => s.state);
-  const sendAttemptFailed = useConnectionStore((s) => s.sendAttemptFailed);
-  const retry = useConnectionStore((s) => s.retry);
-
-  return { state, sendAttemptFailed, retry };
-}
+export const useConnectionState = () => useConnectionStore((s) => s.state);
+export const useConnectionSendAttemptFailed = () => useConnectionStore((s) => s.sendAttemptFailed);
+export const useConnectionRetry = () => useConnectionStore((s) => s.retry);

Then ConnectionBanner imports all three, while ConnectionOrb and MainContent import only useConnectionState.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/connection/hooks/useConnectionState.ts` around lines 13
- 18, The current useConnectionState hook selects three independent fields from
useConnectionStore which causes components that only need one field (like
ConnectionOrb and MainContent) to re-render when the others change; split this
into granular selectors: create three hooks — e.g. useConnectionState (returns
only s.state), useConnectionSendAttemptFailed (returns only s.sendAttemptFailed)
and useConnectionRetry (returns only s.retry) — each implemented with
useConnectionStore((s) => s.<field>); update ConnectionOrb and MainContent to
import only useConnectionState and update ConnectionBanner to import all three
hooks; keep existing names (useConnectionState, sendAttemptFailed, retry)
referenced in components to make locating changes simple and ensure returned
types/signatures remain consistent.
apps/web/src/features/connection/ui/ConnectionBanner.tsx (1)

52-55: Consider disabling ping animation for reduced motion.

The amber dot uses animate-ping which runs indefinitely. Users with prefers-reduced-motion may prefer this disabled.

♻️ Proposed fix
-              <span className="bg-accent-gold absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" />
+              {!reduceMotion && (
+                <span className="bg-accent-gold absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" />
+              )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/connection/ui/ConnectionBanner.tsx` around lines 52 -
55, Replace the unconditional Tailwind animation class on the amber ping with
motion-aware utilities so users with prefers-reduced-motion won't see it; in the
ConnectionBanner component update the element using "animate-ping" (the inner
span with className including animate-ping) to use "motion-safe:animate-ping
motion-reduce:animate-none" (or equivalent CSS respecting
prefers-reduced-motion) so the ping runs only when motion is allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/features/session/api/session.queries.ts`:
- Around line 362-365: The current detection of WS disconnects in the _err
handler (the block that calls emitSendAttemptFailed) is brittle because it only
checks message substrings; create or import a shared predicate named
isWsConnectivityError in the WS layer/util and use it here instead of substring
checks: replace the msg.includes(...) logic with a call to
isWsConnectivityError(_err) (or isWsConnectivityError(_err.message)) so errors
like "WebSocket connection failed" and other variants are consistently detected
and trigger emitSendAttemptFailed. Ensure the new predicate is exported from the
WS layer or shared utils and update imports in session.queries.ts where _err is
evaluated.

---

Nitpick comments:
In `@apps/web/src/features/connection/hooks/useConnectionState.ts`:
- Around line 13-18: The current useConnectionState hook selects three
independent fields from useConnectionStore which causes components that only
need one field (like ConnectionOrb and MainContent) to re-render when the others
change; split this into granular selectors: create three hooks — e.g.
useConnectionState (returns only s.state), useConnectionSendAttemptFailed
(returns only s.sendAttemptFailed) and useConnectionRetry (returns only s.retry)
— each implemented with useConnectionStore((s) => s.<field>); update
ConnectionOrb and MainContent to import only useConnectionState and update
ConnectionBanner to import all three hooks; keep existing names
(useConnectionState, sendAttemptFailed, retry) referenced in components to make
locating changes simple and ensure returned types/signatures remain consistent.

In `@apps/web/src/features/connection/ui/ConnectionBanner.tsx`:
- Around line 52-55: Replace the unconditional Tailwind animation class on the
amber ping with motion-aware utilities so users with prefers-reduced-motion
won't see it; in the ConnectionBanner component update the element using
"animate-ping" (the inner span with className including animate-ping) to use
"motion-safe:animate-ping motion-reduce:animate-none" (or equivalent CSS
respecting prefers-reduced-motion) so the ping runs only when motion is allowed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Lite

Run ID: edc84863-2adf-4c9e-866f-503f50baca71

📥 Commits

Reviewing files that changed from the base of the PR and between 28f6507 and f92f18d.

📒 Files selected for processing (16)
  • .claude/agent-memory/code-reviewer/MEMORY.md
  • apps/web/src/app/layouts/MainContent.tsx
  • apps/web/src/app/layouts/MainLayout.tsx
  • apps/web/src/app/shells/DesktopShell.tsx
  • apps/web/src/app/shells/ServerLayout.tsx
  • apps/web/src/features/connection/hooks/useConnectionState.ts
  • apps/web/src/features/connection/hooks/useConnectionStateInit.ts
  • apps/web/src/features/connection/index.ts
  • apps/web/src/features/connection/lib/connectionEvents.ts
  • apps/web/src/features/connection/store/connectionStore.ts
  • apps/web/src/features/connection/ui/ConnectionBanner.tsx
  • apps/web/src/features/connection/ui/ConnectionIllustration.tsx
  • apps/web/src/features/connection/ui/ConnectionOrb.tsx
  • apps/web/src/features/connection/ui/ServerOfflinePage.tsx
  • apps/web/src/features/session/api/session.queries.ts
  • apps/web/src/features/sidebar/ui/SidebarFooter.tsx

Comment on lines +362 to +365
if (_err instanceof Error) {
const msg = _err.message.toLowerCase();
if (msg.includes("not connected") || msg.includes("disconnected")) {
emitSendAttemptFailed();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current WS-layer error wording used by connect/send paths.
# Expected: enumerate all thrown/rejected connectivity error strings so we can
# confirm whether current substring checks are comprehensive.

set -euo pipefail

echo "== Candidate WS implementation files =="
fd 'ws.*\.ts$' apps/web/src

echo
echo "== Connectivity-related error sites =="
fd 'ws.*\.ts$' apps/web/src -x rg -n -C2 'throw new Error|Error\(|not connected|disconnected|socket|closed'

echo
echo "== sendCommand/connect usage context =="
rg -n -C2 '\bsendCommand\s*\(|\bconnect\s*\(' apps/web/src/features/session/api/session.queries.ts apps/web/src/platform

Repository: zvadaadam/box-ide

Length of output: 727


🏁 Script executed:

# Search for WebSocket client setup and actual implementation
echo "=== Searching for WebSocket client initialization ==="
rg -n "new WebSocket|ws\(" apps/web/src --type ts --type tsx -B2 -A2

echo
echo "=== Look at the full context of lines 359-375 in session.queries.ts ==="
sed -n '350,380p' apps/web/src/features/session/api/session.queries.ts

echo
echo "=== Search for error handling patterns around WebSocket errors ==="
rg -n "instanceof Error|Error\(|disconnect|connectivity" apps/web/src/features/session/api/session.queries.ts -B1 -A1

echo
echo "=== Find where WebSocket is imported/used in the session module ==="
rg -n "import.*[Ww]eb[Ss]ocket|WebSocket|ws\b" apps/web/src/features/session --type ts --type tsx

Repository: zvadaadam/box-ide

Length of output: 2168


🏁 Script executed:

# Search without problematic type flags
echo "=== WebSocket imports and setup ==="
rg -n "WebSocket|ws\(" apps/web/src/features/session -B2 -A2

echo
echo "=== Check useWsToolRequest hook mentioned in earlier search ==="
head -100 apps/web/src/shared/hooks/useWsToolRequest.ts

echo
echo "=== Search broadly for error message generation ==="
rg -n '"WebSocket|disconnected|not connected' apps/web/src

echo
echo "=== Check the full session.queries.ts file structure and imports ==="
head -50 apps/web/src/features/session/api/session.queries.ts

Repository: zvadaadam/box-ide

Length of output: 13107


🏁 Script executed:

# Double-check: verify "WebSocket connection failed" is thrown in connectivity context
sed -n '620,635p' apps/web/src/platform/ws/query-protocol-client.ts

Repository: zvadaadam/box-ide

Length of output: 519


Harden disconnect detection beyond message substrings.

Lines 364–365 rely on substring matching which misses error variants. The WS client throws "WebSocket connection failed" on failed connect attempts, but the current check only catches "not connected" or "disconnected"—causing missed state escalation.

💡 Suggested direction
-      if (_err instanceof Error) {
-        const msg = _err.message.toLowerCase();
-        if (msg.includes("not connected") || msg.includes("disconnected")) {
-          emitSendAttemptFailed();
-        }
-      }
+      if (isWsConnectivityError(_err)) {
+        emitSendAttemptFailed();
+      }

Define isWsConnectivityError once in the WS layer (or a shared util) and reuse it here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/session/api/session.queries.ts` around lines 362 - 365,
The current detection of WS disconnects in the _err handler (the block that
calls emitSendAttemptFailed) is brittle because it only checks message
substrings; create or import a shared predicate named isWsConnectivityError in
the WS layer/util and use it here instead of substring checks: replace the
msg.includes(...) logic with a call to isWsConnectivityError(_err) (or
isWsConnectivityError(_err.message)) so errors like "WebSocket connection
failed" and other variants are consistently detected and trigger
emitSendAttemptFailed. Ensure the new predicate is exported from the WS layer or
shared utils and update imports in session.queries.ts where _err is evaluated.

@zvadaadam

Copy link
Copy Markdown
Owner Author

Re: CodeRabbit Review

Finding #1 (walkthrough/summary) — Informational, no action needed.

Finding #2 (isWsConnectivityError predicate) — Not actionable.

The claim is that "WebSocket connection failed" is missed by the substring check in session.queries.ts:364. However, that error is unreachable in this code path:

  • "WebSocket connection failed" is thrown by connect() (initial connection attempt, line 628 of query-protocol-client.ts)
  • useSendMessage calls sendCommand(), not connect()
  • sendCommand() can only produce these connectivity errors:
    1. "WebSocket not connected" → caught by "not connected"
    2. "WebSocket disconnected" → caught by "disconnected"
    3. "Server disconnected — reconnecting" (relay mode) → caught by "disconnected"
    4. "Command {command} timed out" → timeout, not connectivity (correct to skip) ✅

Similarly, "Relay auth failed" is thrown during WS handshake, not during sendCommand. If relay auth fails, pending commands reject with "WebSocket disconnected" — which IS caught.

Creating a shared isWsConnectivityError predicate would be premature abstraction for a single call site.

No changes needed.

Prettier formatting for 4 connection UI components and remove
unused `capabilities` import from DesktopShell.tsx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e27357a10bbf

@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

🧹 Nitpick comments (1)
apps/web/src/features/connection/ui/ConnectionBanner.tsx (1)

25-26: Set mode="wait" on all AnimatePresence wrappers in this TSX file.

The copy block already uses mode="wait", but the outer banner and retry-button presence wrappers do not.

Suggested patch
-    <AnimatePresence>
+    <AnimatePresence mode="wait">
@@
-            <AnimatePresence>
+            <AnimatePresence mode="wait">

Based on learnings: "Applies to src/**/*.{tsx} : Wrap conditional renders with AnimatePresence when exit animations are needed... Always set mode='wait' to prevent layout jumps".

Also applies to: 87-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/connection/ui/ConnectionBanner.tsx` around lines 25 -
26, The file has multiple AnimatePresence wrappers (the outer banner wrapper
around the showBanner conditional and the retry-button presence wrapper) that
are missing mode="wait", which can cause layout jumps; update every
AnimatePresence instance in ConnectionBanner.tsx (including the one that wraps
the showBanner conditional and the one that controls the retry button
presence—note the copy block already uses mode="wait") to include mode="wait" so
exit animations complete before new enters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/features/connection/ui/ConnectionBanner.tsx`:
- Around line 48-50: The pulsing dot and inner fade animations in
ConnectionBanner still run under reduced-motion; update the component
(ConnectionBanner) to read the same useReducedMotion() flag used for the
container and disable/short-circuit those animations when reducedMotion is true:
remove or omit the "animate-ping" class on the outer span and the
transition/animate props or motion wrappers on the inner fade elements when
reducedMotion is enabled, and instead render their static non-animated
equivalents so the visual remains but no continuous or secondary animations run;
apply this for the spans that contain "animate-ping" and any motion/transitions
referenced around the same blocks (also adjust the similar blocks at the other
indicated ranges).
- Around line 21-23: Replace the ad-hoc boolean checks for connection state in
ConnectionBanner with a ts-pattern match against the `state` value to get
compile-time exhaustiveness: import `match` from 'ts-pattern', compute
`showBanner` and `isEscalated` (and any other derived values used in the render
block between the existing `state` checks around lines 55–83) via
`match(state).with(...).exhaustive()` handling `"connected"`, `"grace_period"`,
`"reconnecting"`, and `"disconnected"` explicitly, and update the render logic
to consume those matched results; ensure you call `.exhaustive()` so the
compiler will force you to handle future states.

---

Nitpick comments:
In `@apps/web/src/features/connection/ui/ConnectionBanner.tsx`:
- Around line 25-26: The file has multiple AnimatePresence wrappers (the outer
banner wrapper around the showBanner conditional and the retry-button presence
wrapper) that are missing mode="wait", which can cause layout jumps; update
every AnimatePresence instance in ConnectionBanner.tsx (including the one that
wraps the showBanner conditional and the one that controls the retry button
presence—note the copy block already uses mode="wait") to include mode="wait" so
exit animations complete before new enters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Lite

Run ID: f564c866-810a-4487-890e-9708f4924577

📥 Commits

Reviewing files that changed from the base of the PR and between f92f18d and 90fd1d0.

📒 Files selected for processing (5)
  • apps/web/src/app/shells/DesktopShell.tsx
  • apps/web/src/features/connection/ui/ConnectionBanner.tsx
  • apps/web/src/features/connection/ui/ConnectionIllustration.tsx
  • apps/web/src/features/connection/ui/ConnectionOrb.tsx
  • apps/web/src/features/connection/ui/ServerOfflinePage.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/features/connection/ui/ServerOfflinePage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/features/connection/ui/ConnectionIllustration.tsx

Comment on lines +21 to +23
const showBanner = state === "reconnecting" || state === "disconnected";
const isEscalated = state === "disconnected";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the ConnectionBanner.tsx file
find . -name "ConnectionBanner.tsx" -type f

Repository: zvadaadam/box-ide

Length of output: 119


🏁 Script executed:

# Check the current content of the file
cat -n apps/web/src/features/connection/ui/ConnectionBanner.tsx

Repository: zvadaadam/box-ide

Length of output: 4950


🏁 Script executed:

# Find the connection state hook to verify the type
find . -name "useConnectionState*" -o -name "*connection*hook*" | grep -E "\.ts$|\.tsx$"

Repository: zvadaadam/box-ide

Length of output: 190


🏁 Script executed:

cat -n apps/web/src/features/connection/hooks/useConnectionState.ts

Repository: zvadaadam/box-ide

Length of output: 816


🏁 Script executed:

find . -name "connectionStore.ts" -o -name "connectionStore.tsx" | head -5

Repository: zvadaadam/box-ide

Length of output: 120


🏁 Script executed:

cat -n apps/web/src/features/connection/store/connectionStore.ts

Repository: zvadaadam/box-ide

Length of output: 3714


🏁 Script executed:

# Check if ts-pattern is available in the project
grep -r "ts-pattern" package.json bun.lock 2>/dev/null | head -20

Repository: zvadaadam/box-ide

Length of output: 298


Convert state branching to ts-pattern with .exhaustive() for compile-time state exhaustiveness.

The current boolean/ternary approach misses opportunities for compile-time safety. Using ts-pattern ensures all connection states ("connected", "grace_period", "reconnecting", "disconnected") are handled explicitly, preventing bugs when new states are added.

Suggested patch
+import { match } from "ts-pattern";
 import { AnimatePresence, m, useReducedMotion } from "framer-motion";
 import { useConnectionState } from "../hooks/useConnectionState";
@@
-  const showBanner = state === "reconnecting" || state === "disconnected";
-  const isEscalated = state === "disconnected";
+  const bannerState = match(state)
+    .with("connected", () => null)
+    .with("grace_period", () => null)
+    .with("reconnecting", () => ({ isEscalated: false }))
+    .with("disconnected", () => ({ isEscalated: true }))
+    .exhaustive();
+
+  if (!bannerState) return null;
+  const { isEscalated } = bannerState;
@@
-      {showBanner && (
-        <m.div
+      <m.div
           key="connection-banner"
@@
-        </m.div>
-      )}
+        </m.div>
     </AnimatePresence>

Applies to lines 21–23 and 55–83.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/connection/ui/ConnectionBanner.tsx` around lines 21 -
23, Replace the ad-hoc boolean checks for connection state in ConnectionBanner
with a ts-pattern match against the `state` value to get compile-time
exhaustiveness: import `match` from 'ts-pattern', compute `showBanner` and
`isEscalated` (and any other derived values used in the render block between the
existing `state` checks around lines 55–83) via
`match(state).with(...).exhaustive()` handling `"connected"`, `"grace_period"`,
`"reconnecting"`, and `"disconnected"` explicitly, and update the render logic
to consume those matched results; ensure you call `.exhaustive()` so the
compiler will force you to handle future states.

Comment thread apps/web/src/features/connection/ui/ConnectionBanner.tsx
zvadaadam and others added 3 commits March 23, 2026 00:25
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 78fb175e4e65
- retry() now resets store to grace_period before forceReconnect()
  so user gets immediate "Reconnecting..." feedback instead of the
  banner staying stuck on "Connection lost" (forceReconnect skips
  notifyConnectionChange when ws is null after 30s+)
- Add "connection failed" to WS error substring matching in
  useSendMessage.onError — catches connect() rejections that produce
  "WebSocket connection failed"
- Replace built-in "easeInOut" with "linear" on WaitingDots per
  CLAUDE.md animation guidelines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4d09e1ce83e2
Suppress animate-ping on the amber dot and set inner fade
transition durations to 0 when useReducedMotion() is true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 9a14372914a6
@zvadaadam

Copy link
Copy Markdown
Owner Author

Re: CodeRabbit Review (Round 2)

Comment #1 — Correcting my earlier analysis

My earlier comment incorrectly stated that "WebSocket connection failed" is unreachable. It IS reachable — useSendMessage calls await connect() on line 267 (if (!isConnected()) await connect()), and connect() can reject with "WebSocket connection failed". Fixed in db9311b — added "connection failed" to the substring matching.

Comment #2 (ts-pattern in ConnectionBanner) — Not actionable.

The current code derives 2 boolean flags from state:

const showBanner = state === "reconnecting" || state === "disconnected";
const isEscalated = state === "disconnected";

This isn't a switch/case or if/else chain — it's 2 assignments. The suggested ts-pattern version returns null | { isEscalated: boolean } which adds complexity for no real gain. ts-pattern is great for mapping state → single value (like ConnectionOrb's aria-label with .exhaustive()), not for deriving independent boolean flags. If a 5th state is added, these 2 lines read clearly enough that the developer will know whether it should show/escalate.

Comment #3 (reduced-motion inner animations) — Fixed in 0cc9b2a.

  • animate-ping on the amber dot now conditionally renders only when !reduceMotion
  • Inner fade transitions (copy cross-fade + retry button) use duration: reduceMotion ? 0 : 0.15 / 0.2

@zvadaadam zvadaadam merged commit 2822148 into main Mar 22, 2026
4 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.

1 participant