mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
278 lines
9.6 KiB
TypeScript
278 lines
9.6 KiB
TypeScript
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;
|
||
}
|
||
}
|