mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
feat(cli): add deep work completion summary stats
This commit is contained in:
@@ -190,6 +190,43 @@ describe('ToolResultDisplay', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders detailed deep work summary display', () => {
|
||||
const deepWorkSummary = {
|
||||
type: 'deep_work_summary' as const,
|
||||
status: 'completed' as const,
|
||||
runId: 'deep-work-run-42',
|
||||
executionCount: 4,
|
||||
maxRuns: 8,
|
||||
elapsedSeconds: 372,
|
||||
maxTimeMinutes: 90,
|
||||
answeredRequiredQuestions: 3,
|
||||
totalRequiredQuestions: 4,
|
||||
readinessVerdict: 'ready' as const,
|
||||
completionPromise: 'MIGRATION_DONE',
|
||||
approvedPlanPath: '/tmp/plans/deep-work-auth.md',
|
||||
reason: 'All acceptance criteria completed.',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={deepWorkSummary}
|
||||
terminalWidth={100}
|
||||
availableTerminalHeight={24}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame() || '';
|
||||
|
||||
expect(output).toContain('Deep Work Summary');
|
||||
expect(output).toContain('COMPLETED');
|
||||
expect(output).toContain('Executions: 4/8');
|
||||
expect(output).toContain('Runtime: 6m 12s / 90m');
|
||||
expect(output).toContain('Required Context: 3/4');
|
||||
expect(output).toContain('Readiness: ready');
|
||||
expect(output).toContain('Completion Signal: MIGRATION_DONE');
|
||||
expect(output).toContain('Plan Context: /tmp/plans/deep-work-auth.md');
|
||||
expect(output).toContain('Reason: All acceptance criteria completed.');
|
||||
expect(output).toContain('Run ID: deep-work-run-42');
|
||||
});
|
||||
|
||||
it('does not fall back to plain text if availableHeight is set and not in alternate buffer', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
|
||||
|
||||
@@ -11,7 +11,11 @@ import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { AnsiOutput, AnsiLine } from '@google/gemini-cli-core';
|
||||
import type {
|
||||
AnsiOutput,
|
||||
AnsiLine,
|
||||
DeepWorkSummaryDisplay,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
@@ -42,6 +46,49 @@ interface FileDiffResult {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
function isDeepWorkSummaryDisplay(
|
||||
value: unknown,
|
||||
): value is DeepWorkSummaryDisplay {
|
||||
return (
|
||||
!!value &&
|
||||
typeof value === 'object' &&
|
||||
'type' in value &&
|
||||
value.type === 'deep_work_summary'
|
||||
);
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number | null): string {
|
||||
if (seconds === null) {
|
||||
return 'n/a';
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
return `${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
function getDeepWorkStatusColor(
|
||||
status: DeepWorkSummaryDisplay['status'],
|
||||
): string {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return theme.status.success;
|
||||
case 'rejected':
|
||||
return theme.status.error;
|
||||
case 'paused':
|
||||
case 'stopped':
|
||||
return theme.status.warning;
|
||||
default:
|
||||
return theme.text.accent;
|
||||
}
|
||||
}
|
||||
|
||||
export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
resultDisplay,
|
||||
availableTerminalHeight,
|
||||
@@ -115,6 +162,81 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
isDeepWorkSummaryDisplay(truncatedResultDisplay)
|
||||
) {
|
||||
const summary = truncatedResultDisplay;
|
||||
const executionBudgetUsed =
|
||||
summary.maxRuns > 0
|
||||
? Math.min(
|
||||
100,
|
||||
Math.round((summary.executionCount / summary.maxRuns) * 100),
|
||||
)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Box width={childWidth} flexDirection="column">
|
||||
<Text color={theme.text.accent} bold>
|
||||
Deep Work Summary
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Status: </Text>
|
||||
<Text color={getDeepWorkStatusColor(summary.status)} bold>
|
||||
{summary.status.toUpperCase()}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Executions: </Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{summary.executionCount}/{summary.maxRuns} ({executionBudgetUsed}%
|
||||
budget used)
|
||||
</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Runtime: </Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{formatDuration(summary.elapsedSeconds)} / {summary.maxTimeMinutes}m
|
||||
</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Required Context: </Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{summary.answeredRequiredQuestions}/{summary.totalRequiredQuestions}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Readiness: </Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{summary.readinessVerdict ?? 'n/a'}
|
||||
</Text>
|
||||
</Text>
|
||||
{summary.completionPromise && (
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Completion Signal: </Text>
|
||||
<Text color={theme.text.primary}>{summary.completionPromise}</Text>
|
||||
</Text>
|
||||
)}
|
||||
{summary.approvedPlanPath && (
|
||||
<Text wrap="wrap">
|
||||
<Text color={theme.text.secondary}>Plan Context: </Text>
|
||||
<Text color={theme.text.primary}>{summary.approvedPlanPath}</Text>
|
||||
</Text>
|
||||
)}
|
||||
{summary.reason && (
|
||||
<Text wrap="wrap">
|
||||
<Text color={theme.text.secondary}>Reason: </Text>
|
||||
<Text color={theme.text.primary}>{summary.reason}</Text>
|
||||
</Text>
|
||||
)}
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>Run ID: </Text>
|
||||
<Text color={theme.text.primary}>{summary.runId}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. High-performance path: Virtualized ANSI in interactive mode
|
||||
if (isAlternateBuffer && Array.isArray(truncatedResultDisplay)) {
|
||||
// If availableHeight is undefined, fallback to a safe default to prevents infinite loop
|
||||
|
||||
@@ -1167,6 +1167,27 @@ function toToolCallContent(toolResult: ToolResult): acp.ToolCallContent | null {
|
||||
newText: toolResult.returnDisplay.newContent,
|
||||
};
|
||||
}
|
||||
if (
|
||||
'type' in toolResult.returnDisplay &&
|
||||
toolResult.returnDisplay.type === 'deep_work_summary'
|
||||
) {
|
||||
const summary = toolResult.returnDisplay;
|
||||
const elapsed =
|
||||
typeof summary.elapsedSeconds === 'number'
|
||||
? `${summary.elapsedSeconds}s`
|
||||
: 'n/a';
|
||||
const text = [
|
||||
`Deep Work summary (${summary.status})`,
|
||||
`Executions: ${summary.executionCount}/${summary.maxRuns}`,
|
||||
`Required context: ${summary.answeredRequiredQuestions}/${summary.totalRequiredQuestions}`,
|
||||
`Elapsed: ${elapsed} / ${summary.maxTimeMinutes}m`,
|
||||
`Readiness: ${summary.readinessVerdict ?? 'n/a'}`,
|
||||
].join('\n');
|
||||
return {
|
||||
type: 'content',
|
||||
content: { type: 'text', text },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user