Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.8 kB · 221 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222import { type FSWatcher, watch } from 'fs'import { useEffect, useRef } from 'react'import { logForDebugging } from '../utils/debug.js'import { claimTask, DEFAULT_TASKS_MODE_TASK_LIST_ID, ensureTasksDir, getTasksDir, listTasks, type Task, updateTask,} from '../utils/tasks.js'
const DEBOUNCE_MS = 1000
type Props = { /** When undefined, the hook does nothing. The task list id is also used as the agent ID. */ taskListId?: string isLoading: boolean /** * Called when a task is ready to be worked on. * Returns true if submission succeeded, false if rejected. */ onSubmitTask: (prompt: string) => boolean}
/** * Hook that watches a task list directory and automatically picks up * open, unowned tasks to work on. * * This enables "tasks mode" where Claude watches for externally-created * tasks and processes them one at a time. */export function useTaskListWatcher({ taskListId, isLoading, onSubmitTask,}: Props): void { const currentTaskRef = useRef<string | null>(null) const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Stabilize unstable props via refs so the watcher effect doesn't depend on // them. isLoading flips every turn, and onSubmitTask's identity changes // whenever onQuery's deps change. Without this, the watcher effect re-runs // on every turn, calling watcher.close() + watch() each time — which is a // trigger for Bun's PathWatcherManager deadlock (oven-sh/bun#27469). const isLoadingRef = useRef(isLoading) isLoadingRef.current = isLoading const onSubmitTaskRef = useRef(onSubmitTask) onSubmitTaskRef.current = onSubmitTask
const enabled = taskListId !== undefined const agentId = taskListId ?? DEFAULT_TASKS_MODE_TASK_LIST_ID
// checkForTasks reads isLoading and onSubmitTask from refs — always // up-to-date, no stale closure, and doesn't force a new function identity // per render. Stored in a ref so the watcher effect can call it without // depending on it. const checkForTasksRef = useRef<() => Promise<void>>(async () => {}) checkForTasksRef.current = async () => { if (!enabled) { return }
// Don't need to submit new tasks if we are already working if (isLoadingRef.current) { return }
const tasks = await listTasks(taskListId)
// If we have a current task, check if it's been resolved if (currentTaskRef.current !== null) { const currentTask = tasks.find(t => t.id === currentTaskRef.current) if (!currentTask || currentTask.status === 'completed') { logForDebugging( `[TaskListWatcher] Task #${currentTaskRef.current} is marked complete, ready for next task`, ) currentTaskRef.current = null } else { // Still working on current task return } }
// Find an open task with no owner that isn't blocked const availableTask = findAvailableTask(tasks)
if (!availableTask) { return }
logForDebugging( `[TaskListWatcher] Found available task #${availableTask.id}: ${availableTask.subject}`, )
// Claim the task using the task list's agent ID const result = await claimTask(taskListId, availableTask.id, agentId)
if (!result.success) { logForDebugging( `[TaskListWatcher] Failed to claim task #${availableTask.id}: ${result.reason}`, ) return }
currentTaskRef.current = availableTask.id
// Format the task as a prompt const prompt = formatTaskAsPrompt(availableTask)
logForDebugging( `[TaskListWatcher] Submitting task #${availableTask.id} as prompt`, )
const submitted = onSubmitTaskRef.current(prompt) if (!submitted) { logForDebugging( `[TaskListWatcher] Failed to submit task #${availableTask.id}, releasing claim`, ) // Release the claim await updateTask(taskListId, availableTask.id, { owner: undefined }) currentTaskRef.current = null } }
// -- Watcher setup
// Schedules a check after DEBOUNCE_MS, collapsing rapid fs events. // Shared between the watcher callback and the idle-trigger effect below. const scheduleCheckRef = useRef<() => void>(() => {})
useEffect(() => { if (!enabled) return
void ensureTasksDir(taskListId) const tasksDir = getTasksDir(taskListId)
let watcher: FSWatcher | null = null
const debouncedCheck = (): void => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current) } debounceTimerRef.current = setTimeout( ref => void ref.current(), DEBOUNCE_MS, checkForTasksRef, ) } scheduleCheckRef.current = debouncedCheck
try { watcher = watch(tasksDir, debouncedCheck) watcher.unref() logForDebugging(`[TaskListWatcher] Watching for tasks in ${tasksDir}`) } catch (error) { // fs.watch throws synchronously on ENOENT — ensureTasksDir should have // created the dir, but handle the race gracefully logForDebugging(`[TaskListWatcher] Failed to watch ${tasksDir}: ${error}`) }
// Initial check debouncedCheck()
return () => { // This cleanup only fires when taskListId changes or on unmount — // never per-turn. That keeps watcher.close() out of the Bun // PathWatcherManager deadlock window. scheduleCheckRef.current = () => {} if (watcher) { watcher.close() } if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current) } } }, [enabled, taskListId])
// Previously, the watcher effect depended on checkForTasks (and transitively // isLoading), so going idle triggered a re-setup whose initial debouncedCheck // would pick up the next task. Preserve that behavior explicitly: when // isLoading drops, schedule a check. useEffect(() => { if (!enabled) return if (isLoading) return scheduleCheckRef.current() }, [enabled, isLoading])}
/** * Find an available task that can be worked on: * - Status is 'pending' * - No owner assigned * - Not blocked by any unresolved tasks */function findAvailableTask(tasks: Task[]): Task | undefined { const unresolvedTaskIds = new Set( tasks.filter(t => t.status !== 'completed').map(t => t.id), )
return tasks.find(task => { if (task.status !== 'pending') return false if (task.owner) return false // Check all blockers are completed return task.blockedBy.every(id => !unresolvedTaskIds.has(id)) })}
/** * Format a task as a prompt for Claude to work on. */function formatTaskAsPrompt(task: Task): string { let prompt = `Complete all open tasks. Start with task #${task.id}: \n\n ${task.subject}`
if (task.description) { prompt += `\n\n${task.description}` }
return prompt}