Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/instructions/code-review.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
applyTo: '**'
excludeAgent: 'coding-agent'
---

# Architecture at a glance

- **P2P-first**: all collaborative content (code, chat, whiteboard) syncs over WebRTC data channels using Yjs CRDTs. The server is signalling-only.
- **Signalling server** (`server/`): Socket.IO — room membership, host election, and WebRTC offer/answer/ICE relay. Stores no user content.
- **Client** (`src/`): React + Vite SPA. Core layers: `signalling.ts` → `webrtc.ts` → `yjs-provider.ts` → `session.tsx` (context) → components.
- **Persistence**: `y-indexeddb` gives offline/rejoin state on the client; no server-side persistence.

# Code review-only baseline

- Prioritise correctness and regressions in real-time collaboration flows (session join/leave, peer lifecycle, Yjs sync, chat/whiteboard updates).
- Flag changes that weaken privacy assumptions or route shared content through the server.
- Flag breaking changes to signalling events, room membership logic, or host election behavior.
- Be explicit about risk level and impacted user flows in review comments.

# Client ↔ Server contract

- Socket.IO event names and payload shapes (see `SignallingEvents` in `signalling.ts` and the server's `io.on` handlers) are a shared contract — changes must land on both sides simultaneously.
- If either side adds/renames/removes an event, flag it and verify the counterpart is updated.

# Validation expectations

- If root/client files change, run:
- `npm run lint`
- `npm run build`
- If `server/` files change, run:
- `npm --prefix server run lint`
- `npm --prefix server run build`
- If both areas change, run all four commands.
- If `docker-compose.yml`, `Dockerfile.*`, or `nginx.conf` change, verify the build still works with `docker compose build`.
- In review summaries, state which commands were run and any skipped checks.
29 changes: 29 additions & 0 deletions .github/instructions/frontend.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
applyTo: 'src/**/*.ts,src/**/*.tsx,public/**,index.html,vite.config.ts,tailwind.config.js,eslint.config.js'
excludeAgent: 'coding-agent'
---

# Frontend review focus

- Protect collaborative UX behavior across editor, chat, and whiteboard flows.
- Check React hooks for stale closures, missing cleanup, and unnecessary re-renders.
- Watch for Yjs synchronization regressions and awareness-state drift.
- Ensure user-facing errors are actionable and do not silently fail.
- Keep bundles lean; challenge unnecessary new dependencies.

# Yjs & persistence pitfalls

- Changes to Yjs shared-type names (e.g., `doc.getText('...')`, `doc.getMap('...')`) break compatibility with data already stored in IndexedDB — flag and require a migration strategy or version bump.
- `origin === 'remote'` guards in Yjs update listeners prevent echo loops — removing or altering these is high-risk.
- Awareness state cleanup must happen on unmount/disconnect; leaked awareness entries cause ghost cursors.

# Environment & config

- `src/lib/config.ts` reads `VITE_*` env vars at build time. Changes here affect all deployment targets — note which vars are new/removed.
- STUN/TURN server changes can silently break connectivity for users behind restrictive NATs.

# Frontend validation

- Run `npm run lint`.
- Run `npm run build`.
- For runtime-sensitive changes (WebRTC/session/sync), describe manual smoke checks in PR comments.
23 changes: 23 additions & 0 deletions .github/instructions/server.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
applyTo: 'server/**/*.ts,server/**/*.js,server/package.json,server/tsconfig.json,server/eslint.config.js'
excludeAgent: 'coding-agent'
---

# Server review focus

- Treat the signalling service as stateful and long-lived; flag stateless/serverless assumptions.
- Protect Socket.IO room membership, host election, and signalling message integrity.
- Verify CORS and origin-handling changes do not broaden access unintentionally.
- Ensure server changes never store or process collaborative content beyond signalling metadata.

# Host election & room lifecycle

- Host = peer with the earliest `joinedAt` timestamp. Changes to `getHostId()` or `joinedAt` assignment affect who is authoritative — test with multi-peer join/leave sequences.
- The duplicate-peerId kick path (`kicked` event) is a reconnection safeguard — removing or loosening it can cause split-brain state.
- Room cleanup runs on an interval (`ROOM_CLEANUP_INTERVAL`); changes to timing or conditions should consider rooms that are briefly empty during reconnects.

# Server validation

- Run `npm --prefix server run lint`.
- Run `npm --prefix server run build`.
- If server contract changes affect client behavior, ask for coordinated client validation notes.
28 changes: 14 additions & 14 deletions src/components/SessionLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useSession } from '../lib/useSession';
import { useTheme } from '../lib/useTheme';
import { type ConnectionStatus } from '../lib/session';
import { CodeEditor } from './CodeEditor';
import { Whiteboard } from './Whiteboard';
import { Whiteboard } from './whiteboard';
import { Participants } from './Participants';
import { Chat } from './Chat';
import {
Expand All @@ -31,7 +31,7 @@ import {
} from './ui/dialog';
import { Switch } from './ui/switch';

type Tab = 'code' | 'diagram';
type Tab = 'code' | 'whiteboard';

// Sidebar resize constraints
const MIN_SIDEBAR_WIDTH = 280;
Expand All @@ -55,7 +55,7 @@ export function SessionLayout({ status, onCopyLink }: SessionLayoutProps) {
const [activeTab, setActiveTab] = useState<Tab>(() => {
if (typeof window === 'undefined') return 'code';
const stored = sessionStorage.getItem(ACTIVE_TAB_STORAGE_KEY);
return stored === 'code' || stored === 'diagram' ? stored : 'code';
return stored === 'code' || stored === 'whiteboard' ? stored : 'code';
});
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
Expand Down Expand Up @@ -241,7 +241,7 @@ export function SessionLayout({ status, onCopyLink }: SessionLayoutProps) {
<div className="hidden md:flex flex-1 justify-center min-w-0">
<div className="flex bg-panel-2 rounded-lg p-1">
<button
className={`px-6 py-2 text-sm font-medium rounded-md transition-all
className={`px-12 py-2 text-sm font-medium rounded-md transition-all
${
activeTab === 'code'
? 'bg-primary text-white shadow-sm'
Expand All @@ -250,19 +250,19 @@ export function SessionLayout({ status, onCopyLink }: SessionLayoutProps) {
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-panel-2`}
onClick={() => setActiveTab('code')}
>
Code
CODE
</button>
<button
className={`px-6 py-2 text-sm font-medium rounded-md transition-all
${
activeTab === 'diagram'
activeTab === 'whiteboard'
? 'bg-primary text-white shadow-sm'
: 'text-text-muted hover:text-text'
}
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-panel-2`}
onClick={() => setActiveTab('diagram')}
onClick={() => setActiveTab('whiteboard')}
>
Diagram
WHITEBOARD
</button>
</div>
</div>
Expand Down Expand Up @@ -401,7 +401,7 @@ export function SessionLayout({ status, onCopyLink }: SessionLayoutProps) {
<div className="flex md:hidden justify-center px-2 pb-2">
<div className="flex bg-panel-2 rounded-lg p-0.5">
<button
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-all
className={`px-12 py-1.5 text-xs font-medium rounded-md transition-all
${
activeTab === 'code'
? 'bg-primary text-white shadow-sm'
Expand All @@ -410,19 +410,19 @@ export function SessionLayout({ status, onCopyLink }: SessionLayoutProps) {
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary`}
onClick={() => setActiveTab('code')}
>
Code
CODE
</button>
<button
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-all
${
activeTab === 'diagram'
activeTab === 'whiteboard'
? 'bg-primary text-white shadow-sm'
: 'text-text-muted hover:text-text'
}
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary`}
onClick={() => setActiveTab('diagram')}
onClick={() => setActiveTab('whiteboard')}
>
Diagram
WHITEBOARD
</button>
</div>
</div>
Expand All @@ -447,7 +447,7 @@ export function SessionLayout({ status, onCopyLink }: SessionLayoutProps) {
</div>
<div
className={
activeTab === 'diagram'
activeTab === 'whiteboard'
? 'flex-1 flex min-w-0 overflow-hidden'
: 'hidden'
}
Expand Down
Loading