From be277b762388cdc95bbc24441bb07a8a575eb4d3 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 23 Feb 2026 21:28:23 -0800 Subject: [PATCH 1/3] Add stat formatting with colorized diff indicators --- src/commands/web.ts | 17 ++++++++++++++++- src/git.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/commands/web.ts b/src/commands/web.ts index 1f4b95b..757d3e1 100644 --- a/src/commands/web.ts +++ b/src/commands/web.ts @@ -25,7 +25,7 @@ export async function action(branch: string) { .replaceAll("{{BRANCH}}", escapeHtml(branch)) .replace("{{PROMPT_SECTION}}", () => session.prompt ? `

${escapeHtml(session.prompt)}

` : "") .replace("{{LOG_SECTION}}", () => log ? `

Commits

${escapeHtml(log)}
` : "") - .replace("{{STAT_SECTION}}", () => stat ? `

Stats

${escapeHtml(stat)}
` : "") + .replace("{{STAT_SECTION}}", () => stat ? `

Stats

${formatStat(stat)}
` : "") .replace("{{DIFF_JSON}}", () => diffJson) const tmpPath = `/tmp/sandlot-${branch}.html` @@ -33,6 +33,21 @@ export async function action(branch: string) { await $`open ${tmpPath}`.nothrow().quiet() } +function formatStat(raw: string): string { + // strip leading space from lines to match the unindented first line + const trimmed = raw.split("\n").map(l => l.startsWith(" ") ? l.slice(1) : l).join("\n") + const escaped = escapeHtml(trimmed) + // colorize + and - in the bar portion (after the |) + return escaped.split("\n").map(line => { + const pipe = line.indexOf("|") + if (pipe === -1) return line + return line.slice(0, pipe + 1) + + line.slice(pipe + 1) + .replace(/\+/g, '+') + .replace(/-/g, '-') + }).join("\n") +} + function escapeHtml(str: string): string { return str .replace(/&/g, "&") diff --git a/src/git.ts b/src/git.ts index 7afd7de..5aee49f 100644 --- a/src/git.ts +++ b/src/git.ts @@ -198,7 +198,7 @@ export async function commitLog(range: string, cwd: string): Promise { /** Get a diff stat summary for a revision range. Returns empty string on failure. */ export async function diffStat(range: string, cwd: string): Promise { - const result = await $`git diff --stat ${range}`.cwd(cwd).nothrow().quiet() + const result = await $`git diff --stat --stat-width=68 ${range}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) return "" return result.text().trim() } From 3d41e3de2907b2df868e9e9b8ff59ef6e0802560 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 24 Feb 2026 08:47:57 -0800 Subject: [PATCH 2/3] Support optional ~/Code mount alongside ~/dev --- src/vm.ts | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/vm.ts b/src/vm.ts index 6425491..260f12b 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -18,6 +18,9 @@ export function containerPath(hostPath: string): string { if (hostPath.startsWith(`${home}/dev`)) { return "/host" + hostPath.slice(`${home}/dev`.length) } + if (hostPath.startsWith(`${home}/Code`)) { + return "/host-code" + hostPath.slice(`${home}/Code`.length) + } return hostPath } @@ -41,11 +44,28 @@ async function run(cmd: ReturnType, step: string): Promise { // ── create() helpers (internal) ────────────────────────────────────── +/** Check which host source directories exist. */ +async function hostMounts(home: string): Promise<{ dev: boolean; code: boolean }> { + const [dev, code] = await Promise.all([ + Bun.file(`${home}/dev`).exists().catch(() => false), + Bun.file(`${home}/Code`).exists().catch(() => false), + ]) + return { dev, code } +} + /** Pull the image and start the container in detached mode. */ async function createContainer(home: string): Promise { - await run( - $`container run -d --name ${CONTAINER_NAME} -m 4G --mount type=bind,source=${home}/dev,target=/host,readonly -v ${home}/.sandlot:/sandlot ubuntu:24.04 sleep infinity`, - "Container creation") + const mounts = await hostMounts(home) + const args = ["container", "run", "-d", "--name", CONTAINER_NAME, "-m", "4G"] + if (mounts.dev) args.push("--mount", `type=bind,source=${home}/dev,target=/host,readonly`) + if (mounts.code) args.push("--mount", `type=bind,source=${home}/Code,target=/host-code,readonly`) + args.push("-v", `${home}/.sandlot:/sandlot`, "ubuntu:24.04", "sleep", "infinity") + const result = await $`${args}`.nothrow().quiet() + if (result.exitCode !== 0) { + const stderr = result.stderr.toString().trim() + const stdout = result.stdout.toString().trim() + throw new Error(`Container creation failed (exit ${result.exitCode}):\n${stderr || stdout || "(no output)"}`) + } } /** Install base system packages (as root). */ @@ -60,8 +80,12 @@ async function installPackages(cached: boolean): Promise { /** Create symlinks so git worktree absolute paths from the host resolve inside the container. */ async function createHostSymlinks(home: string): Promise { + const mounts = await hostMounts(home) + const cmds = [`mkdir -p '${home}'`, `ln -s /sandlot '${home}/.sandlot'`] + if (mounts.dev) cmds.push(`ln -s /host '${home}/dev'`) + if (mounts.code) cmds.push(`ln -s /host-code '${home}/Code'`) await run( - $`container exec ${CONTAINER_NAME} bash -c ${`mkdir -p '${home}' && ln -s /host '${home}/dev' && ln -s /sandlot '${home}/.sandlot'`}`, + $`container exec ${CONTAINER_NAME} bash -c ${cmds.join(" && ")}`, "Symlink creation") } @@ -240,13 +264,17 @@ export async function status(): Promise<"running" | "stopped" | "missing"> { /** Launch claude in the container at the given workdir. */ export async function claude(workdir: string, opts?: { prompt?: string; print?: string; continue?: boolean }): Promise<{ exitCode: number; output?: string }> { const cwd = containerPath(workdir) + const mounts = await hostMounts(homedir()) const systemPromptLines = [ "You are running inside a sandlot container (Apple Container, ubuntu:24.04).", `Your working directory is ${cwd}, a git worktree managed by sandlot.`, - "The host's ~/dev is mounted read-only at /host.", + ] + if (mounts.dev) systemPromptLines.push("The host's ~/dev is mounted read-only at /host.") + if (mounts.code) systemPromptLines.push("The host's ~/Code is mounted read-only at /host-code.") + systemPromptLines.push( "The host's ~/.sandlot is mounted at /sandlot.", "Bun is installed at ~/.local/bin/bun. Use bun instead of node/npm.", - ] + ) if (opts?.print) { systemPromptLines.push("IMPORTANT: Do not use plan mode. Do not call the EnterPlanMode tool. Proceed directly with the task.") } From 3b21a2afd1abb79b26e0b00d2a496d3b030081de Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 24 Feb 2026 08:49:33 -0800 Subject: [PATCH 3/3] Use nested paths for host mounts --- src/vm.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vm.ts b/src/vm.ts index 260f12b..7310736 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -16,10 +16,10 @@ export function containerPath(hostPath: string): string { return "/sandlot" + hostPath.slice(`${home}/.sandlot`.length) } if (hostPath.startsWith(`${home}/dev`)) { - return "/host" + hostPath.slice(`${home}/dev`.length) + return "/host/dev" + hostPath.slice(`${home}/dev`.length) } if (hostPath.startsWith(`${home}/Code`)) { - return "/host-code" + hostPath.slice(`${home}/Code`.length) + return "/host/Code" + hostPath.slice(`${home}/Code`.length) } return hostPath } @@ -57,8 +57,8 @@ async function hostMounts(home: string): Promise<{ dev: boolean; code: boolean } async function createContainer(home: string): Promise { const mounts = await hostMounts(home) const args = ["container", "run", "-d", "--name", CONTAINER_NAME, "-m", "4G"] - if (mounts.dev) args.push("--mount", `type=bind,source=${home}/dev,target=/host,readonly`) - if (mounts.code) args.push("--mount", `type=bind,source=${home}/Code,target=/host-code,readonly`) + if (mounts.dev) args.push("--mount", `type=bind,source=${home}/dev,target=/host/dev,readonly`) + if (mounts.code) args.push("--mount", `type=bind,source=${home}/Code,target=/host/Code,readonly`) args.push("-v", `${home}/.sandlot:/sandlot`, "ubuntu:24.04", "sleep", "infinity") const result = await $`${args}`.nothrow().quiet() if (result.exitCode !== 0) { @@ -82,8 +82,8 @@ async function installPackages(cached: boolean): Promise { async function createHostSymlinks(home: string): Promise { const mounts = await hostMounts(home) const cmds = [`mkdir -p '${home}'`, `ln -s /sandlot '${home}/.sandlot'`] - if (mounts.dev) cmds.push(`ln -s /host '${home}/dev'`) - if (mounts.code) cmds.push(`ln -s /host-code '${home}/Code'`) + if (mounts.dev) cmds.push(`ln -s /host/dev '${home}/dev'`) + if (mounts.code) cmds.push(`ln -s /host/Code '${home}/Code'`) await run( $`container exec ${CONTAINER_NAME} bash -c ${cmds.join(" && ")}`, "Symlink creation") @@ -269,8 +269,8 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?: "You are running inside a sandlot container (Apple Container, ubuntu:24.04).", `Your working directory is ${cwd}, a git worktree managed by sandlot.`, ] - if (mounts.dev) systemPromptLines.push("The host's ~/dev is mounted read-only at /host.") - if (mounts.code) systemPromptLines.push("The host's ~/Code is mounted read-only at /host-code.") + if (mounts.dev) systemPromptLines.push("The host's ~/dev is mounted read-only at /host/dev.") + if (mounts.code) systemPromptLines.push("The host's ~/Code is mounted read-only at /host/Code.") systemPromptLines.push( "The host's ~/.sandlot is mounted at /sandlot.", "Bun is installed at ~/.local/bin/bun. Use bun instead of node/npm.",