From bc102e416c02050d866881c990e503627a9a7b1c Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 18 Mar 2026 23:16:06 -0700 Subject: [PATCH] Add in_review status to sessions and self-heal stale review flags Review status now requires Claude to be active, preventing stale flags from showing after a crash. Consolidates icon/color maps into a single styles record and defers setting in_review until the container is up. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 6 ++++-- src/commands/list.ts | 17 +++++++++++------ src/commands/review.ts | 14 +++++++------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0e7e034..4ce03b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,7 +120,8 @@ Always use `.nothrow()` for commands that may fail non-fatally. Use `.quiet()` t "branch": "branch-name", "worktree": "/Users/you/.sandlot/repo/branch-name", "created_at": "2026-02-16T10:30:00Z", - "prompt": "optional initial prompt text" + "prompt": "optional initial prompt text", + "in_review": false } } } @@ -149,7 +150,8 @@ Always use `.nothrow()` for commands that may fail non-fatally. Use `.quiet()` t - `sandlot squash` generates an AI commit message for the squash commit via `claudePipe()`; falls back to `"squash "` - `sandlot merge` and `sandlot squash` both delegate to `mergeAndClose()` in `helpers.ts`, which merges, resolves conflicts, commits, and then calls `closeAction()` to clean up - `sandlot list` discovers missing session prompts by parsing Claude's `history.jsonl` from inside the container -- `sandlot list` shows four status icons: idle (dim `◌`), active (cyan `◯`), dirty/unsaved (yellow `◎`), saved (green `●`) +- `sandlot list` shows five status icons: idle (dim `◯`), active (cyan `◎`), dirty/unsaved (yellow `◐`), saved (green `●`), review (magenta `⦿`) +- `sandlot review` sets `in_review` on the session during the review and clears it in a `finally` block on exit; `list` only shows review status if Claude is also active (self-heals stale flags) - `sandlot new` and `sandlot open` auto-save changes when Claude exits (disable with `--no-save`) - `sandlot close` has a hidden `rm` alias - Default behavior (no subcommand): always runs `list` (which prints "No active sessions." if empty) diff --git a/src/commands/list.ts b/src/commands/list.ts index 8b09b40..a7dda79 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -46,8 +46,9 @@ export async function action(opts: { json?: boolean }) { // Determine status for each session in parallel const statusEntries = await Promise.all( sessions.map(async (s): Promise<[string, string]> => { - if (s.in_review) return [s.branch, "review"] - if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"] + const active = await vm.isClaudeActive(s.worktree, s.branch) + if (active && s.in_review) return [s.branch, "review"] + if (active) return [s.branch, "active"] const dirty = await git.isDirty(s.worktree) if (dirty) return [s.branch, "dirty"] const commits = await git.hasNewCommits(s.worktree) @@ -62,8 +63,13 @@ export async function action(opts: { json?: boolean }) { return } - const icons: Record = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}`, review: `${magenta}⦿${reset}` } - const branchColors: Record = { idle: dim, active: cyan, dirty: yellow, saved: green, review: magenta } + const styles: Record = { + idle: { icon: `${dim}◯${reset}`, color: dim }, + active: { icon: `${cyan}◎${reset}`, color: cyan }, + dirty: { icon: `${yellow}◐${reset}`, color: yellow }, + saved: { icon: `${green}●${reset}`, color: green }, + review: { icon: `${magenta}⦿${reset}`, color: magenta }, + } const branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length)) const cols = process.stdout.columns || 80 const prefixWidth = branchWidth + 4 @@ -73,8 +79,7 @@ export async function action(opts: { json?: boolean }) { for (const s of sessions) { const prompt = (s.prompt ?? "").split("\n")[0] const status = statuses[s.branch] - const icon = icons[status] - const bc = branchColors[status] + const { icon, color: bc } = styles[status] const maxPrompt = cols - prefixWidth const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) diff --git a/src/commands/review.ts b/src/commands/review.ts index 9e0a862..9b92623 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -1,4 +1,3 @@ -import * as git from "../git.ts" import * as vm from "../vm.ts" import * as state from "../state.ts" import { spinner } from "../spinner.ts" @@ -7,13 +6,13 @@ import { requireSession, saveChanges } from "./helpers.ts" export async function action(branch: string, extra: string | undefined, opts: { print?: boolean }) { const { root, session } = await requireSession(branch) - // Mark session as in review - session.in_review = true - await state.setSession(root, session) - const spin = spinner("Starting container", branch) await vm.ensure((msg) => { spin.text = msg }) + // Mark session as in review only after container is confirmed running + session.in_review = true + await state.setSession(root, session) + let prompt = ` You're a grumpy old senior software engineer. You need to review some code my co-worker wrote. @@ -86,8 +85,9 @@ Your thoughts, in brief. await saveChanges(session.worktree, session.branch) } finally { - // Clear review state + // Clear review state — use .catch() per error handling conventions + // so a disk error here doesn't mask the original exception session.in_review = false - await state.setSession(root, session) + await state.setSession(root, session).catch(() => {}) } }