Track Claude activity state via hook-based marker files

This commit is contained in:
Chris Wanstrath 2026-02-19 18:46:51 -08:00
parent 0d059b8940
commit 86fd1e6c25
2 changed files with 40 additions and 4 deletions

View File

@ -233,10 +233,9 @@ program
} }
// Determine status for each session in parallel // Determine status for each session in parallel
const activeWorktrees = await vm.activeWorktrees()
const statusEntries = await Promise.all( const statusEntries = await Promise.all(
sessions.map(async (s): Promise<[string, string]> => { sessions.map(async (s): Promise<[string, string]> => {
if (activeWorktrees.includes(s.worktree)) return [s.branch, "active"] if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"]
const dirty = await git.isDirty(s.worktree) const dirty = await git.isDirty(s.worktree)
if (dirty) return [s.branch, "dirty"] if (dirty) return [s.branch, "dirty"]
const commits = await git.hasNewCommits(s.worktree) const commits = await git.hasNewCommits(s.worktree)
@ -362,6 +361,8 @@ const closeAction = async (branch: string) => {
const session = await state.getSession(root, branch) const session = await state.getSession(root, branch)
const worktreeAbs = session?.worktree ?? join(homedir(), '.sandlot', basename(root), branch) const worktreeAbs = session?.worktree ?? join(homedir(), '.sandlot', basename(root), branch)
await vm.clearActivity(worktreeAbs, branch)
await git.removeWorktree(worktreeAbs, root) await git.removeWorktree(worktreeAbs, root)
.catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`)) .catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`))

View File

@ -1,5 +1,6 @@
import { $ } from "bun" import { $ } from "bun"
import { homedir } from "os" import { homedir } from "os"
import { dirname } from "path"
const CONTAINER_NAME = "sandlot" const CONTAINER_NAME = "sandlot"
const USER = "ubuntu" const USER = "ubuntu"
@ -73,9 +74,13 @@ export async function ensure(log?: (msg: string) => void): Promise<void> {
log?.("Warning: ANTHROPIC_API_KEY not found in ~/.env — claude will require manual login") log?.("Warning: ANTHROPIC_API_KEY not found in ~/.env — claude will require manual login")
} }
const hooks = {
UserPromptSubmit: [{ hooks: [{ type: "command", command: "sandlot-activity active" }] }],
Stop: [{ hooks: [{ type: "command", command: "sandlot-activity idle" }] }],
}
const settingsJson = JSON.stringify(apiKey const settingsJson = JSON.stringify(apiKey
? { apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true } ? { apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks }
: { skipDangerousModePermissionPrompt: true }) : { skipDangerousModePermissionPrompt: true, hooks })
const claudeJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true }) const claudeJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true })
// Write the helper script to a temp file and copy it in so the key // Write the helper script to a temp file and copy it in so the key
@ -88,6 +93,19 @@ export async function ensure(log?: (msg: string) => void): Promise<void> {
await Bun.file(tmp).unlink() await Bun.file(tmp).unlink()
} }
// Install activity tracking hook script
const activityTmp = `${home}/.sandlot/.sandlot-activity.tmp`
await Bun.write(activityTmp, [
'#!/bin/bash',
'DIR=$(dirname "$CLAUDE_PROJECT_DIR")',
'BRANCH=$(basename "$CLAUDE_PROJECT_DIR")',
'echo "$1" > "$DIR/.activity-$BRANCH"',
'',
].join('\n'))
await $`chmod +x ${activityTmp}`.quiet()
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp /sandlot/.sandlot-activity.tmp ~/.local/bin/sandlot-activity"}`.quiet()
await Bun.file(activityTmp).unlink()
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${` await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${`
mkdir -p ~/.claude mkdir -p ~/.claude
echo '${settingsJson}' > ~/.claude/settings.json echo '${settingsJson}' > ~/.claude/settings.json
@ -178,6 +196,23 @@ export async function activeWorktrees(): Promise<string[]> {
}) })
} }
/** Check if Claude is actively working in the given worktree (based on activity hook). */
export async function isClaudeActive(worktree: string, branch: string): Promise<boolean> {
const file = `${dirname(worktree)}/.activity-${branch}`
try {
const content = await Bun.file(file).text()
return content.trim() === "active"
} catch {
return false
}
}
/** Remove the activity marker file for a worktree. */
export async function clearActivity(worktree: string, branch: string): Promise<void> {
const file = `${dirname(worktree)}/.activity-${branch}`
await Bun.file(file).unlink().catch(() => {})
}
/** 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()