更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)

* 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>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -9,14 +9,19 @@
* 4. Can be idle (waiting for work) or active (processing)
*/
import { isTerminalTaskStatus, type SetAppState, type Task, type TaskStateBase } from '../../Task.js';
import type { Message } from '../../types/message.js';
import { logForDebugging } from '../../utils/debug.js';
import { createUserMessage } from '../../utils/messages.js';
import { killInProcessTeammate } from '../../utils/swarm/spawnInProcess.js';
import { updateTaskState } from '../../utils/task/framework.js';
import type { InProcessTeammateTaskState } from './types.js';
import { appendCappedMessage, isInProcessTeammateTask } from './types.js';
import {
isTerminalTaskStatus,
type SetAppState,
type Task,
type TaskStateBase,
} from '../../Task.js'
import type { Message } from '../../types/message.js'
import { logForDebugging } from '../../utils/debug.js'
import { createUserMessage } from '../../utils/messages.js'
import { killInProcessTeammate } from '../../utils/swarm/spawnInProcess.js'
import { updateTaskState } from '../../utils/task/framework.js'
import type { InProcessTeammateTaskState } from './types.js'
import { appendCappedMessage, isInProcessTeammateTask } from './types.js'
/**
* InProcessTeammateTask - Handles in-process teammate execution.
@@ -25,39 +30,48 @@ export const InProcessTeammateTask: Task = {
name: 'InProcessTeammateTask',
type: 'in_process_teammate',
async kill(taskId, setAppState) {
killInProcessTeammate(taskId, setAppState);
}
};
killInProcessTeammate(taskId, setAppState)
},
}
/**
* Request shutdown for a teammate.
*/
export function requestTeammateShutdown(taskId: string, setAppState: SetAppState): void {
export function requestTeammateShutdown(
taskId: string,
setAppState: SetAppState,
): void {
updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
if (task.status !== 'running' || task.shutdownRequested) {
return task;
return task
}
return {
...task,
shutdownRequested: true
};
});
shutdownRequested: true,
}
})
}
/**
* Append a message to a teammate's conversation history.
* Used for zoomed view to show the teammate's conversation.
*/
export function appendTeammateMessage(taskId: string, message: Message, setAppState: SetAppState): void {
export function appendTeammateMessage(
taskId: string,
message: Message,
setAppState: SetAppState,
): void {
updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
if (task.status !== 'running') {
return task;
return task
}
return {
...task,
messages: appendCappedMessage(task.messages, message)
};
});
messages: appendCappedMessage(task.messages, message),
}
})
}
/**
@@ -65,22 +79,30 @@ export function appendTeammateMessage(taskId: string, message: Message, setAppSt
* Used when viewing a teammate's transcript to send typed messages to them.
* Also adds the message to task.messages so it appears immediately in the transcript.
*/
export function injectUserMessageToTeammate(taskId: string, message: string, setAppState: SetAppState): void {
export function injectUserMessageToTeammate(
taskId: string,
message: string,
setAppState: SetAppState,
): void {
updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
// Allow message injection when teammate is running or idle (waiting for input)
// Only reject if teammate is in a terminal state
if (isTerminalTaskStatus(task.status)) {
logForDebugging(`Dropping message for teammate task ${taskId}: task status is "${task.status}"`);
return task;
logForDebugging(
`Dropping message for teammate task ${taskId}: task status is "${task.status}"`,
)
return task
}
return {
...task,
pendingUserMessages: [...task.pendingUserMessages, message],
messages: appendCappedMessage(task.messages, createUserMessage({
content: message
}))
};
});
messages: appendCappedMessage(
task.messages,
createUserMessage({ content: message }),
),
}
})
}
/**
@@ -89,29 +111,34 @@ export function injectUserMessageToTeammate(taskId: string, message: string, set
* with the same agentId exist.
* Returns undefined if not found.
*/
export function findTeammateTaskByAgentId(agentId: string, tasks: Record<string, TaskStateBase>): InProcessTeammateTaskState | undefined {
let fallback: InProcessTeammateTaskState | undefined;
export function findTeammateTaskByAgentId(
agentId: string,
tasks: Record<string, TaskStateBase>,
): InProcessTeammateTaskState | undefined {
let fallback: InProcessTeammateTaskState | undefined
for (const task of Object.values(tasks)) {
if (isInProcessTeammateTask(task) && task.identity.agentId === agentId) {
// Prefer running tasks in case old killed tasks still exist in AppState
// alongside new running ones with the same agentId
if (task.status === 'running') {
return task;
return task
}
// Keep first match as fallback in case no running task exists
if (!fallback) {
fallback = task;
fallback = task
}
}
}
return fallback;
return fallback
}
/**
* Get all in-process teammate tasks from AppState.
*/
export function getAllInProcessTeammateTasks(tasks: Record<string, TaskStateBase>): InProcessTeammateTaskState[] {
return Object.values(tasks).filter(isInProcessTeammateTask);
export function getAllInProcessTeammateTasks(
tasks: Record<string, TaskStateBase>,
): InProcessTeammateTaskState[] {
return Object.values(tasks).filter(isInProcessTeammateTask)
}
/**
@@ -120,6 +147,10 @@ export function getAllInProcessTeammateTasks(tasks: Record<string, TaskStateBase
* and useBackgroundTaskNavigation — selectedIPAgentIndex maps into this
* array, so all three must agree on sort order.
*/
export function getRunningTeammatesSorted(tasks: Record<string, TaskStateBase>): InProcessTeammateTaskState[] {
return getAllInProcessTeammateTasks(tasks).filter(t => t.status === 'running').sort((a, b) => a.identity.agentName.localeCompare(b.identity.agentName));
export function getRunningTeammatesSorted(
tasks: Record<string, TaskStateBase>,
): InProcessTeammateTaskState[] {
return getAllInProcessTeammateTasks(tasks)
.filter(t => t.status === 'running')
.sort((a, b) => a.identity.agentName.localeCompare(b.identity.agentName))
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}
}

File diff suppressed because it is too large Load Diff