Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 96 additions & 1 deletion apps/labs/electron/kernel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const SESSION_LIMIT = 200;
const OPENWORK_PORT = 8787;
const MICROSANDBOX_IMAGE = process.env.LABS_MICROSANDBOX_IMAGE || "node:current-bookworm";
const KERNEL_DIR = dirname(fileURLToPath(import.meta.url));
const OPENWORK_HOST_DIR = resolve(KERNEL_DIR, "../../openwork-host");
const OPENWORK_HOST_ENTRY = resolve(OPENWORK_HOST_DIR, "dist/index.js");
const SDK_DIR = resolve(KERNEL_DIR, "../node_modules/@opencode-ai/sdk");
const GUEST_START_PATH = resolve(KERNEL_DIR, "../runtime/guest-start.mjs");
const OPENCODE_CACHE_DIR = resolve(KERNEL_DIR, "../.cache/opencode-linux-arm64");
Expand Down Expand Up @@ -210,6 +212,17 @@ async function ensureMicrosandbox() {
}
}

async function ensureOpenworkHostBuild() {
try {
return await ensurePath(OPENWORK_HOST_ENTRY);
} catch {
await run("pnpm", ["--filter", "openwork-host", "build"], {
cwd: resolve(KERNEL_DIR, "../../.."),
});
return ensurePath(OPENWORK_HOST_ENTRY);
}
}

async function ensureGuestAssets() {
await ensurePath(SDK_DIR);
await ensurePath(GUEST_START_PATH);
Expand Down Expand Up @@ -318,6 +331,73 @@ async function maybeResolveRemoteBase(workspace) {
}
}

async function ensureOpenworkHostWorkspace(workspace) {
if (!workspace.repoPath?.trim()) {
throw new Error("Choose a repository folder for this workspace.");
}

const existing = runtimeState.hosts.get(workspace.id);
if (existing) {
return {
...workspace,
baseUrl: existing.baseUrl,
runtime: "openwork-host",
kind: "local",
hostPort: existing.hostPort,
serverType: "opencode",
serverWorkspaceId: null,
};
}

await ensureOpenworkHostBuild();
const hostPort = Number.isFinite(workspace.hostPort) && workspace.hostPort ? workspace.hostPort : await getFreePort();
const child = spawn("node", [OPENWORK_HOST_ENTRY], {
cwd: OPENWORK_HOST_DIR,
env: {
...process.env,
OPENWORK_HOST_PORT: String(hostPort),
OPENWORK_HOST_WORKSPACE_DIR: workspace.repoPath.trim(),
OPENWORK_HOST_MODEL_PROVIDER: process.env.OPENWORK_HOST_MODEL_PROVIDER,
OPENWORK_HOST_MODEL_ID: process.env.OPENWORK_HOST_MODEL_ID,
},
stdio: ["ignore", "pipe", "pipe"],
});

let output = "";
child.stdout?.on("data", (chunk) => {
output += chunk.toString();
});
child.stderr?.on("data", (chunk) => {
output += chunk.toString();
});

const baseUrl = `http://127.0.0.1:${hostPort}`;
await waitFor(() => fetchJson(`${baseUrl}/global/health`, null), 30_000, 1_000).catch((error) => {
child.kill();
throw new Error(`${describeError(error)}\n${output}`.trim());
});

runtimeState.hosts.set(workspace.id, {
child,
hostPort,
baseUrl,
});

child.once("exit", () => {
runtimeState.hosts.delete(workspace.id);
});

return {
...workspace,
baseUrl,
runtime: "openwork-host",
kind: "local",
hostPort,
serverType: "opencode",
serverWorkspaceId: null,
};
}

async function ensureMicrosandboxWorkspace(workspace) {
if (!workspace.repoPath?.trim()) {
throw new Error("Choose a repository folder for this workspace.");
Expand Down Expand Up @@ -423,6 +503,7 @@ const runtimeState = {
localBoot: null,
workspaces: new Map(),
locals: new Map(),
hosts: new Map(),
};

async function ensureLocalServer() {
Expand Down Expand Up @@ -506,8 +587,14 @@ async function removeWorkspace(workspaceId) {
const workspace = cleanupWorkspaceEntry(workspaceId);
const local = runtimeState.locals.get(workspaceId) ?? null;
runtimeState.locals.delete(workspaceId);
const host = runtimeState.hosts.get(workspaceId) ?? null;
runtimeState.hosts.delete(workspaceId);
if (!workspace) return true;

if (host?.child && !host.child.killed) {
host.child.kill();
}

if (local?.sandbox) {
try {
await local.sandbox.kill().catch(() => {});
Expand Down Expand Up @@ -573,7 +660,9 @@ async function refreshWorkspace(workspaceId) {

async function ensureWorkspace(workspace) {
let normalized = { ...workspace };
if (normalized.kind === "local" || normalized.runtime === "microsandbox" || normalized.repoPath?.trim()) {
if (normalized.runtime === "openwork-host") {
normalized = await ensureOpenworkHostWorkspace(normalized);
} else if (normalized.kind === "local" || normalized.runtime === "microsandbox" || normalized.repoPath?.trim()) {
normalized = await ensureMicrosandboxWorkspace(normalized);
} else {
normalized = await maybeResolveRemoteBase(normalized);
Expand Down Expand Up @@ -692,6 +781,12 @@ function teardownKernel() {
for (const [workspaceId] of runtimeState.locals.entries()) {
runtimeState.locals.delete(workspaceId);
}
for (const [workspaceId, host] of runtimeState.hosts.entries()) {
runtimeState.hosts.delete(workspaceId);
if (host.child && !host.child.killed) {
host.child.kill();
}
}
runtimeState.localRuntime?.close();
runtimeState.localRuntime = null;
}
Expand Down
10 changes: 5 additions & 5 deletions apps/labs/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ export function App() {
</main>

{workspaceModalOpen ? (
<ModalFrame title="Create workspace" subtitle="Leave the URL blank to launch a local microsandbox workspace from a repo, or paste any OpenWork/OpenCode server URL.">
<ModalFrame title="Create workspace" subtitle="Leave the URL blank to launch a local OpenWork Host workspace from a repo, or paste any OpenWork/OpenCode server URL.">
<div className="modal-field-grid">
<label className="modal-field">
<span>Name</span>
Expand All @@ -564,7 +564,7 @@ export function App() {
}
placeholder="https://worker.openworklabs.com/opencode"
/>
<small className="modal-help">Leave this blank to launch a local microsandbox workspace from a repo. Add an access token below for protected remote servers.</small>
<small className="modal-help">Leave this blank to launch a local OpenWork Host workspace from a repo. Add an access token below for protected remote servers.</small>
</label>

{!workspaceForm.baseUrl.trim() ? (
Expand All @@ -583,7 +583,7 @@ export function App() {
Browse
</button>
</div>
<small className="modal-help">Local workspaces boot OpenWork inside microsandbox using a snapshot of this repo.</small>
<small className="modal-help">Local workspaces boot OpenWork Host from this repo on your machine in this branch.</small>
</label>
) : null}

Expand Down Expand Up @@ -740,7 +740,7 @@ export function App() {
onChange={(event) => setNewWorkspaceUrl(event.target.value)}
placeholder="https://worker.openworklabs.com/opencode"
/>
<small className="modal-help">Leave this blank to launch a local microsandbox workspace from a repo. Add an access token below for protected remote servers.</small>
<small className="modal-help">Leave this blank to launch a local OpenWork Host workspace from a repo. Add an access token below for protected remote servers.</small>
</label>
{!newWorkspaceUrl.trim() ? (
<label className="modal-field">
Expand All @@ -756,7 +756,7 @@ export function App() {
Browse
</button>
</div>
<small className="modal-help">Local workspaces boot OpenWork inside microsandbox using a snapshot of this repo.</small>
<small className="modal-help">Local workspaces boot OpenWork Host from this repo on your machine in this branch.</small>
</label>
) : null}
{newWorkspaceUrl.trim() ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/labs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export type LabsWorkspace = {
token?: string | null;
color: string;
kind?: "local" | "remote";
runtime?: "microsandbox" | "remote";
runtime?: "microsandbox" | "openwork-host" | "remote";
repoPath?: string | null;
hostPort?: number | null;
sandboxName?: string | null;
Expand Down
6 changes: 3 additions & 3 deletions apps/labs/src/use-labs-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ type WorkspaceInput = {
baseUrl: string;
token?: string | null;
kind?: "local" | "remote";
runtime?: "microsandbox" | "remote";
runtime?: "microsandbox" | "openwork-host" | "remote";
repoPath?: string | null;
hostPort?: number | null;
sandboxName?: string | null;
Expand Down Expand Up @@ -1140,7 +1140,7 @@ export function useLabsApp(): Controller {
token: input.token?.trim() || null,
color: existing?.color ?? pickWorkspaceColor(id),
kind: input.kind ?? existing?.kind ?? "remote",
runtime: input.runtime ?? existing?.runtime ?? (input.kind === "local" ? "microsandbox" : "remote"),
runtime: input.runtime ?? existing?.runtime ?? (input.kind === "local" ? "openwork-host" : "remote"),
repoPath: input.repoPath?.trim() || existing?.repoPath || null,
hostPort: input.hostPort ?? existing?.hostPort ?? null,
sandboxName: input.sandboxName?.trim() || existing?.sandboxName || null,
Expand Down Expand Up @@ -1170,7 +1170,7 @@ export function useLabsApp(): Controller {
token: null,
color: pickWorkspaceColor(id),
kind: "local",
runtime: "microsandbox",
runtime: "openwork-host",
repoPath: sourceRepo,
hostPort: null,
sandboxName: `labs-${id}`,
Expand Down
22 changes: 22 additions & 0 deletions apps/openwork-host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "openwork-host",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"start": "node dist/index.js"
},
"dependencies": {
"@hono/node-server": "^1.19.4",
"@mariozechner/pi-ai": "^0.64.0",
"@mariozechner/pi-coding-agent": "^0.64.0",
"hono": "^4.10.1",
"nanoid": "^5.1.6"
},
"devDependencies": {
"@types/node": "^24.6.0",
"typescript": "^5.9.3"
}
}
Loading
Loading