mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
Fix ESC.
This commit is contained in:
@@ -128,4 +128,19 @@ describe('<SubagentProgressDisplay />', () => {
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('💭 Thinking about life');
|
||||
});
|
||||
|
||||
it('renders cancelled state correctly', () => {
|
||||
const progress: SubagentProgress = {
|
||||
isSubagentProgress: true,
|
||||
agentName: 'TestAgent',
|
||||
recentActivity: [],
|
||||
state: 'cancelled',
|
||||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<SubagentProgressDisplay progress={progress} />,
|
||||
);
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Subagent TestAgent was cancelled.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,11 +41,26 @@ const formatToolArgs = (args?: string): string => {
|
||||
|
||||
export const SubagentProgressDisplay: React.FC<
|
||||
SubagentProgressDisplayProps
|
||||
> = ({ progress }) => (
|
||||
> = ({ progress }) => {
|
||||
let headerText = `Subagent ${progress.agentName} is working...`;
|
||||
let headerColor = theme.text.secondary;
|
||||
|
||||
if (progress.state === 'cancelled') {
|
||||
headerText = `Subagent ${progress.agentName} was cancelled.`;
|
||||
headerColor = theme.status.warning;
|
||||
} else if (progress.state === 'error') {
|
||||
headerText = `Subagent ${progress.agentName} failed.`;
|
||||
headerColor = theme.status.error;
|
||||
} else if (progress.state === 'completed') {
|
||||
headerText = `Subagent ${progress.agentName} completed.`;
|
||||
headerColor = theme.status.success;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingY={0}>
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
Subagent {progress.agentName} is working...
|
||||
<Text color={headerColor} italic>
|
||||
{headerText}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column" marginLeft={0} gap={0}>
|
||||
@@ -104,3 +119,4 @@ export const SubagentProgressDisplay: React.FC<
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ValidatingToolCall,
|
||||
WaitingToolCall,
|
||||
CancelledToolCall,
|
||||
AnsiOutput,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { CoreToolScheduler } from '@google/gemini-cli-core';
|
||||
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
||||
@@ -137,6 +138,41 @@ export function useReactToolScheduler(
|
||||
pid: coreTc.pid,
|
||||
};
|
||||
} else {
|
||||
// If the tool was cancelled and has no resultDisplay (generic cancellation),
|
||||
// try to preserve the liveOutput from the executing state.
|
||||
// This allows rich displays (like subagent progress) to show their cancelled state.
|
||||
if (
|
||||
coreTc.status === 'cancelled' &&
|
||||
!coreTc.response.resultDisplay &&
|
||||
existingTrackedCall &&
|
||||
'liveOutput' in existingTrackedCall &&
|
||||
existingTrackedCall.liveOutput
|
||||
) {
|
||||
let resultDisplay = existingTrackedCall.liveOutput;
|
||||
// If it's a subagent progress, ensure the state reflects cancellation
|
||||
if (
|
||||
typeof resultDisplay === 'object' &&
|
||||
resultDisplay !== null &&
|
||||
'isSubagentProgress' in resultDisplay &&
|
||||
(resultDisplay as { isSubagentProgress: boolean })
|
||||
.isSubagentProgress
|
||||
) {
|
||||
resultDisplay = {
|
||||
...resultDisplay,
|
||||
state: 'cancelled',
|
||||
} as unknown as AnsiOutput; // Cast to satisfy type system
|
||||
}
|
||||
|
||||
return {
|
||||
...coreTc,
|
||||
responseSubmittedToGemini,
|
||||
response: {
|
||||
...coreTc.response,
|
||||
resultDisplay,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...coreTc,
|
||||
responseSubmittedToGemini,
|
||||
|
||||
@@ -337,5 +337,25 @@ describe('LocalSubagentInvocation', () => {
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.llmContent).toContain('Aborted');
|
||||
});
|
||||
|
||||
it('should return cancelled state when execution returns ABORTED', async () => {
|
||||
const mockOutput = {
|
||||
result: 'Cancelled by user',
|
||||
terminate_reason: AgentTerminateMode.ABORTED,
|
||||
};
|
||||
mockExecutorInstance.run.mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await invocation.execute(signal, updateOutput);
|
||||
|
||||
expect(result.llmContent).toEqual(
|
||||
expect.stringContaining(
|
||||
"Subagent 'MockAgent' was cancelled by the user.",
|
||||
),
|
||||
);
|
||||
|
||||
const display = result.returnDisplay as SubagentProgress;
|
||||
expect(display.isSubagentProgress).toBe(true);
|
||||
expect(display.state).toBe('cancelled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,12 +8,13 @@ import type { Config } from '../config/config.js';
|
||||
import { LocalAgentExecutor } from './local-executor.js';
|
||||
import type { AnsiOutput } from '../utils/terminalSerializer.js';
|
||||
import { BaseToolInvocation, type ToolResult } from '../tools/tools.js';
|
||||
import type {
|
||||
LocalAgentDefinition,
|
||||
AgentInputs,
|
||||
SubagentActivityEvent,
|
||||
SubagentProgress,
|
||||
SubagentActivityItem,
|
||||
import {
|
||||
type LocalAgentDefinition,
|
||||
type AgentInputs,
|
||||
type SubagentActivityEvent,
|
||||
type SubagentProgress,
|
||||
type SubagentActivityItem,
|
||||
AgentTerminateMode,
|
||||
} from './types.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
|
||||
@@ -94,6 +95,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [],
|
||||
state: 'running',
|
||||
};
|
||||
updateOutput(initialProgress as unknown as AnsiOutput);
|
||||
}
|
||||
@@ -173,6 +175,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity], // Copy to avoid mutation issues
|
||||
state: 'running',
|
||||
};
|
||||
|
||||
updateOutput(progress as unknown as AnsiOutput);
|
||||
@@ -187,6 +190,24 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
|
||||
const output = await executor.run(this.params, signal);
|
||||
|
||||
if (output.terminate_reason === AgentTerminateMode.ABORTED) {
|
||||
const progress: SubagentProgress = {
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity],
|
||||
state: 'cancelled',
|
||||
};
|
||||
|
||||
if (updateOutput) {
|
||||
updateOutput(progress as unknown as AnsiOutput);
|
||||
}
|
||||
|
||||
return {
|
||||
llmContent: `Subagent '${this.definition.name}' was cancelled by the user.`,
|
||||
returnDisplay: progress,
|
||||
};
|
||||
}
|
||||
|
||||
const resultContent = `Subagent '${this.definition.name}' finished.
|
||||
Termination Reason: ${output.terminate_reason}
|
||||
Result:
|
||||
@@ -209,17 +230,31 @@ ${output.result}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
const isAbort =
|
||||
(error instanceof Error && error.name === 'AbortError') ||
|
||||
errorMessage.includes('Aborted');
|
||||
|
||||
// Mark any running items as error/cancelled
|
||||
for (const item of recentActivity) {
|
||||
if (item.status === 'running') {
|
||||
item.status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the error is reflected in the recent activity for display
|
||||
const lastActivity = recentActivity[recentActivity.length - 1];
|
||||
if (!lastActivity || lastActivity.status !== 'error') {
|
||||
recentActivity.push({
|
||||
type: 'thought',
|
||||
content: `Error: ${errorMessage}`,
|
||||
status: 'error',
|
||||
});
|
||||
// Maintain size limit
|
||||
if (recentActivity.length > MAX_RECENT_ACTIVITY) {
|
||||
recentActivity = recentActivity.slice(-MAX_RECENT_ACTIVITY);
|
||||
// But only if it's NOT an abort, or if we want to show "Cancelled" as a thought
|
||||
if (!isAbort) {
|
||||
const lastActivity = recentActivity[recentActivity.length - 1];
|
||||
if (!lastActivity || lastActivity.status !== 'error') {
|
||||
recentActivity.push({
|
||||
type: 'thought',
|
||||
content: `Error: ${errorMessage}`,
|
||||
status: 'error',
|
||||
});
|
||||
// Maintain size limit
|
||||
if (recentActivity.length > MAX_RECENT_ACTIVITY) {
|
||||
recentActivity = recentActivity.slice(-MAX_RECENT_ACTIVITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,8 +262,13 @@ ${output.result}
|
||||
isSubagentProgress: true,
|
||||
agentName: this.definition.name,
|
||||
recentActivity: [...recentActivity],
|
||||
state: isAbort ? 'cancelled' : 'error',
|
||||
};
|
||||
|
||||
if (updateOutput) {
|
||||
updateOutput(progress as unknown as AnsiOutput);
|
||||
}
|
||||
|
||||
return {
|
||||
llmContent: `Subagent '${this.definition.name}' failed. Error: ${errorMessage}`,
|
||||
returnDisplay: progress,
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface SubagentProgress {
|
||||
isSubagentProgress: true;
|
||||
agentName: string;
|
||||
recentActivity: SubagentActivityItem[];
|
||||
state?: 'running' | 'completed' | 'error' | 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ToolResult,
|
||||
Config,
|
||||
AnsiOutput,
|
||||
ToolResultDisplay,
|
||||
} from '../index.js';
|
||||
import {
|
||||
ToolErrorType,
|
||||
@@ -119,6 +120,7 @@ export class ToolExecutor {
|
||||
return this.createCancelledResult(
|
||||
call,
|
||||
'User cancelled tool execution.',
|
||||
toolResult.returnDisplay,
|
||||
);
|
||||
} else if (toolResult.error === undefined) {
|
||||
return await this.createSuccessResult(call, toolResult);
|
||||
@@ -154,6 +156,7 @@ export class ToolExecutor {
|
||||
private createCancelledResult(
|
||||
call: ToolCall,
|
||||
reason: string,
|
||||
resultDisplay?: ToolResultDisplay,
|
||||
): CancelledToolCall {
|
||||
const errorMessage = `[Operation Cancelled] ${reason}`;
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
@@ -178,7 +181,7 @@ export class ToolExecutor {
|
||||
},
|
||||
},
|
||||
],
|
||||
resultDisplay: undefined,
|
||||
resultDisplay,
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: errorMessage.length,
|
||||
|
||||
Reference in New Issue
Block a user