Run npm run format

- Also updated README.md accordingly.

Part of https://b.corp.google.com/issues/411384603
This commit is contained in:
Taylor Mullen
2025-04-17 18:06:21 -04:00
committed by N. Taylor Mullen
parent 7928c1727f
commit cfc697a96d
45 changed files with 4373 additions and 3332 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Text } from 'ink'
import { Box, Text } from 'ink';
interface DiffLine {
type: 'add' | 'del' | 'context' | 'hunk' | 'other';
@@ -30,29 +30,53 @@ function parseDiffWithLineNumbers(diffContent: string): DiffLine[] {
continue;
}
if (!inHunk) {
// Skip standard Git header lines more robustly
if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('similarity index') || line.startsWith('rename from') || line.startsWith('rename to') || line.startsWith('new file mode') || line.startsWith('deleted file mode')) continue;
// Skip standard Git header lines more robustly
if (
line.startsWith('--- ') ||
line.startsWith('+++ ') ||
line.startsWith('diff --git') ||
line.startsWith('index ') ||
line.startsWith('similarity index') ||
line.startsWith('rename from') ||
line.startsWith('rename to') ||
line.startsWith('new file mode') ||
line.startsWith('deleted file mode')
)
continue;
// If it's not a hunk or header, skip (or handle as 'other' if needed)
continue;
}
if (line.startsWith('+')) {
currentNewLine++; // Increment before pushing
result.push({ type: 'add', newLine: currentNewLine, content: line.substring(1) });
result.push({
type: 'add',
newLine: currentNewLine,
content: line.substring(1),
});
} else if (line.startsWith('-')) {
currentOldLine++; // Increment before pushing
result.push({ type: 'del', oldLine: currentOldLine, content: line.substring(1) });
result.push({
type: 'del',
oldLine: currentOldLine,
content: line.substring(1),
});
} else if (line.startsWith(' ')) {
currentOldLine++; // Increment before pushing
currentNewLine++;
result.push({ type: 'context', oldLine: currentOldLine, newLine: currentNewLine, content: line.substring(1) });
} else if (line.startsWith('\\')) { // Handle "\ No newline at end of file"
result.push({
type: 'context',
oldLine: currentOldLine,
newLine: currentNewLine,
content: line.substring(1),
});
} else if (line.startsWith('\\')) {
// Handle "\ No newline at end of file"
result.push({ type: 'other', content: line });
}
}
return result;
}
interface DiffRendererProps {
diffContent: string;
filename?: string;
@@ -61,7 +85,10 @@ interface DiffRendererProps {
const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEFAULT_TAB_WIDTH }) => {
const DiffRenderer: React.FC<DiffRendererProps> = ({
diffContent,
tabWidth = DEFAULT_TAB_WIDTH,
}) => {
if (!diffContent || typeof diffContent !== 'string') {
return <Text color="yellow">No diff content.</Text>;
}
@@ -69,14 +96,15 @@ const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEF
const parsedLines = parseDiffWithLineNumbers(diffContent);
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
const normalizedLines = parsedLines.map(line => ({
const normalizedLines = parsedLines.map((line) => ({
...line,
content: line.content.replace(/\t/g, ' '.repeat(tabWidth))
content: line.content.replace(/\t/g, ' '.repeat(tabWidth)),
}));
// Filter out non-displayable lines (hunks, potentially 'other') using the normalized list
const displayableLines = normalizedLines.filter(l => l.type !== 'hunk' && l.type !== 'other');
const displayableLines = normalizedLines.filter(
(l) => l.type !== 'hunk' && l.type !== 'other',
);
if (displayableLines.length === 0) {
return (
@@ -93,7 +121,7 @@ const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEF
if (line.content.trim() === '') continue;
const firstCharIndex = line.content.search(/\S/); // Find index of first non-whitespace char
const currentIndent = (firstCharIndex === -1) ? 0 : firstCharIndex; // Indent is 0 if no non-whitespace found
const currentIndent = firstCharIndex === -1 ? 0 : firstCharIndex; // Indent is 0 if no non-whitespace found
baseIndentation = Math.min(baseIndentation, currentIndent);
}
// If baseIndentation remained Infinity (e.g., no displayable lines with content), default to 0
@@ -102,7 +130,6 @@ const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEF
}
// --- End Modification ---
return (
<Box borderStyle="round" borderColor="gray" flexDirection="column">
{/* Iterate over the lines that should be displayed (already normalized) */}
@@ -139,9 +166,13 @@ const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEF
return (
// Using your original rendering structure
<Box key={key} flexDirection="row">
<Text color="gray">{gutterNumStr} </Text>
<Text color={color} dimColor={dim}>{prefixSymbol} </Text>
<Text color={color} dimColor={dim} wrap="wrap">{displayContent}</Text>
<Text color="gray">{gutterNumStr} </Text>
<Text color={color} dimColor={dim}>
{prefixSymbol}{' '}
</Text>
<Text color={color} dimColor={dim} wrap="wrap">
{displayContent}
</Text>
</Box>
);
})}
@@ -149,4 +180,4 @@ const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEF
);
};
export default DiffRenderer;
export default DiffRenderer;

View File

@@ -2,23 +2,25 @@ import React from 'react';
import { Text, Box } from 'ink';
interface ErrorMessageProps {
text: string;
text: string;
}
const ErrorMessage: React.FC<ErrorMessageProps> = ({ text }) => {
const prefix = '✕ ';
const prefixWidth = prefix.length;
const prefix = '✕ ';
const prefixWidth = prefix.length;
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="red">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color="red">{text}</Text>
</Box>
</Box>
);
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="red">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color="red">
{text}
</Text>
</Box>
</Box>
);
};
export default ErrorMessage;
export default ErrorMessage;

View File

@@ -3,42 +3,42 @@ import { Text, Box } from 'ink';
import { MarkdownRenderer } from '../../utils/MarkdownRenderer.js';
interface GeminiMessageProps {
text: string;
text: string;
}
const GeminiMessage: React.FC<GeminiMessageProps> = ({ text }) => {
const prefix = '✦ ';
const prefixWidth = prefix.length;
const prefix = '✦ ';
const prefixWidth = prefix.length;
// Handle potentially null or undefined text gracefully
const safeText = text || '';
// Handle potentially null or undefined text gracefully
const safeText = text || '';
// Use the static render method from the MarkdownRenderer class
// Pass safeText which is guaranteed to be a string
const renderedBlocks = MarkdownRenderer.render(safeText);
// If the original text was actually empty/null, render the minimal state
if (!safeText && renderedBlocks.length === 0) {
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="blue">{prefix}</Text>
</Box>
<Box flexGrow={1}></Box>
</Box>
);
}
// Use the static render method from the MarkdownRenderer class
// Pass safeText which is guaranteed to be a string
const renderedBlocks = MarkdownRenderer.render(safeText);
// If the original text was actually empty/null, render the minimal state
if (!safeText && renderedBlocks.length === 0) {
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="blue">{prefix}</Text>
</Box>
<Box flexGrow={1} flexDirection="column">
{renderedBlocks}
</Box>
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="blue">{prefix}</Text>
</Box>
<Box flexGrow={1}></Box>
</Box>
);
}
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="blue">{prefix}</Text>
</Box>
<Box flexGrow={1} flexDirection="column">
{renderedBlocks}
</Box>
</Box>
);
};
export default GeminiMessage;
export default GeminiMessage;

View File

@@ -2,23 +2,25 @@ import React from 'react';
import { Text, Box } from 'ink';
interface InfoMessageProps {
text: string;
text: string;
}
const InfoMessage: React.FC<InfoMessageProps> = ({ text }) => {
const prefix = ' ';
const prefixWidth = prefix.length;
const prefix = ' ';
const prefixWidth = prefix.length;
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="yellow">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color="yellow">{text}</Text>
</Box>
</Box>
);
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="yellow">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color="yellow">
{text}
</Text>
</Box>
</Box>
);
};
export default InfoMessage;
export default InfoMessage;

View File

@@ -1,7 +1,12 @@
import React from 'react';
import { Box, Text, useInput } from 'ink';
import SelectInput from 'ink-select-input';
import { ToolCallConfirmationDetails, ToolEditConfirmationDetails, ToolConfirmationOutcome, ToolExecuteConfirmationDetails } from '../../types.js'; // Adjust path as needed
import {
ToolCallConfirmationDetails,
ToolEditConfirmationDetails,
ToolConfirmationOutcome,
ToolExecuteConfirmationDetails,
} from '../../types.js'; // Adjust path as needed
import { PartListUnion } from '@google/genai';
import DiffRenderer from './DiffRenderer.js';
import { UI_WIDTH } from '../../constants.js';
@@ -11,7 +16,9 @@ export interface ToolConfirmationMessageProps {
onSubmit: (value: PartListUnion) => void;
}
function isEditDetails(props: ToolCallConfirmationDetails): props is ToolEditConfirmationDetails {
function isEditDetails(
props: ToolCallConfirmationDetails,
): props is ToolEditConfirmationDetails {
return (props as ToolEditConfirmationDetails).fileName !== undefined;
}
@@ -20,7 +27,9 @@ interface InternalOption {
value: ToolConfirmationOutcome;
}
const ToolConfirmationMessage: React.FC<ToolConfirmationMessageProps> = ({ confirmationDetails }) => {
const ToolConfirmationMessage: React.FC<ToolConfirmationMessageProps> = ({
confirmationDetails,
}) => {
const { onConfirm } = confirmationDetails;
useInput((_, key) => {
@@ -39,41 +48,53 @@ const ToolConfirmationMessage: React.FC<ToolConfirmationMessageProps> = ({ confi
const options: InternalOption[] = [];
if (isEditDetails(confirmationDetails)) {
title = "Edit"; // Title for the outer box
title = 'Edit'; // Title for the outer box
// Body content is now the DiffRenderer, passing filename to it
// The bordered box is removed from here and handled within DiffRenderer
bodyContent = (
<DiffRenderer diffContent={confirmationDetails.fileDiff} />
);
bodyContent = <DiffRenderer diffContent={confirmationDetails.fileDiff} />;
question = `Apply this change?`;
options.push(
{ label: '1. Yes, apply change', value: ToolConfirmationOutcome.ProceedOnce },
{ label: "2. Yes, always apply file edits", value: ToolConfirmationOutcome.ProceedAlways },
{ label: '3. No (esc)', value: ToolConfirmationOutcome.Cancel }
{
label: '1. Yes, apply change',
value: ToolConfirmationOutcome.ProceedOnce,
},
{
label: '2. Yes, always apply file edits',
value: ToolConfirmationOutcome.ProceedAlways,
},
{ label: '3. No (esc)', value: ToolConfirmationOutcome.Cancel },
);
} else {
const executionProps = confirmationDetails as ToolExecuteConfirmationDetails;
title = "Execute Command"; // Title for the outer box
const executionProps =
confirmationDetails as ToolExecuteConfirmationDetails;
title = 'Execute Command'; // Title for the outer box
// For execution, we still need context display and description
const commandDisplay = <Text color="cyan">{executionProps.command}</Text>;
// Combine command and description into bodyContent for layout consistency
bodyContent = (
<Box flexDirection="column">
<Box paddingX={1} marginLeft={1}>{commandDisplay}</Box>
<Box flexDirection="column">
<Box paddingX={1} marginLeft={1}>
{commandDisplay}
</Box>
</Box>
);
question = `Allow execution?`;
const alwaysLabel = `2. Yes, always allow '${executionProps.rootCommand}' commands`;
options.push(
{ label: '1. Yes, allow once', value: ToolConfirmationOutcome.ProceedOnce },
{ label: alwaysLabel, value: ToolConfirmationOutcome.ProceedAlways },
{ label: '3. No (esc)', value: ToolConfirmationOutcome.Cancel }
{
label: '1. Yes, allow once',
value: ToolConfirmationOutcome.ProceedOnce,
},
{
label: alwaysLabel,
value: ToolConfirmationOutcome.ProceedAlways,
},
{ label: '3. No (esc)', value: ToolConfirmationOutcome.Cancel },
);
}
@@ -82,7 +103,7 @@ const ToolConfirmationMessage: React.FC<ToolConfirmationMessageProps> = ({ confi
{/* Body Content (Diff Renderer or Command Info) */}
{/* No separate context display here anymore for edits */}
<Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
{bodyContent}
{bodyContent}
</Box>
{/* Confirmation Question */}
@@ -98,4 +119,4 @@ const ToolConfirmationMessage: React.FC<ToolConfirmationMessageProps> = ({ confi
);
};
export default ToolConfirmationMessage;
export default ToolConfirmationMessage;

View File

@@ -6,42 +6,45 @@ import { PartListUnion } from '@google/genai';
import ToolConfirmationMessage from './ToolConfirmationMessage.js';
interface ToolGroupMessageProps {
toolCalls: IndividualToolCallDisplay[];
onSubmit: (value: PartListUnion) => void;
toolCalls: IndividualToolCallDisplay[];
onSubmit: (value: PartListUnion) => void;
}
// Main component renders the border and maps the tools using ToolMessage
const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({ toolCalls, onSubmit }) => {
const hasPending = toolCalls.some(t => t.status === ToolCallStatus.Pending);
const borderColor = hasPending ? "yellow" : "blue";
const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls,
onSubmit,
}) => {
const hasPending = toolCalls.some((t) => t.status === ToolCallStatus.Pending);
const borderColor = hasPending ? 'yellow' : 'blue';
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={borderColor}
>
{toolCalls.map((tool) => {
return (
<React.Fragment key={tool.callId}>
<ToolMessage
key={tool.callId} // Use callId as the key
name={tool.name}
description={tool.description}
resultDisplay={tool.resultDisplay}
status={tool.status}
/>
{tool.status === ToolCallStatus.Confirming && tool.confirmationDetails && (
<ToolConfirmationMessage confirmationDetails={tool.confirmationDetails} onSubmit={onSubmit}></ToolConfirmationMessage>
)}
</React.Fragment>
);
})}
{/* Optional: Add padding below the last item if needed,
return (
<Box flexDirection="column" borderStyle="round" borderColor={borderColor}>
{toolCalls.map((tool) => {
return (
<React.Fragment key={tool.callId}>
<ToolMessage
key={tool.callId} // Use callId as the key
name={tool.name}
description={tool.description}
resultDisplay={tool.resultDisplay}
status={tool.status}
/>
{tool.status === ToolCallStatus.Confirming &&
tool.confirmationDetails && (
<ToolConfirmationMessage
confirmationDetails={tool.confirmationDetails}
onSubmit={onSubmit}
></ToolConfirmationMessage>
)}
</React.Fragment>
);
})}
{/* Optional: Add padding below the last item if needed,
though ToolMessage already has some vertical space implicitly */}
{/* {tools.length > 0 && <Box height={1} />} */}
</Box>
);
{/* {tools.length > 0 && <Box height={1} />} */}
</Box>
);
};
export default ToolGroupMessage;

View File

@@ -7,47 +7,68 @@ import DiffRenderer from './DiffRenderer.js';
import { MarkdownRenderer } from '../../utils/MarkdownRenderer.js';
interface ToolMessageProps {
name: string;
description: string;
resultDisplay: ToolResultDisplay | undefined;
status: ToolCallStatus;
name: string;
description: string;
resultDisplay: ToolResultDisplay | undefined;
status: ToolCallStatus;
}
const ToolMessage: React.FC<ToolMessageProps> = ({ name, description, resultDisplay, status }) => {
const statusIndicatorWidth = 3;
const hasResult = (status === ToolCallStatus.Invoked || status === ToolCallStatus.Canceled) && resultDisplay && resultDisplay.toString().trim().length > 0;
const ToolMessage: React.FC<ToolMessageProps> = ({
name,
description,
resultDisplay,
status,
}) => {
const statusIndicatorWidth = 3;
const hasResult =
(status === ToolCallStatus.Invoked || status === ToolCallStatus.Canceled) &&
resultDisplay &&
resultDisplay.toString().trim().length > 0;
return (
<Box paddingX={1} paddingY={0} flexDirection="column">
{/* Row for Status Indicator and Tool Info */}
<Box minHeight={1}>
{/* Status Indicator */}
<Box minWidth={statusIndicatorWidth}>
{status === ToolCallStatus.Pending && <Spinner type="dots" />}
{status === ToolCallStatus.Invoked && <Text color="green"></Text>}
{status === ToolCallStatus.Confirming && <Text color="blue">?</Text>}
{status === ToolCallStatus.Canceled && <Text color="red" bold>-</Text>}
</Box>
<Box>
<Text color="blue" wrap="truncate-end" strikethrough={status === ToolCallStatus.Canceled}>
<Text bold>{name}</Text> <Text color="gray">{description}</Text>
</Text>
</Box>
</Box>
{hasResult && (
<Box paddingLeft={statusIndicatorWidth}>
<Box flexShrink={1} flexDirection="row">
<Text color="gray"> </Text>
{/* Use default text color (white) or gray instead of dimColor */}
{typeof resultDisplay === 'string' && <Box flexDirection='column'>{MarkdownRenderer.render(resultDisplay)}</Box>}
{typeof resultDisplay === 'object' && <DiffRenderer diffContent={resultDisplay.fileDiff} />}
</Box>
</Box>
)}
return (
<Box paddingX={1} paddingY={0} flexDirection="column">
{/* Row for Status Indicator and Tool Info */}
<Box minHeight={1}>
{/* Status Indicator */}
<Box minWidth={statusIndicatorWidth}>
{status === ToolCallStatus.Pending && <Spinner type="dots" />}
{status === ToolCallStatus.Invoked && <Text color="green"></Text>}
{status === ToolCallStatus.Confirming && <Text color="blue">?</Text>}
{status === ToolCallStatus.Canceled && (
<Text color="red" bold>
-
</Text>
)}
</Box>
);
<Box>
<Text
color="blue"
wrap="truncate-end"
strikethrough={status === ToolCallStatus.Canceled}
>
<Text bold>{name}</Text> <Text color="gray">{description}</Text>
</Text>
</Box>
</Box>
{hasResult && (
<Box paddingLeft={statusIndicatorWidth}>
<Box flexShrink={1} flexDirection="row">
<Text color="gray"> </Text>
{/* Use default text color (white) or gray instead of dimColor */}
{typeof resultDisplay === 'string' && (
<Box flexDirection="column">
{MarkdownRenderer.render(resultDisplay)}
</Box>
)}
{typeof resultDisplay === 'object' && (
<DiffRenderer diffContent={resultDisplay.fileDiff} />
)}
</Box>
</Box>
)}
</Box>
);
};
export default ToolMessage;

View File

@@ -2,23 +2,23 @@ import React from 'react';
import { Text, Box } from 'ink';
interface UserMessageProps {
text: string;
text: string;
}
const UserMessage: React.FC<UserMessageProps> = ({ text }) => {
const prefix = '> ';
const prefixWidth = prefix.length;
const prefix = '> ';
const prefixWidth = prefix.length;
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="gray">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap">{text}</Text>
</Box>
</Box>
);
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color="gray">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap">{text}</Text>
</Box>
</Box>
);
};
export default UserMessage;
export default UserMessage;