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
|
program
|
||||||
.command("list")
|
.command("list")
|
||||||
.description("Show all active sessions")
|
.description("Show all active sessions (◌ idle · ◯ working · ◎ unsaved · ● saved)")
|
||||||
.option("--json", "Output as JSON")
|
.option("--json", "Output as JSON")
|
||||||
.action(async (opts: { json?: boolean }) => {
|
.action(async (opts: { json?: boolean }) => {
|
||||||
const root = await git.repoRoot()
|
const root = await git.repoRoot()
|
||||||
const st = await state.load(root)
|
const st = await state.load(root)
|
||||||
const sessions = Object.values(st.sessions)
|
const sessions = Object.values(st.sessions)
|
||||||
|
|
||||||
if (opts.json) {
|
|
||||||
console.log(JSON.stringify(sessions, null, 2))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
console.log("◆ No active sessions.")
|
if (opts.json) console.log("[]")
|
||||||
|
else console.log("◆ No active sessions.")
|
||||||
return
|
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 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 + 2
|
const prefixWidth = branchWidth + 4
|
||||||
|
|
||||||
console.log(`${"BRANCH".padEnd(branchWidth)} PROMPT`)
|
|
||||||
for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
const prompt = s.prompt ?? ""
|
const prompt = s.prompt ?? ""
|
||||||
|
const icon = icons[statuses[s.branch]]
|
||||||
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(`${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()
|
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). */
|
/** Detect the main branch name (main or master). */
|
||||||
export async function mainBranch(cwd?: string): Promise<string> {
|
export async function mainBranch(cwd?: string): Promise<string> {
|
||||||
const dir = cwd ?? "."
|
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. */
|
/** Stop the container. */
|
||||||
export async function stop(): Promise<void> {
|
export async function stop(): Promise<void> {
|
||||||
await $`container stop ${CONTAINER_NAME}`.nothrow().quiet()
|
await $`container stop ${CONTAINER_NAME}`.nothrow().quiet()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user