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).
88 lines
2.3 KiB
TypeScript
88 lines
2.3 KiB
TypeScript
/** Port file discovery and app health check. */
|
|
|
|
import { readFile, unlink } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
|
|
const PORT_FILE_DIR = '.openpencil';
|
|
const PORT_FILE_NAME = '.port';
|
|
const PORT_FILE_PATH = join(homedir(), PORT_FILE_DIR, PORT_FILE_NAME);
|
|
const APP_BASE_URLS = ['http://127.0.0.1', 'http://localhost'];
|
|
|
|
async function getReachableAppUrl(port: number): Promise<string | null> {
|
|
for (const baseUrl of APP_BASE_URLS) {
|
|
const url = `${baseUrl}:${port}/api/mcp/server`;
|
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
try {
|
|
const res = await fetch(url, {
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
if (res.ok) return `${baseUrl}:${port}`;
|
|
} catch {
|
|
// App may still be starting, or the port file may be stale.
|
|
}
|
|
if (attempt < 4) {
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isPidAlive(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (err: unknown) {
|
|
return (err as NodeJS.ErrnoException).code === 'EPERM';
|
|
}
|
|
}
|
|
|
|
export interface AppInfo {
|
|
port: number;
|
|
pid: number;
|
|
timestamp: number;
|
|
url: string;
|
|
}
|
|
|
|
/** Read port file and return app info, or null if no running instance. */
|
|
export async function getAppInfo(): Promise<AppInfo | null> {
|
|
try {
|
|
const raw = await readFile(PORT_FILE_PATH, 'utf-8');
|
|
const { port, pid, timestamp } = JSON.parse(raw) as {
|
|
port: number;
|
|
pid: number;
|
|
timestamp: number;
|
|
};
|
|
const url = await getReachableAppUrl(port);
|
|
if (url) {
|
|
return { port, pid, timestamp, url };
|
|
}
|
|
if (!isPidAlive(pid)) {
|
|
try {
|
|
await unlink(PORT_FILE_PATH);
|
|
} catch {
|
|
// Ignore stale port file cleanup failures.
|
|
}
|
|
return null;
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Get app URL or throw if not running. */
|
|
export async function requireApp(): Promise<string> {
|
|
const info = await getAppInfo();
|
|
if (!info) {
|
|
throw new Error('No running OpenPencil instance found. Run `openpencil start` first.');
|
|
}
|
|
return info.url;
|
|
}
|
|
|
|
/** Quick check if app is running. */
|
|
export async function isAppRunning(): Promise<boolean> {
|
|
return (await getAppInfo()) !== null;
|
|
}
|