openpencil/apps/web/server/plugins/port-file.ts
Kayshen-X a7d73ebb62 feat(ai): pencil-style agentic design tool-loop, multi-chat tabs, #27 panel restyle
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).
2026-07-02 21:21:06 +08:00

62 lines
1.7 KiB
TypeScript

/**
* Nitro plugin — writes ~/.openpencil/.port on server startup so the MCP
* server can discover the running instance (dev server or Electron).
*
* In Electron production mode the main process also writes this file,
* but this plugin ensures the dev server (`bun --bun run dev`) is
* discoverable too.
*/
import { writeFile, mkdir, unlink, readFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { homedir } from 'node:os';
const PORT_FILE_DIR = join(homedir(), '.openpencil');
const PORT_FILE_PATH = join(PORT_FILE_DIR, '.port');
const PORT_FILE_TOKEN = randomUUID();
function getOwnerPid(): number {
return process.ppid > 1 ? process.ppid : process.pid;
}
async function writePortFile(port: number): Promise<void> {
try {
await mkdir(PORT_FILE_DIR, { recursive: true });
await writeFile(
PORT_FILE_PATH,
JSON.stringify({
port,
pid: getOwnerPid(),
writerPid: process.pid,
token: PORT_FILE_TOKEN,
timestamp: Date.now(),
}),
'utf-8',
);
} catch {
// Non-critical — MCP sync will fall back to file I/O
}
}
async function cleanupPortFile(): Promise<void> {
try {
const raw = await readFile(PORT_FILE_PATH, 'utf-8');
const current = JSON.parse(raw) as { token?: string };
if (current.token !== PORT_FILE_TOKEN) return;
await unlink(PORT_FILE_PATH);
} catch {
// Ignore if already removed
}
}
export default () => {
const port = parseInt(process.env.PORT || '3000', 10);
writePortFile(port);
const cleanup = () => {
cleanupPortFile();
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
};