diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
index 797e405b62..fa1883958f 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
@@ -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(
+ ,
+ );
+ 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
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
index 61f1540017..c9831f6aca 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
@@ -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 = ({
resultDisplay,
availableTerminalHeight,
@@ -115,6 +162,81 @@ export const ToolResultDisplay: React.FC = ({
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 (
+
+
+ Deep Work Summary
+
+
+ Status:
+
+ {summary.status.toUpperCase()}
+
+
+
+ Executions:
+
+ {summary.executionCount}/{summary.maxRuns} ({executionBudgetUsed}%
+ budget used)
+
+
+
+ Runtime:
+
+ {formatDuration(summary.elapsedSeconds)} / {summary.maxTimeMinutes}m
+
+
+
+ Required Context:
+
+ {summary.answeredRequiredQuestions}/{summary.totalRequiredQuestions}
+
+
+
+ Readiness:
+
+ {summary.readinessVerdict ?? 'n/a'}
+
+
+ {summary.completionPromise && (
+
+ Completion Signal:
+ {summary.completionPromise}
+
+ )}
+ {summary.approvedPlanPath && (
+
+ Plan Context:
+ {summary.approvedPlanPath}
+
+ )}
+ {summary.reason && (
+
+ Reason:
+ {summary.reason}
+
+ )}
+
+ Run ID:
+ {summary.runId}
+
+
+ );
+ }
+
// 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
diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts
index 57d8dec3a8..f6bb87c03b 100644
--- a/packages/cli/src/zed-integration/zedIntegration.ts
+++ b/packages/cli/src/zed-integration/zedIntegration.ts
@@ -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 {
diff --git a/packages/core/src/tools/deep-work-tools.test.ts b/packages/core/src/tools/deep-work-tools.test.ts
index a29f4862e0..cc41182745 100644
--- a/packages/core/src/tools/deep-work-tools.test.ts
+++ b/packages/core/src/tools/deep-work-tools.test.ts
@@ -12,7 +12,10 @@ import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { saveDeepWorkState } from '../services/deepWorkState.js';
-import { StartDeepWorkRunTool } from './deep-work-tools.js';
+import {
+ StartDeepWorkRunTool,
+ StopDeepWorkRunTool,
+} from './deep-work-tools.js';
describe('StartDeepWorkRunTool', () => {
let tempRootDir: string;
@@ -84,4 +87,84 @@ describe('StartDeepWorkRunTool', () => {
`Plan context: ${persistedPlanPath}.`,
);
});
+
+ it('returns detailed Deep Work summary stats when run is completed', async () => {
+ const now = new Date();
+ const startedAt = new Date(now.getTime() - 125_000).toISOString();
+ await saveDeepWorkState(mockConfig as Config, {
+ runId: 'deep-work-run-2',
+ status: 'running',
+ prompt: 'Implement and verify multi-step auth migration.',
+ approvedPlanPath: '/tmp/deep-plan.md',
+ maxRuns: 8,
+ maxTimeMinutes: 90,
+ completionPromise: 'AUTH_MIGRATION_DONE',
+ requiredQuestions: [
+ {
+ id: 'scope',
+ question: 'What is the migration scope?',
+ required: true,
+ answer: 'Auth + session layer',
+ done: true,
+ updatedAt: startedAt,
+ },
+ {
+ id: 'fallback',
+ question: 'Rollback strategy?',
+ required: true,
+ answer: '',
+ done: false,
+ updatedAt: startedAt,
+ },
+ ],
+ iteration: 3,
+ createdAt: startedAt,
+ startedAt,
+ lastUpdatedAt: startedAt,
+ rejectionReason: null,
+ readinessReport: {
+ verdict: 'ready',
+ missingRequiredQuestionIds: [],
+ followUpQuestions: [],
+ blockingReasons: [],
+ singleShotRecommendation: false,
+ reviewer: 'heuristic',
+ generatedAt: startedAt,
+ },
+ });
+
+ const tool = new StopDeepWorkRunTool(
+ mockConfig as Config,
+ mockMessageBus as unknown as MessageBus,
+ );
+
+ const result = await tool
+ .build({ mode: 'completed' })
+ .execute(new AbortController().signal);
+
+ expect(typeof result.returnDisplay).toBe('object');
+ if (
+ typeof result.returnDisplay !== 'object' ||
+ !result.returnDisplay ||
+ !('type' in result.returnDisplay) ||
+ result.returnDisplay.type !== 'deep_work_summary'
+ ) {
+ return;
+ }
+
+ expect(result.returnDisplay).toMatchObject({
+ type: 'deep_work_summary',
+ status: 'completed',
+ runId: 'deep-work-run-2',
+ executionCount: 3,
+ maxRuns: 8,
+ maxTimeMinutes: 90,
+ answeredRequiredQuestions: 1,
+ totalRequiredQuestions: 2,
+ readinessVerdict: 'ready',
+ completionPromise: 'AUTH_MIGRATION_DONE',
+ approvedPlanPath: '/tmp/deep-plan.md',
+ });
+ expect(result.returnDisplay.elapsedSeconds).toBeGreaterThan(0);
+ });
});
diff --git a/packages/core/src/tools/deep-work-tools.ts b/packages/core/src/tools/deep-work-tools.ts
index d1fac695ba..625970d8bb 100644
--- a/packages/core/src/tools/deep-work-tools.ts
+++ b/packages/core/src/tools/deep-work-tools.ts
@@ -8,7 +8,9 @@ import {
BaseDeclarativeTool,
BaseToolInvocation,
Kind,
+ type DeepWorkSummaryDisplay,
type ToolResult,
+ type ToolResultDisplay,
type ToolInvocation,
} from './tools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
@@ -64,7 +66,7 @@ export interface StopDeepWorkRunParams {
function createSuccessResult(
llmContent: string,
- returnDisplay: string,
+ returnDisplay: ToolResultDisplay,
data?: Record,
): ToolResult {
return {
@@ -74,6 +76,23 @@ function createSuccessResult(
};
}
+function getElapsedSeconds(
+ startedAt: string | null,
+ endedAt: string,
+): number | null {
+ if (!startedAt) {
+ return null;
+ }
+
+ const startMs = Date.parse(startedAt);
+ const endMs = Date.parse(endedAt);
+ if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
+ return null;
+ }
+
+ return Math.max(0, Math.round((endMs - startMs) / 1000));
+}
+
function createErrorResult(message: string, type: ToolErrorType): ToolResult {
return {
llmContent: message,
@@ -103,6 +122,35 @@ function summarizeState(state: DeepWorkState): Record {
};
}
+function buildDeepWorkSummaryDisplay(
+ state: DeepWorkState,
+ reason?: string,
+): DeepWorkSummaryDisplay {
+ const endedAt = new Date().toISOString();
+ const totalRequiredQuestions = state.requiredQuestions.filter(
+ (q) => q.required,
+ ).length;
+ const answeredRequiredQuestions = state.requiredQuestions.filter(
+ (q) => q.required && q.answer.trim().length > 0,
+ ).length;
+
+ return {
+ type: 'deep_work_summary',
+ status: state.status,
+ runId: state.runId,
+ executionCount: state.iteration,
+ maxRuns: state.maxRuns,
+ elapsedSeconds: getElapsedSeconds(state.startedAt, endedAt),
+ maxTimeMinutes: state.maxTimeMinutes,
+ answeredRequiredQuestions,
+ totalRequiredQuestions,
+ readinessVerdict: state.readinessReport?.verdict ?? null,
+ completionPromise: state.completionPromise,
+ approvedPlanPath: state.approvedPlanPath,
+ reason: reason ?? null,
+ };
+}
+
function syncApprovedPlanPath(config: Config, state: DeepWorkState): void {
const configPath = config.getApprovedPlanPath()?.trim();
const statePath = state.approvedPlanPath?.trim();
@@ -617,16 +665,18 @@ class StopDeepWorkRunInvocation extends BaseToolInvocation<
break;
}
- if (this.params.reason?.trim()) {
- state.rejectionReason = this.params.reason.trim();
+ const reason = this.params.reason?.trim();
+ if (reason) {
+ state.rejectionReason = reason;
}
await saveDeepWorkState(this.config, state);
+ const summary = buildDeepWorkSummaryDisplay(state, reason);
return createSuccessResult(
- JSON.stringify({ state: summarizeState(state) }),
- `Deep Work status set to ${state.status}.`,
- { state },
+ JSON.stringify({ state: summarizeState(state), summary }),
+ summary,
+ { state, summary },
);
}
}
diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts
index dc974a5ff7..6beb21ee01 100644
--- a/packages/core/src/tools/tools.ts
+++ b/packages/core/src/tools/tools.ts
@@ -664,7 +664,35 @@ export interface TodoList {
todos: Todo[];
}
-export type ToolResultDisplay = string | FileDiff | AnsiOutput | TodoList;
+export interface DeepWorkSummaryDisplay {
+ type: 'deep_work_summary';
+ status:
+ | 'configured'
+ | 'ready'
+ | 'running'
+ | 'paused'
+ | 'stopped'
+ | 'completed'
+ | 'rejected';
+ runId: string;
+ executionCount: number;
+ maxRuns: number;
+ elapsedSeconds: number | null;
+ maxTimeMinutes: number;
+ answeredRequiredQuestions: number;
+ totalRequiredQuestions: number;
+ readinessVerdict?: 'ready' | 'needs_answers' | 'reject' | null;
+ completionPromise?: string | null;
+ approvedPlanPath?: string | null;
+ reason?: string | null;
+}
+
+export type ToolResultDisplay =
+ | string
+ | FileDiff
+ | AnsiOutput
+ | TodoList
+ | DeepWorkSummaryDisplay;
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';