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.
This commit is contained in:
Chris Wanstrath 2026-03-23 21:20:44 -07:00
parent 99b0fc0f12
commit d402a3f980
3 changed files with 17 additions and 17 deletions

View File

@ -31,7 +31,7 @@ function renderSessions(
const status = statusMap.get(s) ?? "idle" const status = statusMap.get(s) ?? "idle"
const { icon, color: bc } = styles[status] const { icon, color: bc } = styles[status]
const maxPrompt = cols - prefixWidth 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}`) 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. */ /** Clear in_review flags for sessions where Claude is no longer active. */
async function clearStaleReviews( async function clearStaleReviews(
sessions: state.GlobalSession[], sessions: state.GlobalSession[],
results: string[], statusMap: Map<state.GlobalSession, string>,
) { ) {
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 if (stale.length === 0) return
const byRepo = Map.groupBy(stale, s => s.repoRoot) const byRepo = Map.groupBy(stale, s => s.repoRoot)
for (const [repoRoot, staleSessions] of byRepo) { 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) 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 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<string, string>() const byProject = new Map<string, string>()
for (const e of entries) { for (const line of result.stdout.split("\n")) {
if (e.project && e.display) byProject.set(e.project, e.display) 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) { for (const s of needsPrompt) {
@ -106,7 +106,7 @@ export async function action(opts: { json?: boolean; all?: boolean }) {
} else { } else {
const root = await git.repoRoot() const root = await git.repoRoot()
const st = await state.load(root) 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" 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 results = await Promise.all(sessions.map(s => resolveStatus(s, vmRunning)))
const statusMap = new Map(sessions.map((s, i) => [s, results[i]])) const statusMap = new Map(sessions.map((s, i) => [s, results[i]]))
await clearStaleReviews(sessions, results) await clearStaleReviews(sessions, statusMap)
if (opts.json) { if (opts.json) {
const withStatus = sessions.map(s => ({ ...s, status: statusMap.get(s) ?? "idle" })) const withStatus = sessions.map(s => ({ ...s, status: statusMap.get(s) ?? "idle" }))

View File

@ -1,3 +1,4 @@
import { basename } from "path"
import type { Command } from "commander" import type { Command } from "commander"
import * as vm from "../vm.ts" import * as vm from "../vm.ts"
import * as git from "../git.ts" import * as git from "../git.ts"
@ -67,7 +68,7 @@ export function register(program: Command) {
// 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 (sess): Promise<[string, string]> => { sessions.map(async (sess): Promise<[string, string]> => {
const key = `${sess.repo}/${sess.branch}` const key = `${basename(sess.repoRoot)}/${sess.branch}`
try { try {
if (await vm.isClaudeActive(sess.worktree, sess.branch)) return [key, "active"] if (await vm.isClaudeActive(sess.worktree, sess.branch)) return [key, "active"]
if (await git.isDirty(sess.worktree)) return [key, "dirty"] if (await git.isDirty(sess.worktree)) return [key, "dirty"]
@ -81,7 +82,7 @@ export function register(program: Command) {
const icons: Record<string, string> = { idle: `${dim}${reset}`, active: `${cyan}${reset}`, dirty: `${yellow}${reset}`, saved: `${green}${reset}` } const icons: Record<string, string> = { idle: `${dim}${reset}`, active: `${cyan}${reset}`, dirty: `${yellow}${reset}`, saved: `${green}${reset}` }
const branchColors: Record<string, string> = { idle: dim, active: cyan, dirty: yellow, saved: green } const branchColors: Record<string, string> = { 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 branchWidth = Math.max(6, ...sessions.map(sess => sess.branch.length))
const cols = process.stdout.columns || 80 const cols = process.stdout.columns || 80
const prefixWidth = repoWidth + branchWidth + 6 const prefixWidth = repoWidth + branchWidth + 6
@ -90,13 +91,13 @@ export function register(program: Command) {
for (const sess of sessions) { for (const sess of sessions) {
const prompt = (sess.prompt ?? "").split("\n")[0] 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 status = statuses[key]
const icon = icons[status] const icon = icons[status]
const bc = branchColors[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} ${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}`) console.log(`\n${dim}◯ idle${reset} · ${cyan}◎ active${reset} · ${yellow}◐ unsaved${reset} · ${green}● saved${reset}`)

View File

@ -54,7 +54,6 @@ export async function removeSession(repoRoot: string, branch: string): Promise<v
} }
export interface GlobalSession extends Session { export interface GlobalSession extends Session {
repo: string
repoRoot: string repoRoot: string
} }
@ -94,7 +93,7 @@ export async function loadAll(): Promise<GlobalSession[]> {
try { try {
const st = await load(repoRoot) const st = await load(repoRoot)
for (const session of Object.values(st.sessions)) { for (const session of Object.values(st.sessions)) {
all.push({ ...session, repo: entry.name, repoRoot }) all.push({ ...session, repoRoot })
} }
} catch {} } catch {}
} }