mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)
纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,83 +1,119 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import { stat } from 'fs/promises';
|
||||
import { OUTPUT_FILE_TAG, STATUS_TAG, SUMMARY_TAG, TASK_ID_TAG, TASK_NOTIFICATION_TAG, TOOL_USE_ID_TAG } from '../../constants/xml.js';
|
||||
import { abortSpeculation } from '../../services/PromptSuggestion/speculation.js';
|
||||
import type { AppState } from '../../state/AppState.js';
|
||||
import type { LocalShellSpawnInput, SetAppState, Task, TaskContext, TaskHandle } from '../../Task.js';
|
||||
import { createTaskStateBase } from '../../Task.js';
|
||||
import type { AgentId } from '../../types/ids.js';
|
||||
import { registerCleanup } from '../../utils/cleanupRegistry.js';
|
||||
import { tailFile } from '../../utils/fsOperations.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { enqueuePendingNotification } from '../../utils/messageQueueManager.js';
|
||||
import type { ShellCommand } from '../../utils/ShellCommand.js';
|
||||
import { evictTaskOutput, getTaskOutputPath } from '../../utils/task/diskOutput.js';
|
||||
import { registerTask, updateTaskState } from '../../utils/task/framework.js';
|
||||
import { escapeXml } from '../../utils/xml.js';
|
||||
import { backgroundAgentTask, isLocalAgentTask } from '../LocalAgentTask/LocalAgentTask.js';
|
||||
import { isMainSessionTask } from '../LocalMainSessionTask.js';
|
||||
import { type BashTaskKind, isLocalShellTask, type LocalShellTaskState } from './guards.js';
|
||||
import { killTask } from './killShellTasks.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import { stat } from 'fs/promises'
|
||||
import {
|
||||
OUTPUT_FILE_TAG,
|
||||
STATUS_TAG,
|
||||
SUMMARY_TAG,
|
||||
TASK_ID_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
TOOL_USE_ID_TAG,
|
||||
} from '../../constants/xml.js'
|
||||
import { abortSpeculation } from '../../services/PromptSuggestion/speculation.js'
|
||||
import type { AppState } from '../../state/AppState.js'
|
||||
import type {
|
||||
LocalShellSpawnInput,
|
||||
SetAppState,
|
||||
Task,
|
||||
TaskContext,
|
||||
TaskHandle,
|
||||
} from '../../Task.js'
|
||||
import { createTaskStateBase } from '../../Task.js'
|
||||
import type { AgentId } from '../../types/ids.js'
|
||||
import { registerCleanup } from '../../utils/cleanupRegistry.js'
|
||||
import { tailFile } from '../../utils/fsOperations.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { enqueuePendingNotification } from '../../utils/messageQueueManager.js'
|
||||
import type { ShellCommand } from '../../utils/ShellCommand.js'
|
||||
import {
|
||||
evictTaskOutput,
|
||||
getTaskOutputPath,
|
||||
} from '../../utils/task/diskOutput.js'
|
||||
import { registerTask, updateTaskState } from '../../utils/task/framework.js'
|
||||
import { escapeXml } from '../../utils/xml.js'
|
||||
import {
|
||||
backgroundAgentTask,
|
||||
isLocalAgentTask,
|
||||
} from '../LocalAgentTask/LocalAgentTask.js'
|
||||
import { isMainSessionTask } from '../LocalMainSessionTask.js'
|
||||
import {
|
||||
type BashTaskKind,
|
||||
isLocalShellTask,
|
||||
type LocalShellTaskState,
|
||||
} from './guards.js'
|
||||
import { killTask } from './killShellTasks.js'
|
||||
|
||||
/** Prefix that identifies a LocalShellTask summary to the UI collapse transform. */
|
||||
export const BACKGROUND_BASH_SUMMARY_PREFIX = 'Background command ';
|
||||
const STALL_CHECK_INTERVAL_MS = 5_000;
|
||||
const STALL_THRESHOLD_MS = 45_000;
|
||||
const STALL_TAIL_BYTES = 1024;
|
||||
export const BACKGROUND_BASH_SUMMARY_PREFIX = 'Background command '
|
||||
|
||||
const STALL_CHECK_INTERVAL_MS = 5_000
|
||||
const STALL_THRESHOLD_MS = 45_000
|
||||
const STALL_TAIL_BYTES = 1024
|
||||
|
||||
// Last-line patterns that suggest a command is blocked waiting for keyboard
|
||||
// input. Used to gate the stall notification — we stay silent on commands that
|
||||
// are merely slow (git log -S, long builds) and only notify when the tail
|
||||
// looks like an interactive prompt the model can act on. See CC-1175.
|
||||
const PROMPT_PATTERNS = [/\(y\/n\)/i,
|
||||
// (Y/n), (y/N)
|
||||
/\[y\/n\]/i,
|
||||
// [Y/n], [y/N]
|
||||
/\(yes\/no\)/i, /\b(?:Do you|Would you|Shall I|Are you sure|Ready to)\b.*\? *$/i,
|
||||
// directed questions
|
||||
/Press (any key|Enter)/i, /Continue\?/i, /Overwrite\?/i];
|
||||
const PROMPT_PATTERNS = [
|
||||
/\(y\/n\)/i, // (Y/n), (y/N)
|
||||
/\[y\/n\]/i, // [Y/n], [y/N]
|
||||
/\(yes\/no\)/i,
|
||||
/\b(?:Do you|Would you|Shall I|Are you sure|Ready to)\b.*\? *$/i, // directed questions
|
||||
/Press (any key|Enter)/i,
|
||||
/Continue\?/i,
|
||||
/Overwrite\?/i,
|
||||
]
|
||||
|
||||
export function looksLikePrompt(tail: string): boolean {
|
||||
const lastLine = tail.trimEnd().split('\n').pop() ?? '';
|
||||
return PROMPT_PATTERNS.some(p => p.test(lastLine));
|
||||
const lastLine = tail.trimEnd().split('\n').pop() ?? ''
|
||||
return PROMPT_PATTERNS.some(p => p.test(lastLine))
|
||||
}
|
||||
|
||||
// Output-side analog of peekForStdinData (utils/process.ts): fire a one-shot
|
||||
// notification if output stops growing and the tail looks like a prompt.
|
||||
function startStallWatchdog(taskId: string, description: string, kind: BashTaskKind | undefined, toolUseId?: string, agentId?: AgentId): () => void {
|
||||
if (kind === 'monitor') return () => {};
|
||||
const outputPath = getTaskOutputPath(taskId);
|
||||
let lastSize = 0;
|
||||
let lastGrowth = Date.now();
|
||||
let cancelled = false;
|
||||
function startStallWatchdog(
|
||||
taskId: string,
|
||||
description: string,
|
||||
kind: BashTaskKind | undefined,
|
||||
toolUseId?: string,
|
||||
agentId?: AgentId,
|
||||
): () => void {
|
||||
if (kind === 'monitor') return () => {}
|
||||
const outputPath = getTaskOutputPath(taskId)
|
||||
let lastSize = 0
|
||||
let lastGrowth = Date.now()
|
||||
let cancelled = false
|
||||
|
||||
const timer = setInterval(() => {
|
||||
void stat(outputPath).then(s => {
|
||||
if (s.size > lastSize) {
|
||||
lastSize = s.size;
|
||||
lastGrowth = Date.now();
|
||||
return;
|
||||
}
|
||||
if (Date.now() - lastGrowth < STALL_THRESHOLD_MS) return;
|
||||
void tailFile(outputPath, STALL_TAIL_BYTES).then(({
|
||||
content
|
||||
}) => {
|
||||
if (cancelled) return;
|
||||
if (!looksLikePrompt(content)) {
|
||||
// Not a prompt — keep watching. Reset so the next check is
|
||||
// 45s out instead of re-reading the tail on every tick.
|
||||
lastGrowth = Date.now();
|
||||
return;
|
||||
void stat(outputPath).then(
|
||||
s => {
|
||||
if (s.size > lastSize) {
|
||||
lastSize = s.size
|
||||
lastGrowth = Date.now()
|
||||
return
|
||||
}
|
||||
// Latch before the async-boundary-visible side effects so an
|
||||
// overlapping tick's callback sees cancelled=true and bails.
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
const toolUseIdLine = toolUseId ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>` : '';
|
||||
const summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" appears to be waiting for interactive input`;
|
||||
// No <status> tag — print.ts treats <status> as a terminal
|
||||
// signal and an unknown value falls through to 'completed',
|
||||
// falsely closing the task for SDK consumers. Statusless
|
||||
// notifications are skipped by the SDK emitter (progress ping).
|
||||
const message = `<${TASK_NOTIFICATION_TAG}>
|
||||
if (Date.now() - lastGrowth < STALL_THRESHOLD_MS) return
|
||||
void tailFile(outputPath, STALL_TAIL_BYTES).then(
|
||||
({ content }) => {
|
||||
if (cancelled) return
|
||||
if (!looksLikePrompt(content)) {
|
||||
// Not a prompt — keep watching. Reset so the next check is
|
||||
// 45s out instead of re-reading the tail on every tick.
|
||||
lastGrowth = Date.now()
|
||||
return
|
||||
}
|
||||
// Latch before the async-boundary-visible side effects so an
|
||||
// overlapping tick's callback sees cancelled=true and bails.
|
||||
cancelled = true
|
||||
clearInterval(timer)
|
||||
const toolUseIdLine = toolUseId
|
||||
? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>`
|
||||
: ''
|
||||
const summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" appears to be waiting for interactive input`
|
||||
// No <status> tag — print.ts treats <status> as a terminal
|
||||
// signal and an unknown value falls through to 'completed',
|
||||
// falsely closing the task for SDK consumers. Statusless
|
||||
// notifications are skipped by the SDK emitter (progress ping).
|
||||
const message = `<${TASK_NOTIFICATION_TAG}>
|
||||
<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine}
|
||||
<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
|
||||
<${SUMMARY_TAG}>${escapeXml(summary)}</${SUMMARY_TAG}>
|
||||
@@ -85,47 +121,60 @@ function startStallWatchdog(taskId: string, description: string, kind: BashTaskK
|
||||
Last output:
|
||||
${content.trimEnd()}
|
||||
|
||||
The command is likely blocked on an interactive prompt. Kill this task and re-run with piped input (e.g., \`echo y | command\`) or a non-interactive flag if one exists.`;
|
||||
enqueuePendingNotification({
|
||||
value: message,
|
||||
mode: 'task-notification',
|
||||
priority: 'next',
|
||||
agentId
|
||||
});
|
||||
}, () => {});
|
||||
}, () => {} // File may not exist yet
|
||||
);
|
||||
}, STALL_CHECK_INTERVAL_MS);
|
||||
timer.unref();
|
||||
The command is likely blocked on an interactive prompt. Kill this task and re-run with piped input (e.g., \`echo y | command\`) or a non-interactive flag if one exists.`
|
||||
enqueuePendingNotification({
|
||||
value: message,
|
||||
mode: 'task-notification',
|
||||
priority: 'next',
|
||||
agentId,
|
||||
})
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
},
|
||||
() => {}, // File may not exist yet
|
||||
)
|
||||
}, STALL_CHECK_INTERVAL_MS)
|
||||
timer.unref()
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
};
|
||||
cancelled = true
|
||||
clearInterval(timer)
|
||||
}
|
||||
}
|
||||
function enqueueShellNotification(taskId: string, description: string, status: 'completed' | 'failed' | 'killed', exitCode: number | undefined, setAppState: SetAppState, toolUseId?: string, kind: BashTaskKind = 'bash', agentId?: AgentId): void {
|
||||
|
||||
function enqueueShellNotification(
|
||||
taskId: string,
|
||||
description: string,
|
||||
status: 'completed' | 'failed' | 'killed',
|
||||
exitCode: number | undefined,
|
||||
setAppState: SetAppState,
|
||||
toolUseId?: string,
|
||||
kind: BashTaskKind = 'bash',
|
||||
agentId?: AgentId,
|
||||
): void {
|
||||
// Atomically check and set notified flag to prevent duplicate notifications.
|
||||
// If the task was already marked as notified (e.g., by TaskStopTool), skip
|
||||
// enqueueing to avoid sending redundant messages to the model.
|
||||
let shouldEnqueue = false;
|
||||
updateTaskState<LocalShellTaskState>(taskId, setAppState, task => {
|
||||
let shouldEnqueue = false
|
||||
updateTaskState(taskId, setAppState, task => {
|
||||
if (task.notified) {
|
||||
return task;
|
||||
return task
|
||||
}
|
||||
shouldEnqueue = true;
|
||||
return {
|
||||
...task,
|
||||
notified: true
|
||||
};
|
||||
});
|
||||
shouldEnqueue = true
|
||||
return { ...task, notified: true }
|
||||
})
|
||||
|
||||
if (!shouldEnqueue) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Abort any active speculation — background task state changed, so speculated
|
||||
// results may reference stale task output. The prompt suggestion text is
|
||||
// preserved; only the pre-computed response is discarded.
|
||||
abortSpeculation(setAppState);
|
||||
let summary: string;
|
||||
abortSpeculation(setAppState)
|
||||
|
||||
let summary: string
|
||||
if (feature('MONITOR_TOOL') && kind === 'monitor') {
|
||||
// Monitor is streaming-only (post-#22764) — the script exiting means
|
||||
// the stream ended, not "condition met". Distinct from the bash prefix
|
||||
@@ -133,73 +182,71 @@ function enqueueShellNotification(taskId: string, description: string, status: '
|
||||
// completed" collapse.
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
summary = `Monitor "${description}" stream ended`;
|
||||
break;
|
||||
summary = `Monitor "${description}" stream ended`
|
||||
break
|
||||
case 'failed':
|
||||
summary = `Monitor "${description}" script failed${exitCode !== undefined ? ` (exit ${exitCode})` : ''}`;
|
||||
break;
|
||||
summary = `Monitor "${description}" script failed${exitCode !== undefined ? ` (exit ${exitCode})` : ''}`
|
||||
break
|
||||
case 'killed':
|
||||
summary = `Monitor "${description}" stopped`;
|
||||
break;
|
||||
summary = `Monitor "${description}" stopped`
|
||||
break
|
||||
}
|
||||
} else {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" completed${exitCode !== undefined ? ` (exit code ${exitCode})` : ''}`;
|
||||
break;
|
||||
summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" completed${exitCode !== undefined ? ` (exit code ${exitCode})` : ''}`
|
||||
break
|
||||
case 'failed':
|
||||
summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" failed${exitCode !== undefined ? ` with exit code ${exitCode}` : ''}`;
|
||||
break;
|
||||
summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" failed${exitCode !== undefined ? ` with exit code ${exitCode}` : ''}`
|
||||
break
|
||||
case 'killed':
|
||||
summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" was stopped`;
|
||||
break;
|
||||
summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" was stopped`
|
||||
break
|
||||
}
|
||||
}
|
||||
const outputPath = getTaskOutputPath(taskId);
|
||||
const toolUseIdLine = toolUseId ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>` : '';
|
||||
|
||||
const outputPath = getTaskOutputPath(taskId)
|
||||
const toolUseIdLine = toolUseId
|
||||
? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>`
|
||||
: ''
|
||||
const message = `<${TASK_NOTIFICATION_TAG}>
|
||||
<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine}
|
||||
<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
|
||||
<${STATUS_TAG}>${status}</${STATUS_TAG}>
|
||||
<${SUMMARY_TAG}>${escapeXml(summary)}</${SUMMARY_TAG}>
|
||||
</${TASK_NOTIFICATION_TAG}>`;
|
||||
</${TASK_NOTIFICATION_TAG}>`
|
||||
|
||||
enqueuePendingNotification({
|
||||
value: message,
|
||||
mode: 'task-notification',
|
||||
priority: feature('MONITOR_TOOL') ? 'next' : 'later',
|
||||
agentId
|
||||
});
|
||||
agentId,
|
||||
})
|
||||
}
|
||||
|
||||
export const LocalShellTask: Task = {
|
||||
name: 'LocalShellTask',
|
||||
type: 'local_bash',
|
||||
async kill(taskId, setAppState) {
|
||||
killTask(taskId, setAppState);
|
||||
}
|
||||
};
|
||||
export async function spawnShellTask(input: LocalShellSpawnInput & {
|
||||
shellCommand: ShellCommand;
|
||||
}, context: TaskContext): Promise<TaskHandle> {
|
||||
const {
|
||||
command,
|
||||
description,
|
||||
shellCommand,
|
||||
toolUseId,
|
||||
agentId,
|
||||
kind
|
||||
} = input;
|
||||
const {
|
||||
setAppState
|
||||
} = context;
|
||||
killTask(taskId, setAppState)
|
||||
},
|
||||
}
|
||||
|
||||
export async function spawnShellTask(
|
||||
input: LocalShellSpawnInput & { shellCommand: ShellCommand },
|
||||
context: TaskContext,
|
||||
): Promise<TaskHandle> {
|
||||
const { command, description, shellCommand, toolUseId, agentId, kind } = input
|
||||
const { setAppState } = context
|
||||
|
||||
// TaskOutput owns the data — use its taskId so disk writes are consistent
|
||||
const {
|
||||
taskOutput
|
||||
} = shellCommand;
|
||||
const taskId = taskOutput.taskId;
|
||||
const { taskOutput } = shellCommand
|
||||
const taskId = taskOutput.taskId
|
||||
|
||||
const unregisterCleanup = registerCleanup(async () => {
|
||||
killTask(taskId, setAppState);
|
||||
});
|
||||
killTask(taskId, setAppState)
|
||||
})
|
||||
|
||||
const taskState: LocalShellTaskState = {
|
||||
...createTaskStateBase(taskId, 'local_bash', description, toolUseId),
|
||||
type: 'local_bash',
|
||||
@@ -211,44 +258,64 @@ export async function spawnShellTask(input: LocalShellSpawnInput & {
|
||||
lastReportedTotalLines: 0,
|
||||
isBackgrounded: true,
|
||||
agentId,
|
||||
kind
|
||||
};
|
||||
registerTask(taskState, setAppState);
|
||||
kind,
|
||||
}
|
||||
|
||||
registerTask(taskState, setAppState)
|
||||
|
||||
// Data flows through TaskOutput automatically — no stream listeners needed.
|
||||
// Just transition to backgrounded state so the process keeps running.
|
||||
shellCommand.background(taskId);
|
||||
const cancelStallWatchdog = startStallWatchdog(taskId, description, kind, toolUseId, agentId);
|
||||
shellCommand.background(taskId)
|
||||
|
||||
const cancelStallWatchdog = startStallWatchdog(
|
||||
taskId,
|
||||
description,
|
||||
kind,
|
||||
toolUseId,
|
||||
agentId,
|
||||
)
|
||||
|
||||
void shellCommand.result.then(async result => {
|
||||
cancelStallWatchdog();
|
||||
await flushAndCleanup(shellCommand);
|
||||
let wasKilled = false;
|
||||
cancelStallWatchdog()
|
||||
await flushAndCleanup(shellCommand)
|
||||
let wasKilled = false
|
||||
|
||||
updateTaskState<LocalShellTaskState>(taskId, setAppState, task => {
|
||||
if (task.status === 'killed') {
|
||||
wasKilled = true;
|
||||
return task;
|
||||
wasKilled = true
|
||||
return task
|
||||
}
|
||||
|
||||
return {
|
||||
...task,
|
||||
status: result.code === 0 ? 'completed' : 'failed',
|
||||
result: {
|
||||
code: result.code,
|
||||
interrupted: result.interrupted
|
||||
},
|
||||
result: { code: result.code, interrupted: result.interrupted },
|
||||
shellCommand: null,
|
||||
unregisterCleanup: undefined,
|
||||
endTime: Date.now()
|
||||
};
|
||||
});
|
||||
enqueueShellNotification(taskId, description, wasKilled ? 'killed' : result.code === 0 ? 'completed' : 'failed', result.code, setAppState, toolUseId, kind, agentId);
|
||||
void evictTaskOutput(taskId);
|
||||
});
|
||||
endTime: Date.now(),
|
||||
}
|
||||
})
|
||||
|
||||
enqueueShellNotification(
|
||||
taskId,
|
||||
description,
|
||||
wasKilled ? 'killed' : result.code === 0 ? 'completed' : 'failed',
|
||||
result.code,
|
||||
setAppState,
|
||||
toolUseId,
|
||||
kind,
|
||||
agentId,
|
||||
)
|
||||
|
||||
void evictTaskOutput(taskId)
|
||||
})
|
||||
|
||||
return {
|
||||
taskId,
|
||||
cleanup: () => {
|
||||
unregisterCleanup();
|
||||
}
|
||||
};
|
||||
unregisterCleanup()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,19 +323,19 @@ export async function spawnShellTask(input: LocalShellSpawnInput & {
|
||||
* Called when a bash command has been running long enough to show the BackgroundHint.
|
||||
* @returns taskId for the registered task
|
||||
*/
|
||||
export function registerForeground(input: LocalShellSpawnInput & {
|
||||
shellCommand: ShellCommand;
|
||||
}, setAppState: SetAppState, toolUseId?: string): string {
|
||||
const {
|
||||
command,
|
||||
description,
|
||||
shellCommand,
|
||||
agentId
|
||||
} = input;
|
||||
const taskId = shellCommand.taskOutput.taskId;
|
||||
export function registerForeground(
|
||||
input: LocalShellSpawnInput & { shellCommand: ShellCommand },
|
||||
setAppState: SetAppState,
|
||||
toolUseId?: string,
|
||||
): string {
|
||||
const { command, description, shellCommand, agentId } = input
|
||||
|
||||
const taskId = shellCommand.taskOutput.taskId
|
||||
|
||||
const unregisterCleanup = registerCleanup(async () => {
|
||||
killTask(taskId, setAppState);
|
||||
});
|
||||
killTask(taskId, setAppState)
|
||||
})
|
||||
|
||||
const taskState: LocalShellTaskState = {
|
||||
...createTaskStateBase(taskId, 'local_bash', description, toolUseId),
|
||||
type: 'local_bash',
|
||||
@@ -278,93 +345,119 @@ export function registerForeground(input: LocalShellSpawnInput & {
|
||||
shellCommand,
|
||||
unregisterCleanup,
|
||||
lastReportedTotalLines: 0,
|
||||
isBackgrounded: false,
|
||||
// Not yet backgrounded - running in foreground
|
||||
agentId
|
||||
};
|
||||
registerTask(taskState, setAppState);
|
||||
return taskId;
|
||||
isBackgrounded: false, // Not yet backgrounded - running in foreground
|
||||
agentId,
|
||||
}
|
||||
|
||||
registerTask(taskState, setAppState)
|
||||
return taskId
|
||||
}
|
||||
|
||||
/**
|
||||
* Background a specific foreground task.
|
||||
* @returns true if backgrounded successfully, false otherwise
|
||||
*/
|
||||
function backgroundTask(taskId: string, getAppState: () => AppState, setAppState: SetAppState): boolean {
|
||||
function backgroundTask(
|
||||
taskId: string,
|
||||
getAppState: () => AppState,
|
||||
setAppState: SetAppState,
|
||||
): boolean {
|
||||
// Step 1: Get the task and shell command from current state
|
||||
const state = getAppState();
|
||||
const task = state.tasks[taskId];
|
||||
const state = getAppState()
|
||||
const task = state.tasks[taskId]
|
||||
if (!isLocalShellTask(task) || task.isBackgrounded || !task.shellCommand) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
const shellCommand = task.shellCommand;
|
||||
const description = task.description;
|
||||
const {
|
||||
toolUseId,
|
||||
kind,
|
||||
agentId
|
||||
} = task;
|
||||
|
||||
const shellCommand = task.shellCommand
|
||||
const description = task.description
|
||||
const { toolUseId, kind, agentId } = task
|
||||
|
||||
// Transition to backgrounded — TaskOutput continues receiving data automatically
|
||||
if (!shellCommand.background(taskId)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
setAppState(prev => {
|
||||
const prevTask = prev.tasks[taskId];
|
||||
const prevTask = prev.tasks[taskId]
|
||||
if (!isLocalShellTask(prevTask) || prevTask.isBackgrounded) {
|
||||
return prev;
|
||||
return prev
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
tasks: {
|
||||
...prev.tasks,
|
||||
[taskId]: {
|
||||
...prevTask,
|
||||
isBackgrounded: true
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
const cancelStallWatchdog = startStallWatchdog(taskId, description, kind, toolUseId, agentId);
|
||||
[taskId]: { ...prevTask, isBackgrounded: true },
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const cancelStallWatchdog = startStallWatchdog(
|
||||
taskId,
|
||||
description,
|
||||
kind,
|
||||
toolUseId,
|
||||
agentId,
|
||||
)
|
||||
|
||||
// Set up result handler
|
||||
void shellCommand.result.then(async result => {
|
||||
cancelStallWatchdog();
|
||||
await flushAndCleanup(shellCommand);
|
||||
let wasKilled = false;
|
||||
let cleanupFn: (() => void) | undefined;
|
||||
cancelStallWatchdog()
|
||||
await flushAndCleanup(shellCommand)
|
||||
let wasKilled = false
|
||||
let cleanupFn: (() => void) | undefined
|
||||
|
||||
updateTaskState<LocalShellTaskState>(taskId, setAppState, t => {
|
||||
if (t.status === 'killed') {
|
||||
wasKilled = true;
|
||||
return t;
|
||||
wasKilled = true
|
||||
return t
|
||||
}
|
||||
|
||||
// Capture cleanup function to call outside of updater
|
||||
cleanupFn = t.unregisterCleanup;
|
||||
cleanupFn = t.unregisterCleanup
|
||||
|
||||
return {
|
||||
...t,
|
||||
status: result.code === 0 ? 'completed' : 'failed',
|
||||
result: {
|
||||
code: result.code,
|
||||
interrupted: result.interrupted
|
||||
},
|
||||
result: { code: result.code, interrupted: result.interrupted },
|
||||
shellCommand: null,
|
||||
unregisterCleanup: undefined,
|
||||
endTime: Date.now()
|
||||
};
|
||||
});
|
||||
endTime: Date.now(),
|
||||
}
|
||||
})
|
||||
|
||||
// Call cleanup outside of the state updater (avoid side effects in updater)
|
||||
cleanupFn?.();
|
||||
cleanupFn?.()
|
||||
|
||||
if (wasKilled) {
|
||||
enqueueShellNotification(taskId, description, 'killed', result.code, setAppState, toolUseId, kind, agentId);
|
||||
enqueueShellNotification(
|
||||
taskId,
|
||||
description,
|
||||
'killed',
|
||||
result.code,
|
||||
setAppState,
|
||||
toolUseId,
|
||||
kind,
|
||||
agentId,
|
||||
)
|
||||
} else {
|
||||
const finalStatus = result.code === 0 ? 'completed' : 'failed';
|
||||
enqueueShellNotification(taskId, description, finalStatus, result.code, setAppState, toolUseId, kind, agentId);
|
||||
const finalStatus = result.code === 0 ? 'completed' : 'failed'
|
||||
enqueueShellNotification(
|
||||
taskId,
|
||||
description,
|
||||
finalStatus,
|
||||
result.code,
|
||||
setAppState,
|
||||
toolUseId,
|
||||
kind,
|
||||
agentId,
|
||||
)
|
||||
}
|
||||
void evictTaskOutput(taskId);
|
||||
});
|
||||
return true;
|
||||
|
||||
void evictTaskOutput(taskId)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,34 +471,42 @@ function backgroundTask(taskId: string, getAppState: () => AppState, setAppState
|
||||
export function hasForegroundTasks(state: AppState): boolean {
|
||||
return Object.values(state.tasks).some(task => {
|
||||
if (isLocalShellTask(task) && !task.isBackgrounded && task.shellCommand) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
// Exclude main session tasks - they display in the main view, not as foreground tasks
|
||||
if (isLocalAgentTask(task) && !task.isBackgrounded && !isMainSessionTask(task)) {
|
||||
return true;
|
||||
if (
|
||||
isLocalAgentTask(task) &&
|
||||
!task.isBackgrounded &&
|
||||
!isMainSessionTask(task)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
}
|
||||
export function backgroundAll(getAppState: () => AppState, setAppState: SetAppState): void {
|
||||
const state = getAppState();
|
||||
|
||||
export function backgroundAll(
|
||||
getAppState: () => AppState,
|
||||
setAppState: SetAppState,
|
||||
): void {
|
||||
const state = getAppState()
|
||||
|
||||
// Background all foreground bash tasks
|
||||
const foregroundBashTaskIds = Object.keys(state.tasks).filter(id => {
|
||||
const task = state.tasks[id];
|
||||
return isLocalShellTask(task) && !task.isBackgrounded && task.shellCommand;
|
||||
});
|
||||
const task = state.tasks[id]
|
||||
return isLocalShellTask(task) && !task.isBackgrounded && task.shellCommand
|
||||
})
|
||||
for (const taskId of foregroundBashTaskIds) {
|
||||
backgroundTask(taskId, getAppState, setAppState);
|
||||
backgroundTask(taskId, getAppState, setAppState)
|
||||
}
|
||||
|
||||
// Background all foreground agent tasks
|
||||
const foregroundAgentTaskIds = Object.keys(state.tasks).filter(id => {
|
||||
const task = state.tasks[id];
|
||||
return isLocalAgentTask(task) && !task.isBackgrounded;
|
||||
});
|
||||
const task = state.tasks[id]
|
||||
return isLocalAgentTask(task) && !task.isBackgrounded
|
||||
})
|
||||
for (const taskId of foregroundAgentTaskIds) {
|
||||
backgroundAgentTask(taskId, getAppState, setAppState);
|
||||
backgroundAgentTask(taskId, getAppState, setAppState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,60 +518,86 @@ export function backgroundAll(getAppState: () => AppState, setAppState: SetAppSt
|
||||
* already registered the task (avoiding duplicate task_started SDK events
|
||||
* and leaked cleanup callbacks).
|
||||
*/
|
||||
export function backgroundExistingForegroundTask(taskId: string, shellCommand: ShellCommand, description: string, setAppState: SetAppState, toolUseId?: string): boolean {
|
||||
export function backgroundExistingForegroundTask(
|
||||
taskId: string,
|
||||
shellCommand: ShellCommand,
|
||||
description: string,
|
||||
setAppState: SetAppState,
|
||||
toolUseId?: string,
|
||||
): boolean {
|
||||
if (!shellCommand.background(taskId)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
let agentId: AgentId | undefined;
|
||||
|
||||
let agentId: AgentId | undefined
|
||||
setAppState(prev => {
|
||||
const prevTask = prev.tasks[taskId];
|
||||
const prevTask = prev.tasks[taskId]
|
||||
if (!isLocalShellTask(prevTask) || prevTask.isBackgrounded) {
|
||||
return prev;
|
||||
return prev
|
||||
}
|
||||
agentId = prevTask.agentId;
|
||||
agentId = prevTask.agentId
|
||||
return {
|
||||
...prev,
|
||||
tasks: {
|
||||
...prev.tasks,
|
||||
[taskId]: {
|
||||
...prevTask,
|
||||
isBackgrounded: true
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
const cancelStallWatchdog = startStallWatchdog(taskId, description, undefined, toolUseId, agentId);
|
||||
[taskId]: { ...prevTask, isBackgrounded: true },
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const cancelStallWatchdog = startStallWatchdog(
|
||||
taskId,
|
||||
description,
|
||||
undefined,
|
||||
toolUseId,
|
||||
agentId,
|
||||
)
|
||||
|
||||
// Set up result handler (mirrors backgroundTask's handler)
|
||||
void shellCommand.result.then(async result => {
|
||||
cancelStallWatchdog();
|
||||
await flushAndCleanup(shellCommand);
|
||||
let wasKilled = false;
|
||||
let cleanupFn: (() => void) | undefined;
|
||||
cancelStallWatchdog()
|
||||
await flushAndCleanup(shellCommand)
|
||||
let wasKilled = false
|
||||
let cleanupFn: (() => void) | undefined
|
||||
|
||||
updateTaskState<LocalShellTaskState>(taskId, setAppState, t => {
|
||||
if (t.status === 'killed') {
|
||||
wasKilled = true;
|
||||
return t;
|
||||
wasKilled = true
|
||||
return t
|
||||
}
|
||||
cleanupFn = t.unregisterCleanup;
|
||||
cleanupFn = t.unregisterCleanup
|
||||
return {
|
||||
...t,
|
||||
status: result.code === 0 ? 'completed' : 'failed',
|
||||
result: {
|
||||
code: result.code,
|
||||
interrupted: result.interrupted
|
||||
},
|
||||
result: { code: result.code, interrupted: result.interrupted },
|
||||
shellCommand: null,
|
||||
unregisterCleanup: undefined,
|
||||
endTime: Date.now()
|
||||
};
|
||||
});
|
||||
cleanupFn?.();
|
||||
const finalStatus = wasKilled ? 'killed' : result.code === 0 ? 'completed' : 'failed';
|
||||
enqueueShellNotification(taskId, description, finalStatus, result.code, setAppState, toolUseId, undefined, agentId);
|
||||
void evictTaskOutput(taskId);
|
||||
});
|
||||
return true;
|
||||
endTime: Date.now(),
|
||||
}
|
||||
})
|
||||
|
||||
cleanupFn?.()
|
||||
|
||||
const finalStatus = wasKilled
|
||||
? 'killed'
|
||||
: result.code === 0
|
||||
? 'completed'
|
||||
: 'failed'
|
||||
enqueueShellNotification(
|
||||
taskId,
|
||||
description,
|
||||
finalStatus,
|
||||
result.code,
|
||||
setAppState,
|
||||
toolUseId,
|
||||
undefined,
|
||||
agentId,
|
||||
)
|
||||
|
||||
void evictTaskOutput(taskId)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,45 +605,47 @@ export function backgroundExistingForegroundTask(taskId: string, shellCommand: S
|
||||
* Used when backgrounding raced with completion — the tool result already
|
||||
* carries the full output, so the <task_notification> would be redundant.
|
||||
*/
|
||||
export function markTaskNotified(taskId: string, setAppState: SetAppState): void {
|
||||
updateTaskState<LocalShellTaskState>(taskId, setAppState, t => t.notified ? t : {
|
||||
...t,
|
||||
notified: true
|
||||
});
|
||||
export function markTaskNotified(
|
||||
taskId: string,
|
||||
setAppState: SetAppState,
|
||||
): void {
|
||||
updateTaskState(taskId, setAppState, t =>
|
||||
t.notified ? t : { ...t, notified: true },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a foreground task when the command completes without being backgrounded.
|
||||
*/
|
||||
export function unregisterForeground(taskId: string, setAppState: SetAppState): void {
|
||||
let cleanupFn: (() => void) | undefined;
|
||||
export function unregisterForeground(
|
||||
taskId: string,
|
||||
setAppState: SetAppState,
|
||||
): void {
|
||||
let cleanupFn: (() => void) | undefined
|
||||
|
||||
setAppState(prev => {
|
||||
const task = prev.tasks[taskId];
|
||||
const task = prev.tasks[taskId]
|
||||
// Only remove if it's a foreground task (not backgrounded)
|
||||
if (!isLocalShellTask(task) || task.isBackgrounded) {
|
||||
return prev;
|
||||
return prev
|
||||
}
|
||||
|
||||
// Capture cleanup function to call outside of updater
|
||||
cleanupFn = task.unregisterCleanup;
|
||||
const {
|
||||
[taskId]: removed,
|
||||
...rest
|
||||
} = prev.tasks;
|
||||
return {
|
||||
...prev,
|
||||
tasks: rest
|
||||
};
|
||||
});
|
||||
cleanupFn = task.unregisterCleanup
|
||||
|
||||
const { [taskId]: removed, ...rest } = prev.tasks
|
||||
return { ...prev, tasks: rest }
|
||||
})
|
||||
|
||||
// Call cleanup outside of the state updater (avoid side effects in updater)
|
||||
cleanupFn?.();
|
||||
cleanupFn?.()
|
||||
}
|
||||
|
||||
async function flushAndCleanup(shellCommand: ShellCommand): Promise<void> {
|
||||
try {
|
||||
await shellCommand.taskOutput.flush();
|
||||
shellCommand.cleanup();
|
||||
await shellCommand.taskOutput.flush()
|
||||
shellCommand.cleanup()
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user