import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; const STATUS_KEY = "worktree-tui-status"; const GIT_TIMEOUT_MS = 2_000; const WORKTREE_SEGMENTS = ["runs", "agents", "worktrees"] as const; const EXPERIMENT_SEGMENTS = ["runs", "experiments"] as const; const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; export interface DetectedWorktree { root: string; slug: string; source: string; } export interface DetectedExperiment { root: string; product: string; slug: string; source: string; } interface WorktreeStatus extends DetectedWorktree { branch: string | null; dirtyCount: number | null; } type ActiveContext = | { kind: "worktree"; detected: DetectedWorktree } | { kind: "experiment"; detected: DetectedExperiment }; type ToolCallLike = { toolName?: string; input?: Record; }; type UserBashLike = { command?: string; }; function uniq(values: string[]): string[] { return [...new Set(values.filter((value) => value.trim().length > 0))]; } function trimShellToken(value: string): string { return value .trim() .replace(/^@/, "") .replace(/^(["'])(.*)\1$/, "$2") .replace(/[),.;]+$/, ""); } export function normalizePathInput(rawPath: string): string { const normalized = trimShellToken(rawPath).replace(UNICODE_SPACES, " ").replace(/\\/g, "/"); if (normalized === "~") return os.homedir(); if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2)); return normalized; } function absolutePath(rawPath: string, cwd: string): string { const normalized = normalizePathInput(rawPath); return path.normalize(path.isAbsolute(normalized) ? normalized : path.resolve(cwd, normalized)); } export function detectWorktreePath(rawPath: string, cwd: string, source = "path"): DetectedWorktree | null { const absolute = absolutePath(rawPath, cwd); const parsed = path.parse(absolute); const parts = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean); for (let i = 0; i <= parts.length - WORKTREE_SEGMENTS.length - 1; i++) { if ( parts[i] === WORKTREE_SEGMENTS[0] && parts[i + 1] === WORKTREE_SEGMENTS[1] && parts[i + 2] === WORKTREE_SEGMENTS[2] ) { const slug = parts[i + 3]; if (!slug) return null; const root = path.join(parsed.root, ...parts.slice(0, i + 4)); return { root, slug, source }; } } return null; } export function detectExperimentPath(rawPath: string, cwd: string, source = "path"): DetectedExperiment | null { const absolute = absolutePath(rawPath, cwd); const parsed = path.parse(absolute); const parts = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean); for (let i = 0; i <= parts.length - EXPERIMENT_SEGMENTS.length - 2; i++) { if (parts[i] === EXPERIMENT_SEGMENTS[0] && parts[i + 1] === EXPERIMENT_SEGMENTS[1]) { const product = parts[i + 2]; const slug = parts[i + 3]; if (!product || !slug) return null; const root = path.join(parsed.root, ...parts.slice(0, i + 4)); return { root, product, slug, source }; } } return null; } export function extractBashPathCandidates(command: string): string[] { const candidates: string[] = []; // git -C ... const gitC = /\bgit\s+(?:[^\s'"`;|&<>]+\s+)*-C\s+(?:"([^"]+)"|'([^']+)'|([^\s'"`;|&<>]+))/g; for (const match of command.matchAll(gitC)) { candidates.push(match[1] ?? match[2] ?? match[3] ?? ""); } // Any token that contains a project workflow run convention. const runPath = /((?:~|\.{1,2}|\/)?[^\s'"`;|&<>]*(?:runs\/agents\/worktrees|runs\/experiments)\/[^\s'"`;|&<>)]*)/g; for (const match of command.matchAll(runPath)) { candidates.push(match[1] ?? ""); } const cleaned = uniq(candidates.map(trimShellToken)); return cleaned.filter((candidate) => !cleaned.some((other) => other !== candidate && other.startsWith(`${candidate} `))); } export function extractToolPathCandidates(event: ToolCallLike): string[] { const input = event.input; if (!input || typeof input !== "object") return []; const candidates: string[] = []; for (const key of ["path", "cwd", "file", "directory", "target"] as const) { const value = input[key]; if (typeof value === "string") candidates.push(value); } const command = input.command; if (typeof command === "string") { candidates.push(...extractBashPathCandidates(command)); } return uniq(candidates); } function parseDirtyCount(stdout: string): number { return stdout.split(/\r?\n/).filter((line) => line.trim().length > 0).length; } async function runGit(pi: ExtensionAPI, root: string, args: string[]): Promise { try { const result = await pi.exec("git", ["-C", root, ...args], { timeout: GIT_TIMEOUT_MS } as never); if (result.code !== 0) return null; return result.stdout.trim(); } catch { return null; } } async function readWorktreeStatus(pi: ExtensionAPI, detected: DetectedWorktree): Promise { const [branch, dirtyOutput] = await Promise.all([ runGit(pi, detected.root, ["branch", "--show-current"]), runGit(pi, detected.root, ["--no-optional-locks", "status", "--porcelain", "--untracked-files=normal"]), ]); return { ...detected, branch: branch && branch.length > 0 ? branch : null, dirtyCount: dirtyOutput === null ? null : parseDirtyCount(dirtyOutput), }; } function nearestExistingDirectory(fileOrDirectory: string): string | null { let current = fileOrDirectory; while (current && current !== path.dirname(current)) { if (fs.existsSync(current)) { try { return fs.statSync(current).isDirectory() ? current : path.dirname(current); } catch { return null; } } current = path.dirname(current); } return fs.existsSync(current) ? current : null; } async function detectLinkedWorktreeFromExistingPath( pi: ExtensionAPI, rawPath: string, cwd: string, source: string, ): Promise { const probe = nearestExistingDirectory(absolutePath(rawPath, cwd)); if (!probe) return null; const [inside, topLevel, gitDir, commonDir] = await Promise.all([ runGit(pi, probe, ["rev-parse", "--is-inside-work-tree"]), runGit(pi, probe, ["rev-parse", "--show-toplevel"]), runGit(pi, probe, ["rev-parse", "--path-format=absolute", "--git-dir"]), runGit(pi, probe, ["rev-parse", "--path-format=absolute", "--git-common-dir"]), ]); if (inside !== "true" || !topLevel || !gitDir || !commonDir) return null; if (path.resolve(gitDir) === path.resolve(commonDir)) return null; return { root: path.resolve(topLevel), slug: path.basename(topLevel), source, }; } function renderStatus(status: WorktreeStatus): string { const branch = status.branch ? `🌿 ${status.branch}` : "🌿 unknown"; const dirty = status.dirtyCount === null ? "dirty ?" : status.dirtyCount > 0 ? `dirty +${status.dirtyCount}` : "clean"; return `🌳 ${status.slug} | ${branch} | ${dirty}`; } function renderChecking(detected: DetectedWorktree): string { return `🌳 ${detected.slug} | checking…`; } function renderExperiment(detected: DetectedExperiment): string { return `🧪 ${detected.product}/${detected.slug} | experiment`; } function setStatus(ctx: ExtensionContext, text: string | undefined): void { if (!ctx.hasUI) return; try { ctx.ui.setStatus(STATUS_KEY, text); } catch { // Ignore stale UI contexts during reload/session replacement. } } export default function worktreeTuiStatus(pi: ExtensionAPI): void { let active: ActiveContext | null = null; let refreshGeneration = 0; const activateWorktree = (ctx: ExtensionContext, detected: DetectedWorktree): void => { active = { kind: "worktree", detected }; const generation = ++refreshGeneration; setStatus(ctx, renderChecking(detected)); void readWorktreeStatus(pi, detected).then((status) => { if (generation !== refreshGeneration) return; setStatus(ctx, renderStatus(status)); }).catch(() => { if (generation !== refreshGeneration) return; setStatus(ctx, renderChecking(detected)); }); }; const activateExperiment = (ctx: ExtensionContext, detected: DetectedExperiment): void => { active = { kind: "experiment", detected }; refreshGeneration++; setStatus(ctx, renderExperiment(detected)); }; const inspectCandidates = (ctx: ExtensionContext, candidates: string[], source: string): void => { for (const candidate of candidates) { const detected = detectExperimentPath(candidate, ctx.cwd, source); if (detected) { activateExperiment(ctx, detected); return; } } for (const candidate of candidates) { const detected = detectWorktreePath(candidate, ctx.cwd, source); if (detected) { activateWorktree(ctx, detected); return; } } // Fallback for linked worktrees outside runs/agents/worktrees when the path already exists. for (const candidate of candidates) { void detectLinkedWorktreeFromExistingPath(pi, candidate, ctx.cwd, source).then((detected) => { if (detected) activateWorktree(ctx, detected); }); } }; pi.on("session_start", (_event, ctx) => { active = null; refreshGeneration++; setStatus(ctx, undefined); const cwdExperiment = detectExperimentPath(ctx.cwd, ctx.cwd, "cwd"); if (cwdExperiment) { activateExperiment(ctx, cwdExperiment); return; } const cwdWorktree = detectWorktreePath(ctx.cwd, ctx.cwd, "cwd"); if (cwdWorktree) { activateWorktree(ctx, cwdWorktree); return; } // If Pi itself starts inside a linked worktree, show that too. If not, remain silent. void detectLinkedWorktreeFromExistingPath(pi, ctx.cwd, ctx.cwd, "cwd").then((detected) => { if (detected) activateWorktree(ctx, detected); }); }); pi.on("tool_call", (event, ctx) => { inspectCandidates(ctx, extractToolPathCandidates(event as ToolCallLike), event.toolName ?? "tool"); }); pi.on("user_bash", (event, ctx) => { const command = (event as UserBashLike).command; if (typeof command === "string") { inspectCandidates(ctx, extractBashPathCandidates(command), "user_bash"); } }); pi.on("agent_end", (_event, ctx) => { if (!active) return; if (active.kind === "worktree") activateWorktree(ctx, active.detected); else activateExperiment(ctx, active.detected); }); pi.on("session_shutdown", (_event, ctx) => { active = null; refreshGeneration++; setStatus(ctx, undefined); }); }