Files
claude-code/src/components/messages/AssistantToolUseMessage.tsx
znygugeyx-ctrl 5c107e5f8c Merge pull request #416 from znygugeyx-ctrl/feat/subagent-fork-render
feat: 参考 claude code 官方实现,改进 sub agent 以及 fork agent 的渲染方式
2026-05-06 09:57:52 +08:00

278 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import React, { useMemo } from 'react';
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
import type { ThemeName } from 'src/utils/theme.js';
import type { Command } from '../../commands.js';
import { BLACK_CIRCLE } from '../../constants/figures.js';
import { Box, Text, stringWidth, useTheme } from '@anthropic/ink';
import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js';
import { findToolByName, type Tool, type ToolProgressData, type Tools } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js';
import { logError } from '../../utils/log.js';
import type { buildMessageLookups } from '../../utils/messages.js';
import { MessageResponse } from '../MessageResponse.js';
import { useSelectedMessageBg } from '../messageActions.js';
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
import { ToolUseLoader } from '../ToolUseLoader.js';
import { HookProgressMessage } from './HookProgressMessage.js';
type Props = {
param: ToolUseBlockParam;
addMargin: boolean;
tools: Tools;
commands: Command[];
verbose: boolean;
inProgressToolUseIDs: Set<string>;
progressMessagesForMessage: ProgressMessage[];
shouldAnimate: boolean;
shouldShowDot: boolean;
inProgressToolCallCount?: number;
lookups: ReturnType<typeof buildMessageLookups>;
isTranscriptMode?: boolean;
defaultCollapsed?: boolean;
};
export function AssistantToolUseMessage({
param,
addMargin,
tools,
commands,
verbose,
inProgressToolUseIDs,
progressMessagesForMessage,
shouldAnimate,
shouldShowDot,
inProgressToolCallCount,
lookups,
isTranscriptMode,
defaultCollapsed,
}: Props): React.ReactNode {
const terminalSize = useTerminalSize();
const [theme] = useTheme();
const bg = useSelectedMessageBg();
const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider(state => state.pendingWorkerRequest);
const isClassifierCheckingRaw = useIsClassifierChecking(param.id);
const permissionMode = useAppStateMaybeOutsideOfProvider(state => state.toolPermissionContext.mode);
// strippedDangerousRules is set by stripDangerousPermissionsForAutoMode
// (even to {}) whenever auto is active, and cleared by restoreDangerousPermissions
// on deactivation — a reliable proxy for isAutoModeActive() during plan.
// prePlanMode would be stale after transitionPlanAutoMode deactivates mid-plan.
const hasStrippedRules = useAppStateMaybeOutsideOfProvider(
state => !!state.toolPermissionContext.strippedDangerousRules,
);
const isAutoClassifier = permissionMode === 'auto' || (permissionMode === 'plan' && hasStrippedRules);
const isClassifierChecking = process.env.USER_TYPE === 'ant' && isClassifierCheckingRaw && permissionMode !== 'auto';
// Memoize on param identity (stable — from the persisted message object).
// Zod safeParse allocates per call, and some tools' userFacingName()
// (BashTool → shouldUseSandbox → shell-quote parse) are expensive. Without
// this, ~50 bash messages × shell-quote-per-render pushed transition
// render past the shimmer tick → abort → infinite retry (#21605).
const parsed = useMemo(() => {
if (!tools) return null;
const tool = findToolByName(tools, param.name);
if (!tool) return null;
const input = tool.inputSchema.safeParse(param.input);
const data = input.success ? input.data : undefined;
return {
tool,
input,
userFacingToolName: tool.userFacingName(data),
userFacingToolNameBackgroundColor: tool.userFacingNameBackgroundColor?.(data),
isTransparentWrapper: tool.isTransparentWrapper?.() ?? false,
};
}, [tools, param]);
if (!parsed) {
// Guard against undefined tools (required prop) or unknown tool name
logError(new Error(tools ? `Tool ${param.name} not found` : `Tools array is undefined for tool ${param.name}`));
return null;
}
const { tool, input, userFacingToolName, userFacingToolNameBackgroundColor, isTransparentWrapper } = parsed;
const isResolved = lookups.resolvedToolUseIDs.has(param.id);
const isQueued = !inProgressToolUseIDs.has(param.id) && !isResolved;
const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id;
if (isTransparentWrapper) {
if (isQueued || isResolved) return null;
return (
<Box flexDirection="column" width="100%" backgroundColor={bg}>
{renderToolUseProgressMessage(
tool,
tools,
lookups,
param.id,
progressMessagesForMessage,
{ verbose, inProgressToolCallCount, isTranscriptMode },
terminalSize,
)}
</Box>
);
}
if (userFacingToolName === '') {
return null;
}
const renderedToolUseMessage = input.success
? renderToolUseMessage(tool, input.data, { theme, verbose, commands })
: null;
if (renderedToolUseMessage === null) {
return null;
}
return (
<Box
flexDirection="row"
justifyContent="space-between"
marginTop={addMargin ? 1 : 0}
width="100%"
backgroundColor={bg}
>
<Box flexDirection="column">
<Box flexDirection="row" flexWrap="nowrap" minWidth={stringWidth(userFacingToolName) + (shouldShowDot ? 2 : 0)}>
{shouldShowDot &&
(isQueued ? (
<Box minWidth={2}>
<Text dimColor={isQueued}>{BLACK_CIRCLE}</Text>
</Box>
) : (
// WARNING: The code here and in ToolUseLoader is particularly
// sensitive to what *should* just be trivial refactorings. See
// the comment in ToolUseLoader for more details.
<ToolUseLoader
shouldAnimate={shouldAnimate}
isUnresolved={!isResolved}
isError={lookups.erroredToolUseIDs.has(param.id)}
/>
))}
<Box flexShrink={0}>
<Text
bold
wrap="truncate-end"
backgroundColor={userFacingToolNameBackgroundColor}
color={userFacingToolNameBackgroundColor ? 'inverseText' : undefined}
>
{userFacingToolName}
</Text>
</Box>
{renderedToolUseMessage !== '' && (
<Box flexWrap="nowrap">
<Text>({renderedToolUseMessage})</Text>
</Box>
)}
{/* Render tool-specific tags (timeout, model, resume ID, etc.) */}
{input.success && tool.renderToolUseTag && tool.renderToolUseTag(input.data)}
</Box>
{!isResolved &&
!isQueued &&
!defaultCollapsed &&
(isClassifierChecking ? (
<MessageResponse height={1}>
<Text dimColor>
{isAutoClassifier ? 'Auto classifier checking\u2026' : 'Bash classifier checking\u2026'}
</Text>
</MessageResponse>
) : isWaitingForPermission ? (
<MessageResponse height={1}>
<Text dimColor>Waiting for permission</Text>
</MessageResponse>
) : (
renderToolUseProgressMessage(
tool,
tools,
lookups,
param.id,
progressMessagesForMessage,
{
verbose,
inProgressToolCallCount,
isTranscriptMode,
},
terminalSize,
)
))}
{!isResolved && isQueued && renderToolUseQueuedMessage(tool)}
</Box>
</Box>
);
}
function renderToolUseMessage(
tool: Tool,
input: unknown,
{ theme, verbose, commands }: { theme: ThemeName; verbose: boolean; commands: Command[] },
): React.ReactNode {
try {
const parsed = tool.inputSchema.safeParse(input);
if (!parsed.success) {
return '';
}
return tool.renderToolUseMessage(parsed.data, { theme, verbose, commands });
} catch (error) {
logError(new Error(`Error rendering tool use message for ${tool.name}: ${error}`));
return '';
}
}
function renderToolUseProgressMessage(
tool: Tool,
tools: Tools,
lookups: ReturnType<typeof buildMessageLookups>,
toolUseID: string,
progressMessagesForMessage: ProgressMessage[],
{
verbose,
inProgressToolCallCount,
isTranscriptMode,
}: {
verbose: boolean;
inProgressToolCallCount?: number;
isTranscriptMode?: boolean;
},
terminalSize: { columns: number; rows: number },
): React.ReactNode {
const toolProgressMessages = progressMessagesForMessage.filter(
(msg): msg is ProgressMessage<ToolProgressData> => (msg.data as Record<string, unknown>).type !== 'hook_progress',
);
try {
const toolMessages =
tool.renderToolUseProgressMessage?.(toolProgressMessages, {
tools,
verbose,
terminalSize,
inProgressToolCallCount: inProgressToolCallCount ?? 1,
isTranscriptMode,
}) ?? null;
return (
<>
<SentryErrorBoundary>
<HookProgressMessage
hookEvent="PreToolUse"
lookups={lookups}
toolUseID={toolUseID}
verbose={verbose}
isTranscriptMode={isTranscriptMode}
/>
</SentryErrorBoundary>
{toolMessages}
</>
);
} catch (error) {
logError(new Error(`Error rendering tool use progress message for ${tool.name}: ${error}`));
return null;
}
}
function renderToolUseQueuedMessage(tool: Tool): React.ReactNode {
try {
return tool.renderToolUseQueuedMessage?.();
} catch (error) {
logError(new Error(`Error rendering tool use queued message for ${tool.name}: ${error}`));
return null;
}
}