diff --git a/dev-dashboard/backend/src/server.ts b/dev-dashboard/backend/src/server.ts index f4fe0f7cc1..8ec0dc0a08 100644 --- a/dev-dashboard/backend/src/server.ts +++ b/dev-dashboard/backend/src/server.ts @@ -6,6 +6,7 @@ import { openWorktree, closeWorktree, sendPrompt, + readEnvLocal, type Profile, } from "./workmux"; import { @@ -21,6 +22,42 @@ import { const PORT = parseInt(process.env.DASHBOARD_PORT || "5111"); +/** Map branch name → worktree directory using git worktree list. */ +function getWorktreePaths(): Map { + const result = Bun.spawnSync(["git", "worktree", "list", "--porcelain"], { stdout: "pipe" }); + const output = new TextDecoder().decode(result.stdout); + const paths = new Map(); + let currentPath = ""; + for (const line of output.split("\n")) { + if (line.startsWith("worktree ")) { + currentPath = line.slice("worktree ".length); + } else if (line.startsWith("branch ")) { + // branch refs/heads/foo → "foo" + const branch = line.slice("branch ".length).replace("refs/heads/", ""); + // Also map by directory basename (workmux uses basename as branch key) + const basename = currentPath.split("/").pop() ?? ""; + paths.set(branch, currentPath); + if (basename !== branch) paths.set(basename, currentPath); + } + } + return paths; +} + +/** Check if a TCP port is listening by attempting a connection. */ +function isPortListening(port: number): Promise { + return new Promise((resolve) => { + const socket = Bun.connect({ + hostname: "127.0.0.1", + port, + socket: { + open(s) { s.end(); resolve(true); }, + error() { resolve(false); }, + data() {}, + }, + }).catch(() => resolve(false)); + }); +} + function jsonResponse(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), { status, @@ -129,12 +166,30 @@ async function handleApi(req: Request, url: URL): Promise { // GET /api/worktrees if (parts[0] === "worktrees" && parts.length === 1 && method === "GET") { const [worktrees, status] = await Promise.all([listWorktrees(), getStatus()]); - const merged = worktrees.map(wt => { + const wtPaths = getWorktreePaths(); + const merged = await Promise.all(worktrees.map(async (wt) => { const st = status.find(s => s.worktree.includes(wt.branch) || s.worktree.startsWith(wt.branch) ); - return { ...wt, status: st?.status ?? "", elapsed: st?.elapsed ?? "", title: st?.title ?? "" }; - }); + const wtDir = wtPaths.get(wt.branch); + const env = wtDir ? readEnvLocal(wtDir) : {}; + const backendPort = env.BACKEND_PORT ? parseInt(env.BACKEND_PORT) : null; + const frontendPort = env.FRONTEND_PORT ? parseInt(env.FRONTEND_PORT) : null; + const [backendRunning, frontendRunning] = await Promise.all([ + backendPort ? isPortListening(backendPort) : false, + frontendPort ? isPortListening(frontendPort) : false, + ]); + return { + ...wt, + status: st?.status ?? "", + elapsed: st?.elapsed ?? "", + title: st?.title ?? "", + backendPort, + frontendPort, + backendRunning, + frontendRunning, + }; + })); return jsonResponse(merged); } diff --git a/dev-dashboard/backend/src/workmux.ts b/dev-dashboard/backend/src/workmux.ts index 7ac187cf44..dc0b151934 100644 --- a/dev-dashboard/backend/src/workmux.ts +++ b/dev-dashboard/backend/src/workmux.ts @@ -79,7 +79,7 @@ async function runChecked(args: string[]): Promise { export type Profile = "full" | "agent-only" | "agent-yolo"; -function readEnvLocal(wtDir: string): Record { +export function readEnvLocal(wtDir: string): Record { try { const content = Bun.spawnSync(["cat", `${wtDir}/.env.local`], { stdout: "pipe" }); const text = new TextDecoder().decode(content.stdout).trim(); diff --git a/dev-dashboard/frontend/src/lib/TopBar.svelte b/dev-dashboard/frontend/src/lib/TopBar.svelte index 4eeefd3bf9..131bce3dff 100644 --- a/dev-dashboard/frontend/src/lib/TopBar.svelte +++ b/dev-dashboard/frontend/src/lib/TopBar.svelte @@ -11,7 +11,25 @@
- {name ?? "Select a worktree"} +
+ {name ?? "Select a worktree"} + {#if worktree?.backendPort} + BE :{worktree.backendPort} + {/if} + {#if worktree?.frontendPort} + FE :{worktree.frontendPort} + {/if} +
{#if name}
{worktree?.status || worktree?.agent || ""} diff --git a/dev-dashboard/frontend/src/lib/types.ts b/dev-dashboard/frontend/src/lib/types.ts index 1382ae4836..84b7e2dd0a 100644 --- a/dev-dashboard/frontend/src/lib/types.ts +++ b/dev-dashboard/frontend/src/lib/types.ts @@ -6,4 +6,8 @@ export interface WorktreeInfo { status: string; elapsed: string; title: string; + backendPort: number | null; + frontendPort: number | null; + backendRunning: boolean; + frontendRunning: boolean; }