feat(cli): add deep work completion summary stats

This commit is contained in:
Dmitry Lyalin
2026-02-12 23:36:31 -05:00
parent c57b55fa03
commit 5ab0c1e31b
6 changed files with 350 additions and 9 deletions
@@ -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 {