Simplify session status tracking with identity-keyed Map
Replace the repoRoot/branch composite-key workaround with a Map keyed directly by session objects. Also tighten the global lock to properly throw on acquisition failure and fix prompt truncation at narrow widths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3ba27c80b4
commit
965f233245
|
|
@ -1,4 +1,4 @@
|
|||
import { basename } from "path"
|
||||
import { basename, resolve } from "path"
|
||||
import { homedir } from "os"
|
||||
import { stat } from "fs/promises"
|
||||
import * as git from "../git.ts"
|
||||
|
|
@ -18,8 +18,7 @@ const styles = Object.fromEntries(
|
|||
|
||||
function renderSessions(
|
||||
sessions: state.GlobalSession[],
|
||||
statuses: Record<string, string>,
|
||||
keyFn: (s: state.GlobalSession) => string,
|
||||
statusMap: Map<state.GlobalSession, string>,
|
||||
) {
|
||||
const branchWidth = Math.max(6, ...sessions.map(s => s.branch.length))
|
||||
const cols = process.stdout.columns || 80
|
||||
|
|
@ -29,10 +28,10 @@ function renderSessions(
|
|||
|
||||
for (const s of sessions) {
|
||||
const prompt = (s.prompt ?? "").split("\n")[0]
|
||||
const status = statuses[keyFn(s)] ?? "idle"
|
||||
const status = statusMap.get(s) ?? "idle"
|
||||
const { icon, color: bc } = styles[status]
|
||||
const maxPrompt = cols - prefixWidth
|
||||
const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt
|
||||
const truncated = maxPrompt < 1 ? "" : maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt
|
||||
console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,19 +67,14 @@ async function clearStaleReviews(
|
|||
sessions: state.GlobalSession[],
|
||||
results: string[],
|
||||
) {
|
||||
const staleByRepo = new Map<string, string[]>()
|
||||
for (let i = 0; i < sessions.length; i++) {
|
||||
if (sessions[i].in_review && results[i] !== "review") {
|
||||
sessions[i].in_review = false
|
||||
const arr = staleByRepo.get(sessions[i].repoRoot) ?? []
|
||||
arr.push(sessions[i].branch)
|
||||
staleByRepo.set(sessions[i].repoRoot, arr)
|
||||
}
|
||||
}
|
||||
for (const [repoRoot, branches] of staleByRepo) {
|
||||
const stale = sessions.filter((s, i) => s.in_review && results[i] !== "review")
|
||||
if (stale.length === 0) return
|
||||
for (const s of stale) s.in_review = false
|
||||
const byRepo = Map.groupBy(stale, s => s.repoRoot)
|
||||
for (const [repoRoot, staleSessions] of byRepo) {
|
||||
const fresh = await state.load(repoRoot)
|
||||
for (const branch of branches) {
|
||||
if (fresh.sessions[branch]) fresh.sessions[branch].in_review = false
|
||||
for (const s of staleSessions) {
|
||||
if (fresh.sessions[s.branch]) fresh.sessions[s.branch].in_review = false
|
||||
}
|
||||
await state.save(repoRoot, fresh).catch(() => {})
|
||||
}
|
||||
|
|
@ -133,14 +127,12 @@ export async function action(opts: { json?: boolean; all?: boolean; add?: string
|
|||
}
|
||||
await backfillPrompts(sessions, vmRunning)
|
||||
|
||||
// Key by repoRoot/branch to avoid collisions across repos with the same basename
|
||||
const keyFn = (s: state.GlobalSession) => `${s.repoRoot}/${s.branch}`
|
||||
const results = await Promise.all(sessions.map(s => resolveStatus(s, vmRunning)))
|
||||
const statuses = Object.fromEntries(sessions.map((s, i) => [keyFn(s), results[i]]))
|
||||
const statusMap = new Map(sessions.map((s, i) => [s, results[i]]))
|
||||
await clearStaleReviews(sessions, results)
|
||||
|
||||
if (opts.json) {
|
||||
const withStatus = sessions.map(s => ({ ...s, status: statuses[keyFn(s)] }))
|
||||
const withStatus = sessions.map(s => ({ ...s, status: statusMap.get(s) ?? "idle" }))
|
||||
console.log(JSON.stringify(withStatus, null, 2))
|
||||
return
|
||||
}
|
||||
|
|
@ -149,10 +141,10 @@ export async function action(opts: { json?: boolean; all?: boolean; add?: string
|
|||
const byRepo = Map.groupBy(sessions, s => s.repoRoot)
|
||||
for (const [repoRoot, repoSessions] of byRepo) {
|
||||
console.log(`\n${dim}── ${reset}${basename(repoRoot)}${dim} ──${reset}`)
|
||||
renderSessions(repoSessions, statuses, keyFn)
|
||||
renderSessions(repoSessions, statusMap)
|
||||
}
|
||||
} else {
|
||||
renderSessions(sessions, statuses, keyFn)
|
||||
renderSessions(sessions, statusMap)
|
||||
}
|
||||
|
||||
renderLegend()
|
||||
|
|
@ -164,7 +156,7 @@ async function actionAdd(dir: string) {
|
|||
try {
|
||||
registered = await state.scanAndRegister(dir)
|
||||
} catch (e) {
|
||||
die(e instanceof Error ? e.message : `Failed to scan ${dir}`)
|
||||
return die(e instanceof Error ? e.message : `Failed to scan ${dir}`)
|
||||
}
|
||||
if (registered.length === 0) {
|
||||
console.log(`No sandlot projects found under ${dir}`)
|
||||
|
|
@ -177,12 +169,12 @@ async function actionAdd(dir: string) {
|
|||
}
|
||||
|
||||
async function actionRemove(dir: string) {
|
||||
const resolved = state.normalizePath(dir)
|
||||
const resolved = resolve(dir.replace(/^~(?=\/|$)/, homedir()))
|
||||
let removed = false
|
||||
try {
|
||||
removed = await state.unregisterProject(dir)
|
||||
} catch {
|
||||
die("Could not acquire registry lock")
|
||||
return die("Could not acquire registry lock")
|
||||
}
|
||||
if (removed) {
|
||||
console.log(`${red}-${reset} ${resolved}`)
|
||||
|
|
|
|||
13
src/state.ts
13
src/state.ts
|
|
@ -66,9 +66,11 @@ const GLOBAL_STATE_PATH = join(GLOBAL_DIR, "registry.json")
|
|||
async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const lockPath = join(GLOBAL_DIR, "registry.lock")
|
||||
await mkdir(GLOBAL_DIR, { recursive: true })
|
||||
let acquired = false
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try {
|
||||
await mkdir(lockPath)
|
||||
acquired = true
|
||||
break
|
||||
} catch {
|
||||
// If the lock is older than 5 minutes, assume it's stale (crashed process)
|
||||
|
|
@ -84,6 +86,7 @@ async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
|||
await Bun.sleep(50)
|
||||
}
|
||||
}
|
||||
if (!acquired) throw new Error("Could not acquire registry lock")
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
|
|
@ -97,7 +100,7 @@ async function loadGlobal(): Promise<GlobalState> {
|
|||
try {
|
||||
const data = await file.json()
|
||||
if (data && Array.isArray(data.projects)) {
|
||||
return { projects: data.projects.filter((p: unknown) => typeof p === "string") }
|
||||
return { projects: data.projects as string[] }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
|
@ -110,7 +113,7 @@ async function saveGlobal(gs: GlobalState): Promise<void> {
|
|||
await rename(tmpPath, GLOBAL_STATE_PATH)
|
||||
}
|
||||
|
||||
export function normalizePath(dir: string): string {
|
||||
function normalizePath(dir: string): string {
|
||||
return resolve(dir.replace(/^~(?=\/|$)/, homedir()))
|
||||
}
|
||||
|
||||
|
|
@ -204,10 +207,8 @@ export async function loadAll(): Promise<GlobalSession[]> {
|
|||
return
|
||||
}
|
||||
if (Object.keys(st.sessions).length === 0) {
|
||||
if (!(await Bun.file(join(project, ".sandlot", "state.json")).exists())) {
|
||||
stale.push(project)
|
||||
return
|
||||
}
|
||||
try { await stat(join(project, ".sandlot")) } catch { stale.push(project) }
|
||||
return
|
||||
}
|
||||
const repo = basename(project)
|
||||
for (const s of Object.values(st.sessions)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user