Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
112 lines
3 KiB
TypeScript
112 lines
3 KiB
TypeScript
/**
|
|
* In-memory sync state for MCP <-> Renderer real-time communication.
|
|
* Shared across Nitro API endpoints: GET/POST /api/mcp/document, GET /api/mcp/events.
|
|
*/
|
|
|
|
import type { PenDocument } from '../../src/types/pen';
|
|
|
|
let currentDocument: PenDocument | null = null;
|
|
let documentVersion = 0;
|
|
let currentSelection: string[] = [];
|
|
let currentActivePageId: string | null = null;
|
|
let lastActiveClientId: string | null = null;
|
|
|
|
interface SSEWriter {
|
|
push(data: string): void;
|
|
}
|
|
|
|
interface SSEClient {
|
|
id: string;
|
|
writer: SSEWriter;
|
|
}
|
|
|
|
const clients = new Map<string, SSEClient>();
|
|
|
|
export function getSyncDocument(): { doc: PenDocument | null; version: number } {
|
|
return { doc: currentDocument, version: documentVersion };
|
|
}
|
|
|
|
export function setSyncDocument(doc: PenDocument, sourceClientId?: string): number {
|
|
currentDocument = doc;
|
|
documentVersion++;
|
|
if (sourceClientId) lastActiveClientId = sourceClientId;
|
|
broadcast({ type: 'document:update', version: documentVersion, document: doc }, sourceClientId);
|
|
return documentVersion;
|
|
}
|
|
|
|
export function getSyncSelection(): { selectedIds: string[]; activePageId: string | null } {
|
|
return { selectedIds: currentSelection, activePageId: currentActivePageId };
|
|
}
|
|
|
|
export function clearSyncState(): void {
|
|
currentDocument = null;
|
|
documentVersion = 0;
|
|
currentSelection = [];
|
|
currentActivePageId = null;
|
|
lastActiveClientId = null;
|
|
}
|
|
|
|
export function setSyncSelection(
|
|
selectedIds: string[],
|
|
activePageId?: string | null,
|
|
sourceClientId?: string,
|
|
): void {
|
|
currentSelection = selectedIds;
|
|
if (activePageId !== undefined) currentActivePageId = activePageId;
|
|
if (sourceClientId) lastActiveClientId = sourceClientId;
|
|
}
|
|
|
|
export function registerSSEClient(id: string, writer: SSEWriter): void {
|
|
clients.set(id, { id, writer });
|
|
}
|
|
|
|
export function unregisterSSEClient(id: string): void {
|
|
clients.delete(id);
|
|
}
|
|
|
|
function broadcast(payload: Record<string, unknown>, excludeClientId?: string): void {
|
|
const recipients: SSEClient[] = [];
|
|
for (const [id, client] of clients) {
|
|
if (id === excludeClientId) continue;
|
|
recipients.push(client);
|
|
}
|
|
|
|
// Return early when there are no recipients to avoid pointless JSON serialization for large documents.
|
|
if (recipients.length === 0) return;
|
|
|
|
const data = JSON.stringify(payload);
|
|
for (const client of recipients) {
|
|
try {
|
|
client.writer.push(data);
|
|
} catch {
|
|
clients.delete(client.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function markClientActive(clientId: string): void {
|
|
if (clients.has(clientId)) {
|
|
lastActiveClientId = clientId;
|
|
}
|
|
}
|
|
|
|
export function getLastActiveClientId(): string | null {
|
|
return lastActiveClientId;
|
|
}
|
|
|
|
export function isClientConnected(clientId: string): boolean {
|
|
return clients.has(clientId);
|
|
}
|
|
|
|
export function sendToClient(clientId: string, payload: Record<string, unknown>): boolean {
|
|
const client = clients.get(clientId);
|
|
if (!client) return false;
|
|
try {
|
|
client.writer.push(JSON.stringify(payload));
|
|
return true;
|
|
} catch {
|
|
clients.delete(clientId);
|
|
return false;
|
|
}
|
|
}
|