add status icons to list command showing session activity state
This commit is contained in:
parent
25d2de5348
commit
c76340777e
36
src/cli.ts
36
src/cli.ts
|
|
@ -153,33 +153,49 @@ program
|
|||
|
||||
program
|
||||
.command("list")
|
||||
.description("Show all active sessions")
|
||||
.description("Show all active sessions (◌ idle · ◯ working · ◎ unsaved · ● saved)")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
const root = await git.repoRoot()
|
||||
const st = await state.load(root)
|
||||
const sessions = Object.values(st.sessions)
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(sessions, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.log("◆ No active sessions.")
|
||||
if (opts.json) console.log("[]")
|
||||
else console.log("◆ No active sessions.")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine status for each session in parallel
|
||||
const activeWorktrees = await vm.activeWorktrees()
|
||||
const statusEntries = await Promise.all(
|
||||
sessions.map(async (s): Promise<[string, string]> => {
|
||||
if (activeWorktrees.includes(s.worktree)) return [s.branch, "active"]
|
||||
const dirty = await git.isDirty(s.worktree)
|
||||
if (dirty) return [s.branch, "dirty"]
|
||||
const commits = await git.hasNewCommits(s.worktree)
|
||||
return [s.branch, commits ? "saved" : "idle"]
|
||||
})
|
||||
)
|
||||
const statuses = Object.fromEntries(statusEntries)
|
||||
|
||||
if (opts.json) {
|
||||
const withStatus = sessions.map(s => ({ ...s, status: statuses[s.branch] }))
|
||||
console.log(JSON.stringify(withStatus, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
const icons: Record<string, string> = { idle: "◌", active: "◯", dirty: "◎", saved: "●" }
|
||||
const branchWidth = Math.max(6, ...sessions.map((s) => s.branch.length))
|
||||
const cols = process.stdout.columns || 80
|
||||
const prefixWidth = branchWidth + 2
|
||||
const prefixWidth = branchWidth + 4
|
||||
|
||||
console.log(`${"BRANCH".padEnd(branchWidth)} PROMPT`)
|
||||
for (const s of sessions) {
|
||||
const prompt = s.prompt ?? ""
|
||||
const icon = icons[statuses[s.branch]]
|
||||
const maxPrompt = cols - prefixWidth
|
||||
const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt
|
||||
console.log(`${s.branch.padEnd(branchWidth)} ${truncated}`)
|
||||
console.log(`${icon} ${s.branch.padEnd(branchWidth)} ${truncated}`)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
15
src/git.ts
15
src/git.ts
|
|
@ -113,6 +113,21 @@ export async function abortMerge(cwd: string): Promise<void> {
|
|||
await $`git merge --abort`.cwd(cwd).nothrow().quiet()
|
||||
}
|
||||
|
||||
/** Check if a worktree has uncommitted changes. */
|
||||
export async function isDirty(worktreePath: string): Promise<boolean> {
|
||||
const result = await $`git -C ${worktreePath} status --porcelain`.nothrow().quiet()
|
||||
if (result.exitCode !== 0) return false
|
||||
return result.text().trim().length > 0
|
||||
}
|
||||
|
||||
/** Check if a branch has commits beyond main. */
|
||||
export async function hasNewCommits(worktreePath: string): Promise<boolean> {
|
||||
const main = await mainBranch(worktreePath)
|
||||
const result = await $`git -C ${worktreePath} rev-list ${main}..HEAD --count`.nothrow().quiet()
|
||||
if (result.exitCode !== 0) return false
|
||||
return parseInt(result.text().trim(), 10) > 0
|
||||
}
|
||||
|
||||
/** Detect the main branch name (main or master). */
|
||||
export async function mainBranch(cwd?: string): Promise<string> {
|
||||
const dir = cwd ?? "."
|
||||
|
|
|
|||
15
src/vm.ts
15
src/vm.ts
|
|
@ -160,6 +160,21 @@ export async function exec(workdir: string, command: string): Promise<{ exitCode
|
|||
}
|
||||
}
|
||||
|
||||
/** Get host paths of worktrees where claude is currently running in the container. */
|
||||
export async function activeWorktrees(): Promise<string[]> {
|
||||
const s = await status()
|
||||
if (s !== "running") return []
|
||||
|
||||
const result = await $`container exec ${CONTAINER_NAME} bash -c ${'for pid in $(pgrep -x claude 2>/dev/null); do readlink /proc/$pid/cwd 2>/dev/null; done'}`.nothrow().quiet().text()
|
||||
|
||||
const home = homedir()
|
||||
return result.trim().split("\n").filter(Boolean).map(p => {
|
||||
if (p.startsWith("/sandlot")) return `${home}/.sandlot${p.slice("/sandlot".length)}`
|
||||
if (p.startsWith("/host")) return `${home}/dev${p.slice("/host".length)}`
|
||||
return p
|
||||
})
|
||||
}
|
||||
|
||||
/** Stop the container. */
|
||||
export async function stop(): Promise<void> {
|
||||
await $`container stop ${CONTAINER_NAME}`.nothrow().quiet()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user