From d402a3f980e4fc8d74677a76b140e90792519356 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 23 Mar 2026 21:20:44 -0700 Subject: [PATCH] Derive repo name from repoRoot instead of storing it separately Remove the redundant `repo` field from GlobalSession and compute it via basename(repoRoot) at render time. Also fix prompt truncation when terminal is extremely narrow, and simplify backfillPrompts to avoid an intermediate array allocation. --- src/commands/list.ts | 22 +++++++++++----------- src/commands/vm.ts | 9 +++++---- src/state.ts | 3 +-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index 708ab97..0dec953 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -31,7 +31,7 @@ function renderSessions( const status = statusMap.get(s) ?? "idle" const { icon, color: bc } = styles[status] const maxPrompt = cols - prefixWidth - const truncated = prompt.length <= maxPrompt ? prompt : prompt.slice(0, maxPrompt - 3) + "..." + const truncated = maxPrompt <= 3 ? "" : prompt.length <= maxPrompt ? prompt : prompt.slice(0, maxPrompt - 3) + "..." console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) } } @@ -61,9 +61,9 @@ async function resolveStatus( /** Clear in_review flags for sessions where Claude is no longer active. */ async function clearStaleReviews( sessions: state.GlobalSession[], - results: string[], + statusMap: Map, ) { - const stale = sessions.filter((s, i) => s.in_review && results[i] !== "review") + const stale = sessions.filter(s => s.in_review && statusMap.get(s) !== "review") if (stale.length === 0) return const byRepo = Map.groupBy(stale, s => s.repoRoot) for (const [repoRoot, staleSessions] of byRepo) { @@ -82,13 +82,13 @@ async function backfillPrompts(sessions: { worktree: string; prompt?: string }[] const result = await vm.exec(homedir() + "/.sandlot", "cat /home/ubuntu/.claude/history.jsonl 2>/dev/null").catch(() => null) if (!result || result.exitCode !== 0 || !result.stdout) return - const entries = result.stdout.split("\n").filter(Boolean).map(line => { - try { return JSON.parse(line) } catch { return null } - }).filter(Boolean) - const byProject = new Map() - for (const e of entries) { - if (e.project && e.display) byProject.set(e.project, e.display) + for (const line of result.stdout.split("\n")) { + if (!line) continue + try { + const e = JSON.parse(line) + if (e.project && e.display) byProject.set(e.project, e.display) + } catch {} } for (const s of needsPrompt) { @@ -106,7 +106,7 @@ export async function action(opts: { json?: boolean; all?: boolean }) { } else { const root = await git.repoRoot() const st = await state.load(root) - sessions = Object.values(st.sessions).map(s => ({ ...s, repo: basename(root), repoRoot: root })) + sessions = Object.values(st.sessions).map(s => ({ ...s, repoRoot: root })) } const vmRunning = (await vm.status()) === "running" @@ -120,7 +120,7 @@ export async function action(opts: { json?: boolean; all?: boolean }) { const results = await Promise.all(sessions.map(s => resolveStatus(s, vmRunning))) const statusMap = new Map(sessions.map((s, i) => [s, results[i]])) - await clearStaleReviews(sessions, results) + await clearStaleReviews(sessions, statusMap) if (opts.json) { const withStatus = sessions.map(s => ({ ...s, status: statusMap.get(s) ?? "idle" })) diff --git a/src/commands/vm.ts b/src/commands/vm.ts index be1f29a..b1217c6 100644 --- a/src/commands/vm.ts +++ b/src/commands/vm.ts @@ -1,3 +1,4 @@ +import { basename } from "path" import type { Command } from "commander" import * as vm from "../vm.ts" import * as git from "../git.ts" @@ -67,7 +68,7 @@ export function register(program: Command) { // Determine status for each session in parallel const statusEntries = await Promise.all( sessions.map(async (sess): Promise<[string, string]> => { - const key = `${sess.repo}/${sess.branch}` + const key = `${basename(sess.repoRoot)}/${sess.branch}` try { if (await vm.isClaudeActive(sess.worktree, sess.branch)) return [key, "active"] if (await git.isDirty(sess.worktree)) return [key, "dirty"] @@ -81,7 +82,7 @@ export function register(program: Command) { const icons: Record = { idle: `${dim}◯${reset}`, active: `${cyan}◎${reset}`, dirty: `${yellow}◐${reset}`, saved: `${green}●${reset}` } const branchColors: Record = { idle: dim, active: cyan, dirty: yellow, saved: green } - const repoWidth = Math.max(4, ...sessions.map(sess => sess.repo.length)) + const repoWidth = Math.max(4, ...sessions.map(sess => basename(sess.repoRoot).length)) const branchWidth = Math.max(6, ...sessions.map(sess => sess.branch.length)) const cols = process.stdout.columns || 80 const prefixWidth = repoWidth + branchWidth + 6 @@ -90,13 +91,13 @@ export function register(program: Command) { for (const sess of sessions) { const prompt = (sess.prompt ?? "").split("\n")[0] - const key = `${sess.repo}/${sess.branch}` + const key = `${basename(sess.repoRoot)}/${sess.branch}` const status = statuses[key] const icon = icons[status] const bc = branchColors[status] const maxPrompt = cols - prefixWidth const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt - console.log(`${icon} ${dim}${sess.repo.padEnd(repoWidth)}${reset} ${bc}${sess.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) + console.log(`${icon} ${dim}${basename(sess.repoRoot).padEnd(repoWidth)}${reset} ${bc}${sess.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) } console.log(`\n${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}`) diff --git a/src/state.ts b/src/state.ts index 452cd92..fbbbf56 100644 --- a/src/state.ts +++ b/src/state.ts @@ -54,7 +54,6 @@ export async function removeSession(repoRoot: string, branch: string): Promise { try { const st = await load(repoRoot) for (const session of Object.values(st.sessions)) { - all.push({ ...session, repo: entry.name, repoRoot }) + all.push({ ...session, repoRoot }) } } catch {} }