/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useMemo } from 'react'; import { Box, Text } from 'ink'; import type { IndividualToolCallDisplay } from '../../types.js'; import { ToolCallStatus } from '../../types.js'; import { Colors } from '../../colors.js'; import { theme } from '../../semantic-colors.js'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; import { useShellCommandDisplay } from '../../contexts/ShellCommandDisplayContext.js'; import { ShellExecutionService, splitCommands, } from '@vybestack/llxprt-code-core'; import { ShellInputPrompt } from '../ShellInputPrompt.js'; import { StickyHeader } from '../StickyHeader.js'; import { ToolStatusIndicator, ToolInfo, TrailingIndicator, STATUS_INDICATOR_WIDTH, type TextEmphasis, } from './ToolShared.js'; import { ToolResultDisplay } from './ToolResultDisplay.js'; import type { ShellState } from '../../cliUiRuntime.js'; import { getBorderStyle } from '../../contexts/UnicodeRenderingContext.js'; export type { TextEmphasis } from './ToolShared.js'; /** * Find the first delimiter position between paren and bracket. * Returns the position of whichever delimiter comes first, or -1 if neither found. */ function findFirstDelimiter(paren: number, bracket: number): number { if (paren === -1) return bracket; if (bracket === -1) return paren; return Math.min(paren, bracket); } /** * Extract echo text from a command if it's an echo command. */ function extractEchoText(cmd: string): string | null { // Parse an echo command's argument. A single whitespace separator avoids the // overlapping `\s+(.*)` backtracking flagged by sonarjs/slow-regex (the // captured text is trimmed below, so semantics are unchanged); the pattern is // passed to RegExp via an identifier so it is not a static literal flagged by // sonarjs/regular-expr. const echoPattern = '^\\s*echo\\s(.*)$'; const m = cmd.match(new RegExp(echoPattern, 'i')); if (!m) return null; let text = m[1].trim(); if ( (text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'")) ) { text = text.slice(1, -1); } return text; } /** * Compute current subcommand for display when details visible. */ function computeCurrentSubcommand( showFullShellDescription: boolean, status: ToolCallStatus, description: string | undefined, resultDisplay: string | object | undefined, ): string | null { if (!showFullShellDescription || status !== ToolCallStatus.Executing) { return null; } if (!description) return null; const outputString = typeof resultDisplay === 'string' ? resultDisplay : undefined; let raw = description; const paren = raw.indexOf(' ('); const bracket = raw.indexOf(' [in '); const cut = findFirstDelimiter(paren, bracket); if (cut > 0) raw = raw.slice(0, cut); const segments = splitCommands(raw).filter((s) => s.length > 0); if (segments.length === 0) return null; return findCurrentSubcommand(segments, outputString); } /** * Find the current subcommand based on last output line. */ function findCurrentSubcommand( segments: string[], outputString: string | undefined, ): string | null { const lastLine = outputString ?.split('\n') .map((l) => l.trim()) .filter((l) => l.length > 0) .pop() ?? ''; let idx = 0; if (lastLine) { for (let i = 0; i < segments.length; i++) { const t = extractEchoText(segments[i]); if (t && t === lastLine) { idx = Math.min(i + 1, segments.length - 1); } } } return segments[idx] ?? null; } interface ShellFocusState { isShellTool: boolean; isThisShellTargeted: boolean; isThisShellFocused: boolean; isThisShellFocusable: boolean; } function computeShellFocusState( name: string, status: ToolCallStatus, ptyId: number | undefined, activeShellPtyId: number | null | undefined, embeddedShellFocused: boolean | undefined, shell: ShellState | undefined, ): ShellFocusState { const isShellTool = name === SHELL_COMMAND_NAME || name === SHELL_NAME; const lastActivePtyId = ShellExecutionService.getLastActivePtyId(); const isLastActiveShellTarget = lastActivePtyId !== null && ptyId !== undefined && ptyId === lastActivePtyId; const isThisShellTargeted = ptyId === activeShellPtyId || (activeShellPtyId == null && isLastActiveShellTarget); const isPtyAlive = ptyId !== undefined && ShellExecutionService.isActivePty(ptyId); const isExecuting = status === ToolCallStatus.Executing; const isActiveOrAlive = isExecuting || isPtyAlive; const isThisShellFocused = isShellTool && isThisShellTargeted && embeddedShellFocused === true && isActiveOrAlive; const interactiveShellEnabled = shell?.getEnableInteractiveShell() === true; const isThisShellFocusable = isShellTool && interactiveShellEnabled && isThisShellTargeted && isActiveOrAlive; return { isShellTool, isThisShellTargeted, isThisShellFocused, isThisShellFocusable, }; } /** * Render the executing status hint. */ function renderExecutingHint(showExecutingHint: boolean): React.ReactNode { if (!showExecutingHint) { return null; } return ( Press 'ctrl+r' to show running command ); } /** * Render the current subcommand display. */ function renderCurrentSubcommand( currentSubcommand: string | null, ): React.ReactNode { if (!currentSubcommand) { return null; } return ( Running: {currentSubcommand} ); } /** * Render the shell input prompt if needed. */ function renderShellInput( isThisShellFocused: boolean, shell: ShellState | undefined, activeShellPtyId: number | null | undefined, embeddedShellFocused: boolean | undefined, ): React.ReactNode { if (!isThisShellFocused || shell == null) { return null; } return ( ); } /** * Render the tool message header section. */ function renderToolMessageHeader( terminalWidth: number, isFirst: boolean, borderColor: string, borderDimColor: boolean, status: ToolCallStatus, name: string, description: string, emphasis: TextEmphasis, showFullDescription: boolean, isThisShellFocusable: boolean, isThisShellFocused: boolean, ): React.ReactNode { return ( {isThisShellFocusable && ( {isThisShellFocused ? '(Focused - Tab/Shift+Tab/Ctrl+F to return)' : '(Tab/Ctrl+F to focus)'} )} {emphasis === 'high' && } ); } /** * Render the tool message content box. */ function renderToolMessageContent( terminalWidth: number, borderColor: string, borderDimColor: boolean, status: ToolCallStatus, showExecutingHint: boolean, currentSubcommand: string | null, resultDisplay: string | object | undefined, availableTerminalHeight: number | undefined, renderOutputAsMarkdown: boolean, isThisShellFocused: boolean, shell: ShellState | undefined, activeShellPtyId: number | null | undefined, embeddedShellFocused: boolean | undefined, ): React.ReactNode { return ( {renderExecutingHint(showExecutingHint)} {renderCurrentSubcommand(currentSubcommand)} {renderShellInput( isThisShellFocused, shell, activeShellPtyId, embeddedShellFocused, )} ); } export interface ToolMessageProps extends IndividualToolCallDisplay { availableTerminalHeight?: number; terminalWidth: number; emphasis?: TextEmphasis; renderOutputAsMarkdown?: boolean; activeShellPtyId?: number | null; embeddedShellFocused?: boolean; shell?: ShellState; isFirst?: boolean; borderColor?: string; borderDimColor?: boolean; } export const ToolMessage: React.FC = ({ callId, name, description, resultDisplay, status, availableTerminalHeight, terminalWidth, emphasis = 'medium', renderOutputAsMarkdown = true, isFocused = true, activeShellPtyId, embeddedShellFocused, ptyId, shell, isFirst = false, borderColor = Colors.Gray, borderDimColor = false, }) => { const isShellTool = name === SHELL_NAME || name === SHELL_COMMAND_NAME; const showFullShellDescription = useShellCommandDisplay( callId, name, status, isFocused, ); const { isThisShellFocused, isThisShellFocusable } = computeShellFocusState( name, status, ptyId, activeShellPtyId, embeddedShellFocused, shell, ); const currentSubcommand = useMemo( () => computeCurrentSubcommand( showFullShellDescription, status, description, resultDisplay, ), [showFullShellDescription, status, description, resultDisplay], ); return ( <> {renderToolMessageHeader( terminalWidth, isFirst, borderColor, borderDimColor, status, name, description || '', emphasis, showFullShellDescription, isThisShellFocusable, isThisShellFocused, )} {renderToolMessageContent( terminalWidth, borderColor, borderDimColor, status, isShellTool && status === ToolCallStatus.Executing && !showFullShellDescription, currentSubcommand, resultDisplay, availableTerminalHeight, renderOutputAsMarkdown, isThisShellFocused, shell, activeShellPtyId, embeddedShellFocused, )} ); };