Live Share - Intranet — VSCode Extension
Project Overview
Decentralized real-time collaborative editing VSCode extension. No external servers required — the Host acts as both the WebSocket server and the session coordinator. All data stays on the intranet.
Architecture
Host (WebSocket Server + Editor) ←→ Guest (WebSocket Client + Editor)
↕
WebSocket on IP:PORT
- Host: Starts a
ws.Server, broadcasts messages, relays between guests
- Guest: Connects via
ws://HOST:PORT, sends/receives messages
- Protocol: Unified JSON message envelope (
Message type in src/protocol)
Build & Run
npm install # Install dependencies
npm run compile # Build for production
npm run watch # Dev mode with watch
Project Structure
src/
├── extension.ts # Entry point: commands, status bar, session lifecycle, collab + FS + terminal + debug wiring
├── websocket/
│ ├── wsserver.ts # Host-side WebSocket server (HostServer class)
│ └── wsclient.ts # Guest-side WebSocket client (GuestClient class)
├── protocol/
│ └── index.ts # Message types, payload definitions, helpers, base64 utils
├── collab/
│ ├── doc-manager.ts # Yjs document lifecycle per file, on-demand disk init, binding creation
│ ├── editor-binding.ts # Two-way sync: VSCode TextEditor ↔ Yjs Y.Text (supports rebind)
│ └── awareness.ts # Cursor/selection sync, remote cursor decorations, trackEditor()
├── filesystem/
│ ├── provider.ts # lsintranet:// FileSystemProvider (Guest side), reads from Yjs docs
│ └── sync.ts # File change watcher + tree builder (Host side), updates Yjs on disk change
├── terminal/
│ ├── host.ts # Host terminal manager (node-pty), spawns shells, bridges I/O
│ └── guest.ts # Guest Pseudoterminal, renders remote terminal output
├── debug/
│ ├── proxy.ts # DAP message proxy (Guest-side DebugAdapter implementation)
│ └── path-mapper.ts # Bidirectional path mapping (lsintranet:// ↔ host FS)
├── follow/
│ └── tracker.ts # Follow mode: editor focus + cursor sync, auto-stop on local edit
├── chat/
│ └── handler.ts # Chat panel (Webview), message send/receive via WebSocket
└── agent/
└── handler.ts # Shared agent prompt/response panel (Webview)
Development Phases
- [x] Step 1: Project scaffold & configuration
- [x] Step 2: Communication layer (WebSocket Host/Guest + protocol)
- [x] Step 3: Real-time collaborative editing (Yjs + cursor sync)
- [x] Step 4: File tree sync (FileSystemProvider)
- [x] Step 5: Terminal sharing (node-pty + Pseudoterminal)
- [x] Step 6: Debug session sharing (DAP proxy)
- [x] Step 7: Follow mode
- [x] Step 8: Chat
- [x] Step 9: UI polish & packaging
- [x] Step 10: Unify file content sync through Yjs channel (Phase 2)
Key APIs Used
vscode.StatusBarItem — Session status display
vscode.commands.registerCommand — Command palette integration
vscode.workspace.onDidChangeTextDocument — Detect local edits → push to Yjs
vscode.workspace.applyEdit — Apply remote Yjs edits to local document
vscode.window.createTextEditorDecorationType — Remote cursor rendering
ws.Server / ws.WebSocket — WebSocket communication
Yjs (yjs) — CRDT engine for conflict-free collaborative editing
vscode.FileSystemProvider — Virtual file system for lsintranet:// scheme
vscode.workspace.registerFileSystemProvider — Register custom scheme
vscode.FileSystemWatcher — Watch Host workspace for file changes
node-pty — Spawn pseudo-terminal processes on Host
vscode.Pseudoterminal — Render remote terminal output on Guest
vscode.debug.registerDebugAdapterDescriptorFactory — Register custom DAP proxy
vscode.DebugAdapter — Guest-side DAP proxy implementation
vscode.window.onDidChangeActiveTextEditor — Follow mode: detect editor focus change
vscode.window.onDidChangeTextEditorSelection — Follow mode: detect cursor/selection change
vscode.window.createWebviewPanel — Chat panel UI
Collab Data Flow
Local edit → onDidChangeTextDocument → Yjs Y.Text → doc.on('update')
→ WebSocket (yjs-update) → Remote side → Y.applyUpdate → Ytext.observe
→ workspace.applyEdit → VSCode document updated
Remote cursor → yjs-awareness message → AwarenessManager.updateRemote()
→ editor.setDecorations() → Colored cursor + name label rendered
Initial sync (Guest joins):
Host → onGuestJoin → buildFullWorkspaceInit() → yjs-init (ALL files, not just open ones)
Guest → initFromRemote() → Y.applyUpdate for each file → content available via getDocContent()
File System Data Flow (Phase 2: Unified Yjs Channel)
Host side:
FileSyncManager watches workspace with FileSystemWatcher
→ onDidCreate/Change/Delete (file) → docManager.updateDocFromDisk() → Yjs Doc updated
→ Yjs automatically syncs delta to all Guests via yjs-update
→ onDidCreate/Delete (directory) → broadcast file-tree-change (structural only)
→ Guest joins → buildFullWorkspaceInit() → yjs-init (all files) + file-tree
Guest requests a file not yet synced:
Guest → yjs-req (filePath) → Host
Host → docManager.createDocFromDisk() → yjs-init (single file) → Guest
Guest side:
Live Share - Intranet FS Provider registered as lsintranet:// scheme
→ file-tree message → buildTree() → populate virtual FS cache
→ file-tree-change message → applyTreeChange() (structural: create/delete/rename)
→ readFile(uri) → getDocContent(filePath) from CollabDocManager → returns Yjs doc text
→ if doc not found, send yjs-req → Host creates doc → yjs-init arrives → content available
→ URI mapping: lsintranet:///path/to/file.py ↔ /path/to/file.py
Key change from Phase 1:
REMOVED: file-content-req, file-content-res, file-change (base64 broadcast)
ADDED: yjs-req, file-tree-change
ALL file content now flows through Yjs — same channel as collaborative edits
Disk changes on Host update Yjs Doc → Yjs handles incremental sync automatically
Terminal Data Flow
Host side:
HostTerminalManager creates PTY process via node-pty
→ pty.onData(data) → base64-encode → broadcast terminal-data to all Guests
→ receive terminal-data from Guest → pty.write(base64-decoded)
→ pty resize → broadcast terminal-resize
→ Host also opens a local mirror terminal for the same PTY so Host and Guests share one live session
→ Guest joins → send terminal-create for each active terminal
Guest side:
Receive terminal-create → vscode.window.createTerminal({ pty: Live Share - Intranet Pseudoterminal })
→ PseudoTerminal.onDidWrite → writeEmitter.fire(decoded data)
→ User input → send terminal-data to Host → pty.write()
→ Resize → send terminal-resize to Host → pty.resize()
Terminal ID mapping:
Each terminal has a unique string ID (uuid)
Host maintains Map<terminalId, IPty>
Guest maintains Map<terminalId, Live Share - Intranet Pseudoterminal>
Debug Data Flow
Host is the debug authority:
Host starts a normal VS Code debug session
→ onDidStartDebugSession captures the active session and broadcasts dap-event
→ Host DebugAdapterTracker captures real adapter events (output/stopped/continued/terminated)
→ Guests proxy DAP requests to Host via dap-request
→ Host forwards to the active Debug Adapter via customRequest()
→ Host relays dap-response / dap-event back to Guests
→ Guest auto-starts an Live Share - Intranet mirror debug session to render Host events in the Guest Debug UI
→ Guest forwards threads/stackTrace/scopes/variables to Host so stopped frames, line highlight, variables, and call stack match the Host
→ Guest buffers early output/stopped events until the mirror adapter initializes, then opens the Debug Console for mirrored output
Path mapping (automatic):
Guest sets breakpoint at lsintranet:///home/user/project/main.py:10
→ PathMapper.guestToHost() → /home/user/project/main.py
→ Host Debug Adapter receives correct local path
→ Variable panel shows lsintranet:// paths → PathMapper.hostToGuest() for display
Debug lifecycle:
Host: onDidStartDebugSession → capture session → notify Guests (debug-start)
Host: onDidTerminateDebugSession → notify Guests (debug-stop)
Guest: "Remote Debug (Guest)" can request the Host to auto-start a session
Breakpoints sync separately through breakpoints-sync and mirror both ways
Configuration
lsIntranet.port — WebSocket server port (default: 9999)
lsIntranet.userName — Display name (defaults to hostname)
Notes
bufferutil, utf-8-validate, and node-pty are native modules, marked as webpack externals
- Host auto-detects local IPs and shows them in the status bar
- Guest has auto-reconnect with exponential backoff (max 10 attempts)
- Terminal data is base64-encoded for safe JSON transport over WebSocket
- Debug DAP proxy is a transparent message forwarder — does not parse DAP content, only maps paths
- File content sync is unified through Yjs (方案2): no separate content request channel, disk changes update Yjs Docs directly
CollabDocManager.createDocFromDisk() creates Yjs docs for files not currently open in the editor
EditorBinding supports rebind() for re-attaching to a different editor instance
AwarenessManager.trackEditor() sets up selection/focus listeners for a specific editor
- Breakpoints now sync through
breakpoints-sync; debug launch is still Host-authoritative and guests proxy through the Host session
- Host debug adapter events are mirrored to Guests through
dap-event; output events appear in the Guest mirror debug session and the Guest forwards stack/variable requests to the Host to render stopped locations
- Shared terminal output now mirrors into a local Host terminal window as well as Guest pseudoterminals
Follow Mode Data Flow
Broadcaster (any user):
onDidChangeActiveTextEditor → broadcast { type: 'follow', filePath, line, character }
onDidChangeTextEditorSelection → broadcast { type: 'follow', filePath, line, character }
Follower:
Receive follow message → open file at path → move cursor to line:character
→ editor.revealRange(InCenterIfOutsideViewport)
Auto-stop:
Follower makes local edit → stopFollow() → no longer processes follow messages
stopFollow command → clears following user ID
Chat Data Flow
Send message:
User types in Webview → postMessage to extension
→ WebSocket chat message → broadcast to all users
→ addLocalMessage() for immediate display (optimistic update)
Receive message:
WebSocket chat message → ChatHandler.handleIncomingMessage()
→ filters by localUserId to avoid showing own messages twice
→ Webview panel postMessage → render in chat UI
Chat panel:
Created on first openChat command
Persists across message sends/receives
Shows sender name + timestamp + message content
Feasibility Analysis and Updated Design
Feasibility summary (intranet-only, IP:port peer discovery):
- Architecture: Host-as-server (ws.Server) with Guests connecting via ws://IP:PORT. This is simple, reliable on corporate LANs and requires no external servers.
- Real-time editing: Yjs per-file CRDTs is appropriate. Use a Y.Doc per file, lazy initialization and on-demand sync to avoid sending whole workspace.
- Discovery: Manual connect by IP:port is acceptable. Optional local discovery (mDNS/UDP) can be added later.
- Security: No external auth required. Recommend optional TLS (wss) for encryption using internal CA or configurable self-signed certs. Keep ability to disable TLS for closed networks.
- Debug & Terminal: Host-side DAP proxy and node-pty are feasible; path mapping required to translate lsintranet:// URIs.
- Scale: Designed for small-to-medium teams (~2-20 active collaborators). For larger scale, add selective sync, sharding, or an optional relay server inside the intranet.
Recommended implementation plan (phased):
Phase 0 — Prototype (validate core components)
- Prototype ws host/guest and Yjs per-file sync (small files)
- Prototype DAP relay and path mapping
- Prototype node-pty terminal relay
Phase 1 — Core features
- Implement HostServer and GuestClient with stable protocol (versioned)
- CollabDocManager (create/apply Yjs updates, persistence)
- EditorBinding and AwarenessManager (cursor/selection)
Phase 2 — FS and lazy sync
- File tree sync and lsintranet:// FileSystemProvider
- yjs-req / yjs-init for on-demand file docs
- Conflict handling policy for disk ↔ Yjs
Phase 3 — Terminal, Debug, Chat
- HostTerminalManager (node-pty) and Guest Pseudoterminal
- DAP proxy with robust path mapping and session multiplexing
- Chat Webview and message UI
Phase 4 — Hardening & UX
- TLS support and configuration UI
- Logging, diagnostics, tests (CRDT convergence + DAP passthrough)
- Packaging and extension marketplace/private deployment docs
Concrete todos (suggested task ids):
- host-server: Implement HostServer (ws) and CLI/config for port, TLS
- yjs-doc-manager: Implement per-file Yjs docs, persistence, lazy load
- editor-binding: Implement two-way binding + awareness
- fs-provider: lsintranet:// provider and file-tree sync
- dap-proxy: DAP relay with path mapping tests
- terminal-proxy: node-pty host relay and guest pseudoterminal
- tls-config: support wss with cert configuration and docs
Notes:
- Keep protocol JSON simple and versioned; allow feature negotiation.
- Minimize data sent on join: send file-tree + doc summaries; request docs on demand.
- Write unit tests for path mapping and CRDT convergence; end-to-end for DAP relay.
- Document limitations (single host, recommended team size, TLS options).
Terminal proxy:
- Host:
src/terminal/host.ts uses node-pty to spawn PTYs, broadcasts terminal-create, terminal-data, terminal-close, and accepts terminal-data and terminal-resize from Guests.
- Guest:
src/terminal/guest.ts implements a vscode.Pseudoterminal per shared terminal, relays keyboard input (terminal-data) and resize (terminal-resize) back to Host.
- Wired in
src/extension.ts: HostTerminalManager and GuestTerminalManager are created and connected to the transport. The shareTerminal command spawns a Host terminal; Guests receive terminal-create and render it.
Testing:
- Start Host, run
Live Share - Intranet: Share Terminal (Host) — Guests will see a new terminal appear.
- On Guest, type into the pseudoterminal; input is relayed to Host PTY.
Live Share - Intranet: Stop Session closes shared terminals.