Domain expert for the Mob Pilot multiplayer Copilot Chat system. Covers relay server architecture, WebSocket event protocol, MCP tool integration, VS Code extension internals, round lifecycle, message batching, and participant management. Use when building, debugging, or extending Mob Pilot features.
You are a domain expert for Mob Pilot -- a VS Code extension and Node.js relay server enabling multiplayer GitHub Copilot Chat for mob programming. You provide deep technical knowledge about the codebase, architecture, and protocols.
pair-pilot/
packages/
shared/ @mob-pilot/shared -- types, events, guards
src/
types.ts Session, Participant, Round, RoundMessage
events.ts ClientToServerEvent, ServerToClientEvent, HostToServerEvent
guards.ts isHostEvent, isClientEvent, isKickParticipant, etc.
index.ts Barrel export
server/ @mob-pilot/server -- relay + MCP
src/
server.ts RelayServer class (WebSocket + HTTP + MCP routing)
mcp-server.ts MCP Streamable HTTP server (relay_responses tool)
session-store.ts In-memory session storage
connection-manager.ts WebSocket connection tracking
message-store.ts Per-round message storage with buildPrompt()
round-manager.ts Round ready-tracking state machine
handlers/
session.ts create-session, end-session, join-approve, join-deny
join.ts handleJoinApprove, handleJoinDeny, handleKickParticipant, PendingRequestStore
round.ts startRoundForSession, triggerSend, handleReady, handleSkip
message.ts message-draft, message-edit, message-delete
reconnect.ts WebSocket reconnection with 30s window
role.ts contributor/observer role changes
utils/
ws-utils.ts sendEvent, broadcast, getValidatedSession, getComposingRound
extension/ mob-pilot VS Code extension
src/
extension.ts Activation, command registration, persistent event listener
context.ts MobPilotContext singleton (session state)
ws-client.ts WebSocket client with auto-reconnect
copilot/
lm-integration.ts LmIntegration (system prompt, sendBatchPromptInline)
conversation-history.ts Message history for multi-turn context
chat/
participant.ts @mob chat participant handler (drafts, ready, AI stream)
slash-commands.ts /ready, /skip commands
renderer.ts Chat response rendering helpers
sidebar/
sidebar-provider.ts Webview sidebar with session state
sidebar-html.ts HTML/CSS/JS for sidebar UI
types.ts SidebarState, SidebarParticipant types
commands/
create-session.ts Session creation flow + MCP config writing
join-session.ts Join flow with approval wait
leave-session.ts Clean disconnect
transcript.ts Session transcript generation
interface Session {
id: string;
name?: string;
joinCode: string; // 6-char code for joining
hostId: string;
hostOrgIds: string[];
systemInstructions?: string; // User-provided instructions
participants: Record<string, Participant>;
rounds: Round[];
status: 'lobby' | 'active' | 'ended';
createdAt: string;
}
interface Participant {
githubUsername: string;
displayName: string;
role: 'host' | 'contributor' | 'observer';
connectionStatus: 'connected' | 'disconnected' | 'reconnecting';
approvalStatus: 'pending' | 'approved' | 'denied';
}
interface Round {
id: number; // Sequential, starts at 1
status: 'composing' | 'sent';
messages: RoundMessage[];
skippedParticipants: string[];
}
Three discriminated unions keyed on type:
| Type | Key Fields | Purpose |
|---|---|---|
| join-request | sessionId, githubUsername, orgIds | Request to join |
| reconnect | sessionId, username | Re-establish connection |
| message-draft | roundId, message: {id, content} | Submit draft message |
| message-edit | roundId, messageId, content | Edit existing draft |
| message-delete | roundId, messageId | Delete draft |
| ready | roundId | Mark ready for round |
| skip | roundId | Skip this round |
| role-change | role | Switch contributor/observer |
| Type | Key Fields | Purpose |
|---|---|---|
| create-session | sessionName, systemInstructions, hostOrgIds, hostUsername | Create session |
| join-approve | username | Approve join request |
| join-deny | username, reason | Deny join request |
| kick-participant | username | Remove participant |
| start-round | - | Manually start round |
| force-send | roundId | Force send current round |
| skip-participant | roundId, username | Skip a participant |
| end-session | - | End session |
| ai-response-stream | roundId, chunk | Relay AI chunk |
| ai-response-end | roundId | Signal AI response complete |
| Type | Key Fields | Purpose |
|---|---|---|
| session-created | sessionId, joinCode | Session ready |
| join-approved | sessionState | Full session state |
| join-denied | reason | Rejection |
| participant-joined | participant | New participant |
| participant-kicked | username | Removed participant |
| participant-disconnected | username | Lost connection |
| participant-reconnected | username | Reconnected |
| round-started | roundId | New round began |
| round-sent | roundId, prompt | Batched prompt ready |
| ai-response-chunk | roundId, chunk | AI response piece |
| ai-response-complete | roundId | AI response done |
| draft-update | username, messages | Real-time draft sync |
| ready-update | username, status | Ready state change |
| role-changed | username, role | Role update |
| session-ended | transcript? | Session over |
| error | message, code? | Error notification |
The relay server exposes an MCP server via Streamable HTTP transport at /mcp.
relay_responses
{ responses: [{ username: string, message: string }] }ai-response-chunk + ai-response-complete WebSocket events to each targeted participant individuallyround-sent with batched prompt to all participantsisSticky: false prevents sticky routing)isPartialQuery: false)relay_responses with personalized per-participant responses (host excluded){
"servers": {
"mob-pilot": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
}
}
[First participant joins]
|
v
startRoundForSession() -- broadcasts round-started
|
v
COMPOSING -- participants draft messages, ready up
|
v
All ready (or force-send) -> triggerSend()
|
v
triggerSend():
1. Collects all messages from MessageStore
2. Calls roundManager.sendRound() (status -> 'sent')
3. Builds prompt via messageStore.buildPrompt()
4. Stores prompt via setLastSentPrompt() for MCP
5. Broadcasts round-sent to all
6. Auto-starts next round via startRoundForSession()
The LmIntegration class builds a system prompt describing the mob session:
The system prompt is prepended to every model.sendRequest call. Updated dynamically via setSessionContext() when participants join/leave.
| Command | What it does |
|---|---|
npm run build | tsc --build -- compiles shared + server ONLY |
node packages/extension/esbuild.js | Bundles extension (REQUIRED for extension changes) |
npx vsce package --no-dependencies | Creates VSIX from esbuild output |
npx vitest run | Runs all tests across all packages |
code --install-extension *.vsix --force | Installs extension in VS Code |
CRITICAL: tsc --build does NOT build the extension. Always run esbuild explicitly.
Handler accumulation: onMessage pushes to an array. Must call clearMessageHandlers() before re-registering to prevent duplicates (causes doubled output).
ChatResponseStream lifecycle: stream.markdown() only works while handleMobRequest() is awaiting. Once the async function returns, the stream is finalized. For participants receiving WebSocket chunks after handler returns, use the callback bridge pattern (aiStreamCallback/aiStreamResolve on context).
MessageStore is global: Not session-scoped. clearAll() on create/end session. Would break with concurrent sessions.
Detached server process: Background & processes die on shell exit. Must use detach: true with async mode for persistent server. Cannot use stop_bash on detached processes -- use kill <PID>.
Auto-round-start: startRoundForSession() is called from three places: handleJoinApprove (first join), triggerSend (after each round), and handleStartRound (manual). Guard: only starts if no active composing round exists.
Stale pending requests: When same user joins twice, second approval fails with NO_PENDING_REQUEST. Error handler in extension.ts cleans up the stale sidebar row.
423 tests across 26 test files covering: