vnc sidecar: per-user desktop sessions via sidecar architecture
replaces the single hardcoded systemd VNC service with a dynamic sidecar that manages per-user VNC sessions on demand. any authenticated user can now access their own desktop, not just Super Admin. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,17 +2,18 @@ import type { ServerWebSocket } from 'bun';
|
||||
import type {
|
||||
SidecarCommand,
|
||||
SidecarEvent,
|
||||
SidecarState,
|
||||
ClaudeState,
|
||||
ClaudeSpawnParams,
|
||||
ClaudeSpawnStreamingParams,
|
||||
ClaudeCodeResult,
|
||||
PiSpawnParams,
|
||||
PtyCommand,
|
||||
PtyEvent,
|
||||
VncStartParams,
|
||||
VncSessionInfo,
|
||||
} from './sidecar/protocol';
|
||||
import type { SidecarRegistration } from './sidecar/registration-protocol';
|
||||
import type { PiEvent } from './api/pi/types';
|
||||
import type { Job, EnqueueParams } from './queue/types';
|
||||
|
||||
// ── Types ──
|
||||
|
||||
@@ -36,7 +37,7 @@ type EventHandler = (event: SidecarEvent | PtyEvent) => void;
|
||||
const sidecars = new Map<string, RegisteredSidecar>();
|
||||
const pending = new Map<string, PendingRequest>();
|
||||
const eventHandlers = new Map<string, Set<EventHandler>>();
|
||||
let cachedState: SidecarState | null = null;
|
||||
let cachedState: ClaudeState | null = null;
|
||||
let idCounter = 0;
|
||||
|
||||
function nextId(): string {
|
||||
@@ -81,7 +82,7 @@ export function unregisterSidecar(id: string): void {
|
||||
pending.delete(reqId);
|
||||
}
|
||||
|
||||
// Clear cached state if the process sidecar disconnects
|
||||
// Clear cached state if the claude sidecar disconnects
|
||||
if (sc.capabilities.includes('proxy')) {
|
||||
cachedState = null;
|
||||
}
|
||||
@@ -110,12 +111,6 @@ function findSidecarByCapability(cap: string): RegisteredSidecar | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function requireSidecar(cap: string): RegisteredSidecar {
|
||||
const sc = findSidecarByCapability(cap);
|
||||
if (!sc) throw new Error(`No sidecar with capability "${cap}" is connected`);
|
||||
return sc;
|
||||
}
|
||||
|
||||
// ── Event dispatch ──
|
||||
|
||||
function dispatchEvent(msg: SidecarEvent | PtyEvent) {
|
||||
@@ -171,17 +166,17 @@ function sendFire(cap: string, cmd: SidecarCommand | PtyCommand): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public API (same signatures as sidecar-client.ts) ──
|
||||
// ── Public API ──
|
||||
|
||||
export function isConnected(): boolean {
|
||||
return findSidecarByCapability('proxy') !== undefined;
|
||||
}
|
||||
|
||||
export function getCachedState(): SidecarState | null {
|
||||
export function getCachedState(): ClaudeState | null {
|
||||
return cachedState;
|
||||
}
|
||||
|
||||
export async function syncState(): Promise<SidecarState> {
|
||||
export async function syncState(): Promise<ClaudeState> {
|
||||
const res = await sendCommand('proxy', { type: 'state:sync', id: nextId() });
|
||||
if (res.type === 'state:sync') {
|
||||
cachedState = res.state;
|
||||
@@ -272,34 +267,6 @@ export function onPiEvent(handler: (sessionId: string, event: PiEvent) => void):
|
||||
});
|
||||
}
|
||||
|
||||
// ── Queue ──
|
||||
|
||||
export async function enqueueJob(params: EnqueueParams): Promise<Job> {
|
||||
const res = await sendCommand('queue', { type: 'queue:enqueue', id: nextId(), params });
|
||||
if (res.type === 'queue:enqueued') return res.job;
|
||||
if (res.type === 'queue:error') throw new Error(res.error);
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export async function cancelJob(jobId: string): Promise<Job | null> {
|
||||
const res = await sendCommand('queue', { type: 'queue:cancel', id: nextId(), jobId });
|
||||
if (res.type === 'queue:cancelled') return res.job;
|
||||
if (res.type === 'queue:error') throw new Error(res.error);
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export async function listJobs(): Promise<Job[]> {
|
||||
const res = await sendCommand('queue', { type: 'queue:list', id: nextId() });
|
||||
if (res.type === 'queue:list') return res.jobs;
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export async function getJob(jobId: string): Promise<Job | null> {
|
||||
const res = await sendCommand('queue', { type: 'queue:get', id: nextId(), jobId });
|
||||
if (res.type === 'queue:get') return res.job;
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
// ── Terminal (PTY sidecar) ──
|
||||
|
||||
export function sendPtyCommand(cmd: PtyCommand): void {
|
||||
@@ -313,3 +280,26 @@ export async function sendPtyCommandAsync(cmd: PtyCommand, timeoutMs = DEFAULT_T
|
||||
export function isTerminalConnected(): boolean {
|
||||
return findSidecarByCapability('terminal') !== undefined;
|
||||
}
|
||||
|
||||
// ── VNC ──
|
||||
|
||||
export async function startVnc(params: VncStartParams): Promise<{ port: number; display: number }> {
|
||||
const res = await sendCommand('vnc', { type: 'vnc:start', id: nextId(), params });
|
||||
if (res.type === 'vnc:started') return { port: res.port, display: res.display };
|
||||
if (res.type === 'vnc:error') throw new Error(res.error);
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export function stopVnc(email: string): void {
|
||||
sendFire('vnc', { type: 'vnc:stop', id: nextId(), email });
|
||||
}
|
||||
|
||||
export async function getVncStatus(email: string): Promise<VncSessionInfo | null> {
|
||||
const res = await sendCommand('vnc', { type: 'vnc:status', id: nextId(), email });
|
||||
if (res.type === 'vnc:status') return res.session;
|
||||
throw new Error('Unexpected response');
|
||||
}
|
||||
|
||||
export function isVncConnected(): boolean {
|
||||
return findSidecarByCapability('vnc') !== undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user