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 <noreply@anthropic.com>
This commit is contained in:
parent
92dbad3cad
commit
bc102e416c
|
|
@ -120,7 +120,8 @@ Always use `.nothrow()` for commands that may fail non-fatally. Use `.quiet()` t
|
||||||
"branch": "branch-name",
|
"branch": "branch-name",
|
||||||
"worktree": "/Users/you/.sandlot/repo/branch-name",
|
"worktree": "/Users/you/.sandlot/repo/branch-name",
|
||||||
"created_at": "2026-02-16T10:30:00Z",
|
"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 <branch>"`
|
- `sandlot squash` generates an AI commit message for the squash commit via `claudePipe()`; falls back to `"squash <branch>"`
|
||||||
- `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 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` 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 new` and `sandlot open` auto-save changes when Claude exits (disable with `--no-save`)
|
||||||
- `sandlot close` has a hidden `rm` alias
|
- `sandlot close` has a hidden `rm` alias
|
||||||
- Default behavior (no subcommand): always runs `list` (which prints "No active sessions." if empty)
|
- Default behavior (no subcommand): always runs `list` (which prints "No active sessions." if empty)
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,9 @@ export async function action(opts: { json?: boolean }) {
|
||||||
// Determine status for each session in parallel
|
// Determine status for each session in parallel
|
||||||
const statusEntries = await Promise.all(
|
const statusEntries = await Promise.all(
|
||||||
sessions.map(async (s): Promise<[string, string]> => {
|
sessions.map(async (s): Promise<[string, string]> => {
|
||||||
if (s.in_review) return [s.branch, "review"]
|
const active = await vm.isClaudeActive(s.worktree, s.branch)
|
||||||
if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"]
|
if (active && s.in_review) return [s.branch, "review"]
|
||||||
|
if (active) return [s.branch, "active"]
|
||||||
const dirty = await git.isDirty(s.worktree)
|
const dirty = await git.isDirty(s.worktree)
|
||||||
if (dirty) return [s.branch, "dirty"]
|
if (dirty) return [s.branch, "dirty"]
|
||||||
const commits = await git.hasNewCommits(s.worktree)
|
const commits = await git.hasNewCommits(s.worktree)
|
||||||
|
|
@ -62,8 +63,13 @@ export async function action(opts: { json?: boolean }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const icons: Record<string, string> = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}`, review: `${magenta}⦿${reset}` }
|
const styles: Record<string, { icon: string; color: string }> = {
|
||||||
const branchColors: Record<string, string> = { idle: dim, active: cyan, dirty: yellow, saved: green, review: magenta }
|
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 branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length))
|
||||||
const cols = process.stdout.columns || 80
|
const cols = process.stdout.columns || 80
|
||||||
const prefixWidth = branchWidth + 4
|
const prefixWidth = branchWidth + 4
|
||||||
|
|
@ -73,8 +79,7 @@ export async function action(opts: { json?: boolean }) {
|
||||||
for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
const prompt = (s.prompt ?? "").split("\n")[0]
|
const prompt = (s.prompt ?? "").split("\n")[0]
|
||||||
const status = statuses[s.branch]
|
const status = statuses[s.branch]
|
||||||
const icon = icons[status]
|
const { icon, color: bc } = styles[status]
|
||||||
const bc = branchColors[status]
|
|
||||||
const maxPrompt = cols - prefixWidth
|
const maxPrompt = cols - prefixWidth
|
||||||
const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt
|
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}`)
|
console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import * as git from "../git.ts"
|
|
||||||
import * as vm from "../vm.ts"
|
import * as vm from "../vm.ts"
|
||||||
import * as state from "../state.ts"
|
import * as state from "../state.ts"
|
||||||
import { spinner } from "../spinner.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 }) {
|
export async function action(branch: string, extra: string | undefined, opts: { print?: boolean }) {
|
||||||
const { root, session } = await requireSession(branch)
|
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)
|
const spin = spinner("Starting container", branch)
|
||||||
await vm.ensure((msg) => { spin.text = msg })
|
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 = `
|
let prompt = `
|
||||||
You're a grumpy old senior software engineer. You need to review some code my co-worker wrote.
|
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)
|
await saveChanges(session.worktree, session.branch)
|
||||||
} finally {
|
} 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
|
session.in_review = false
|
||||||
await state.setSession(root, session)
|
await state.setSession(root, session).catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user