feat: add emphasis to tool confirmations (#502)
This commit is contained in:
parent
1d0856dcc8
commit
01971741e0
|
@ -10403,6 +10403,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"fast-glob": "^3.3.3",
|
||||
"shell-quote": "^1.8.2",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
"bundle": "esbuild packages/cli/index.ts --bundle --outfile=bundle/gemini.js --platform=node --format=esm --banner:js=\"import { createRequire } from 'module'; const require = createRequire(import.meta.url); globalThis.__filename = require('url').fileURLToPath(import.meta.url); globalThis.__dirname = require('path').dirname(globalThis.__filename);\" && bash scripts/copy_bundle_assets.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"esbuild": "^0.25.4",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
|
|
|
@ -29,6 +29,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
|
||||
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
|
||||
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
// note, after the CTA, this automatically moves over to the next 'confirming' tool
|
||||
const toolAwaitingApproval = useMemo(
|
||||
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls],
|
||||
|
@ -50,27 +52,38 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
borderColor={borderColor}
|
||||
marginBottom={1}
|
||||
>
|
||||
{toolCalls.map((tool) => (
|
||||
<Box key={tool.callId} flexDirection="column">
|
||||
<ToolMessage
|
||||
key={tool.callId}
|
||||
callId={tool.callId}
|
||||
name={tool.name}
|
||||
description={tool.description}
|
||||
resultDisplay={tool.resultDisplay}
|
||||
status={tool.status}
|
||||
confirmationDetails={tool.confirmationDetails}
|
||||
availableTerminalHeight={availableTerminalHeight - staticHeight}
|
||||
/>
|
||||
{tool.status === ToolCallStatus.Confirming &&
|
||||
tool.callId === toolAwaitingApproval?.callId &&
|
||||
tool.confirmationDetails && (
|
||||
<ToolConfirmationMessage
|
||||
{toolCalls.map((tool) => {
|
||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||
return (
|
||||
<Box key={tool.callId} flexDirection="column">
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<ToolMessage
|
||||
callId={tool.callId}
|
||||
name={tool.name}
|
||||
description={tool.description}
|
||||
resultDisplay={tool.resultDisplay}
|
||||
status={tool.status}
|
||||
confirmationDetails={tool.confirmationDetails}
|
||||
></ToolConfirmationMessage>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
availableTerminalHeight={availableTerminalHeight - staticHeight}
|
||||
emphasis={
|
||||
isConfirming
|
||||
? 'high'
|
||||
: toolAwaitingApproval
|
||||
? 'low'
|
||||
: 'medium'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{tool.status === ToolCallStatus.Confirming &&
|
||||
isConfirming &&
|
||||
tool.confirmationDetails && (
|
||||
<ToolConfirmationMessage
|
||||
confirmationDetails={tool.confirmationDetails}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,8 +12,15 @@ import { DiffRenderer } from './DiffRenderer.js';
|
|||
import { Colors } from '../../colors.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
||||
const STATUS_INDICATOR_WIDTH = 3;
|
||||
|
||||
export type TextEmphasis = 'high' | 'medium' | 'low';
|
||||
|
||||
export interface ToolMessageProps extends IndividualToolCallDisplay {
|
||||
availableTerminalHeight: number;
|
||||
emphasis?: TextEmphasis;
|
||||
}
|
||||
|
||||
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
|
@ -22,63 +29,45 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
resultDisplay,
|
||||
status,
|
||||
availableTerminalHeight,
|
||||
emphasis = 'medium',
|
||||
}) => {
|
||||
const statusIndicatorWidth = 3;
|
||||
const hasResult = resultDisplay && resultDisplay.toString().trim().length > 0;
|
||||
const staticHeight = /* Header */ 1;
|
||||
|
||||
let displayableResult = resultDisplay;
|
||||
let hiddenLines = 0;
|
||||
const contentHeightEstimate =
|
||||
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT;
|
||||
const resultIsString =
|
||||
typeof resultDisplay === 'string' && resultDisplay.trim().length > 0;
|
||||
const lines = React.useMemo(
|
||||
() => (resultIsString ? resultDisplay.split('\n') : []),
|
||||
[resultIsString, resultDisplay],
|
||||
);
|
||||
|
||||
// Truncate the overall string content if it's too long.
|
||||
// MarkdownRenderer will handle specific truncation for code blocks within this content.
|
||||
if (typeof resultDisplay === 'string' && resultDisplay.length > 0) {
|
||||
const lines = resultDisplay.split('\n');
|
||||
// Estimate available height for this specific tool message content area
|
||||
// This is a rough estimate; ideally, we'd have a more precise measurement.
|
||||
const contentHeightEstimate = availableTerminalHeight - staticHeight - 5; // Subtracting lines for tool name, status, padding etc.
|
||||
if (lines.length > contentHeightEstimate && contentHeightEstimate > 0) {
|
||||
displayableResult = lines.slice(0, contentHeightEstimate).join('\n');
|
||||
hiddenLines = lines.length - contentHeightEstimate;
|
||||
}
|
||||
}
|
||||
// Estimate available height for this specific tool message content area
|
||||
// This is a rough estimate; ideally, we'd have a more precise measurement.
|
||||
const displayableResult = React.useMemo(
|
||||
() =>
|
||||
resultIsString
|
||||
? lines.slice(0, contentHeightEstimate).join('\n')
|
||||
: resultDisplay,
|
||||
[lines, resultIsString, contentHeightEstimate, resultDisplay],
|
||||
);
|
||||
const hiddenLines = lines.length - contentHeightEstimate;
|
||||
|
||||
return (
|
||||
<Box paddingX={1} paddingY={0} flexDirection="column">
|
||||
<Box minHeight={1}>
|
||||
{/* Status Indicator */}
|
||||
<Box minWidth={statusIndicatorWidth}>
|
||||
{(status === ToolCallStatus.Pending ||
|
||||
status === ToolCallStatus.Executing) && <Spinner type="dots" />}
|
||||
{status === ToolCallStatus.Success && (
|
||||
<Text color={Colors.AccentGreen}>✔</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Confirming && (
|
||||
<Text color={Colors.AccentYellow}>?</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Canceled && (
|
||||
<Text color={Colors.AccentYellow} bold>
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Error && (
|
||||
<Text color={Colors.AccentRed} bold>
|
||||
x
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Text
|
||||
wrap="truncate-end"
|
||||
strikethrough={status === ToolCallStatus.Canceled}
|
||||
>
|
||||
<Text bold>{name}</Text>{' '}
|
||||
<Text color={Colors.SubtleComment}>{description}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<ToolStatusIndicator status={status} />
|
||||
<ToolInfo
|
||||
name={name}
|
||||
status={status}
|
||||
description={description}
|
||||
emphasis={emphasis}
|
||||
/>
|
||||
{emphasis === 'high' && <TrailingIndicator />}
|
||||
</Box>
|
||||
{hasResult && (
|
||||
<Box paddingLeft={statusIndicatorWidth} width="100%">
|
||||
{displayableResult && (
|
||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} width="100%">
|
||||
<Box flexDirection="column">
|
||||
{typeof displayableResult === 'string' && (
|
||||
<Box flexDirection="column">
|
||||
|
@ -89,7 +78,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
/>
|
||||
</Box>
|
||||
)}
|
||||
{typeof displayableResult === 'object' && (
|
||||
{typeof displayableResult !== 'string' && (
|
||||
<DiffRenderer
|
||||
diffContent={displayableResult.fileDiff}
|
||||
filename={displayableResult.fileName}
|
||||
|
@ -109,3 +98,76 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
type ToolStatusIndicator = {
|
||||
status: ToolCallStatus;
|
||||
};
|
||||
const ToolStatusIndicator: React.FC<ToolStatusIndicator> = ({ status }) => (
|
||||
<Box minWidth={STATUS_INDICATOR_WIDTH}>
|
||||
{status === ToolCallStatus.Pending && (
|
||||
<Text color={Colors.AccentGreen}>o</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Executing && <Spinner type="dots" />}
|
||||
{status === ToolCallStatus.Success && (
|
||||
<Text color={Colors.AccentGreen}>✔</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Confirming && (
|
||||
<Text color={Colors.AccentYellow}>?</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Canceled && (
|
||||
<Text color={Colors.AccentYellow} bold>
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Error && (
|
||||
<Text color={Colors.AccentRed} bold>
|
||||
x
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
type ToolInfo = {
|
||||
name: string;
|
||||
description: string;
|
||||
status: ToolCallStatus;
|
||||
emphasis: TextEmphasis;
|
||||
};
|
||||
const ToolInfo: React.FC<ToolInfo> = ({
|
||||
name,
|
||||
description,
|
||||
status,
|
||||
emphasis,
|
||||
}) => {
|
||||
const nameColor = React.useMemo<string>(() => {
|
||||
switch (emphasis) {
|
||||
case 'high':
|
||||
return Colors.Foreground;
|
||||
case 'medium':
|
||||
return Colors.Foreground;
|
||||
case 'low':
|
||||
return Colors.SubtleComment;
|
||||
default: {
|
||||
const exhaustiveCheck: never = emphasis;
|
||||
return exhaustiveCheck;
|
||||
}
|
||||
}
|
||||
}, [emphasis]);
|
||||
return (
|
||||
<Box>
|
||||
<Text
|
||||
wrap="truncate-end"
|
||||
strikethrough={status === ToolCallStatus.Canceled}
|
||||
>
|
||||
<Text color={nameColor} bold>
|
||||
{name}
|
||||
</Text>{' '}
|
||||
<Text color={Colors.SubtleComment}>{description}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TrailingIndicator: React.FC = () => (
|
||||
<Text color={Colors.Foreground}> ←</Text>
|
||||
);
|
||||
|
|
|
@ -184,48 +184,53 @@ export function useToolScheduler(
|
|||
|
||||
useEffect(() => {
|
||||
// effect for executing scheduled tool calls
|
||||
if (toolCalls.every((t) => t.status === 'scheduled')) {
|
||||
const allToolsConfirmed = toolCalls.every(
|
||||
(t) => t.status === 'scheduled' || t.status === 'cancelled',
|
||||
);
|
||||
if (allToolsConfirmed) {
|
||||
const signal = abortController.signal;
|
||||
toolCalls.forEach((c) => {
|
||||
const callId = c.request.callId;
|
||||
setToolCalls(setStatus(c.request.callId, 'executing'));
|
||||
c.tool
|
||||
.execute(c.request.args, signal)
|
||||
.then((result) => {
|
||||
if (signal.aborted) {
|
||||
setToolCalls(
|
||||
setStatus(callId, 'cancelled', 'Cancelled during execution'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const functionResponse: Part = {
|
||||
functionResponse: {
|
||||
name: c.request.name,
|
||||
id: callId,
|
||||
response: { output: result.llmContent },
|
||||
},
|
||||
};
|
||||
const response: ToolCallResponseInfo = {
|
||||
callId,
|
||||
responsePart: functionResponse,
|
||||
resultDisplay: result.returnDisplay,
|
||||
error: undefined,
|
||||
};
|
||||
setToolCalls(setStatus(callId, 'success', response));
|
||||
})
|
||||
.catch((e) =>
|
||||
setToolCalls(
|
||||
setStatus(
|
||||
toolCalls
|
||||
.filter((t) => t.status === 'scheduled')
|
||||
.forEach((t) => {
|
||||
const callId = t.request.callId;
|
||||
setToolCalls(setStatus(t.request.callId, 'executing'));
|
||||
t.tool
|
||||
.execute(t.request.args, signal)
|
||||
.then((result) => {
|
||||
if (signal.aborted) {
|
||||
setToolCalls(
|
||||
setStatus(callId, 'cancelled', 'Cancelled during execution'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const functionResponse: Part = {
|
||||
functionResponse: {
|
||||
name: t.request.name,
|
||||
id: callId,
|
||||
response: { output: result.llmContent },
|
||||
},
|
||||
};
|
||||
const response: ToolCallResponseInfo = {
|
||||
callId,
|
||||
'error',
|
||||
toolErrorResponse(
|
||||
c.request,
|
||||
e instanceof Error ? e : new Error(String(e)),
|
||||
responsePart: functionResponse,
|
||||
resultDisplay: result.returnDisplay,
|
||||
error: undefined,
|
||||
};
|
||||
setToolCalls(setStatus(callId, 'success', response));
|
||||
})
|
||||
.catch((e) =>
|
||||
setToolCalls(
|
||||
setStatus(
|
||||
callId,
|
||||
'error',
|
||||
toolErrorResponse(
|
||||
t.request,
|
||||
e instanceof Error ? e : new Error(String(e)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [toolCalls, toolRegistry, abortController.signal]);
|
||||
|
||||
|
|
Loading…
Reference in New Issue