diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts
index d2fa89088b..26daaf66a1 100644
--- a/packages/cli/src/nonInteractiveCli.ts
+++ b/packages/cli/src/nonInteractiveCli.ts
@@ -6,41 +6,33 @@
import type {
Config,
+ ToolCallRequestInfo,
ResumedSessionData,
UserFeedbackPayload,
- AgentEvent,
- ContentPart,
} from '@google/gemini-cli-core';
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
convertSessionToClientHistory,
- FatalError,
- FatalAuthenticationError,
+ GeminiEventType,
FatalInputError,
- FatalSandboxError,
- FatalConfigError,
- FatalTurnLimitedError,
- FatalToolExecutionError,
- FatalCancellationError,
promptIdContext,
OutputFormat,
JsonFormatter,
StreamJsonFormatter,
JsonStreamEventType,
uiTelemetryService,
+ debugLogger,
coreEvents,
CoreEvent,
createWorkingStdio,
+ recordToolCallInteractions,
+ ToolErrorType,
Scheduler,
ROOT_SCHEDULER_ID,
- LegacyAgentSession,
- ToolErrorType,
- geminiPartsToContentParts,
- debugLogger,
} from '@google/gemini-cli-core';
-import type { Part } from '@google/genai';
+import type { Content, Part } from '@google/genai';
import readline from 'node:readline';
import stripAnsi from 'strip-ansi';
@@ -163,6 +155,8 @@ export async function runNonInteractive(
}, 200);
abortController.abort();
+ // Note: Don't exit here - let the abort flow through the system
+ // and trigger handleCancellationError() which will exit with proper code
}
};
@@ -193,8 +187,6 @@ export async function runNonInteractive(
};
let errorToHandle: unknown | undefined;
- let terminalProcessExitHandled = false;
- let abortSession = () => {};
try {
consolePatcher.patch();
@@ -259,6 +251,9 @@ export async function runNonInteractive(
config,
settings,
);
+ // If a slash command is found and returns a prompt, use it.
+ // Otherwise, slashCommandResult falls through to the default prompt
+ // handling.
if (slashCommandResult) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
query = slashCommandResult as Part[];
@@ -276,6 +271,8 @@ export async function runNonInteractive(
escapePastedAtSymbols: false,
});
if (error || !processedQuery) {
+ // An error occurred during @include processing (e.g., file not found).
+ // The error message is already logged by handleAtCommand.
throw new FatalInputError(
error || 'Exiting due to an error processing the @ command.',
);
@@ -294,335 +291,235 @@ export async function runNonInteractive(
});
}
- // Create LegacyAgentSession — owns the agentic loop
- const session = new LegacyAgentSession({
- client: geminiClient,
- scheduler,
- config,
- promptId: prompt_id,
- });
+ let currentMessages: Content[] = [{ role: 'user', parts: query }];
- // Wire Ctrl+C to session abort
- abortSession = () => {
- void session.abort();
- };
- abortController.signal.addEventListener('abort', abortSession);
- if (abortController.signal.aborted) {
- return handleCancellationError(config);
- }
+ let turnCount = 0;
+ while (true) {
+ turnCount++;
+ if (
+ config.getMaxSessionTurns() >= 0 &&
+ turnCount > config.getMaxSessionTurns()
+ ) {
+ handleMaxTurnsExceededError(config);
+ }
+ const toolCallRequests: ToolCallRequestInfo[] = [];
- // Start the agentic loop (runs in background)
- const { streamId } = await session.send({
- message: {
- content: geminiPartsToContentParts(query),
- displayContent: input,
- },
- });
- if (streamId === null) {
- throw new Error(
- 'LegacyAgentSession.send() unexpectedly returned no stream for a message send.',
+ const responseStream = geminiClient.sendMessageStream(
+ currentMessages[0]?.parts || [],
+ abortController.signal,
+ prompt_id,
+ undefined,
+ false,
+ turnCount === 1 ? input : undefined,
);
- }
- const getTextContent = (parts?: ContentPart[]): string | undefined => {
- const text = parts
- ?.map((part) => (part.type === 'text' ? part.text : ''))
- .join('');
- return text ? text : undefined;
- };
+ let responseText = '';
+ for await (const event of responseStream) {
+ if (abortController.signal.aborted) {
+ handleCancellationError(config);
+ }
- const emitFinalSuccessResult = (): void => {
- if (streamFormatter) {
- const metrics = uiTelemetryService.getMetrics();
- const durationMs = Date.now() - startTime;
- streamFormatter.emitEvent({
- type: JsonStreamEventType.RESULT,
- timestamp: new Date().toISOString(),
- status: 'success',
- stats: streamFormatter.convertToStreamStats(metrics, durationMs),
- });
- } else if (config.getOutputFormat() === OutputFormat.JSON) {
- const formatter = new JsonFormatter();
- const stats = uiTelemetryService.getMetrics();
- textOutput.write(
- formatter.format(config.getSessionId(), responseText, stats),
- );
- } else {
- textOutput.ensureTrailingNewline();
- }
- };
-
- const reconstructFatalError = (event: AgentEvent<'error'>): Error => {
- const errorMeta = event._meta;
- const name =
- typeof errorMeta?.['errorName'] === 'string'
- ? errorMeta['errorName']
- : undefined;
-
- let errToThrow: Error;
- switch (name) {
- case 'FatalAuthenticationError':
- errToThrow = new FatalAuthenticationError(event.message);
- break;
- case 'FatalInputError':
- errToThrow = new FatalInputError(event.message);
- break;
- case 'FatalSandboxError':
- errToThrow = new FatalSandboxError(event.message);
- break;
- case 'FatalConfigError':
- errToThrow = new FatalConfigError(event.message);
- break;
- case 'FatalTurnLimitedError':
- errToThrow = new FatalTurnLimitedError(event.message);
- break;
- case 'FatalToolExecutionError':
- errToThrow = new FatalToolExecutionError(event.message);
- break;
- case 'FatalCancellationError':
- errToThrow = new FatalCancellationError(event.message);
- break;
- case 'FatalError':
- errToThrow = new FatalError(
- event.message,
- typeof errorMeta?.['exitCode'] === 'number'
- ? errorMeta['exitCode']
- : 1,
- );
- break;
- default:
- errToThrow = new Error(event.message);
- if (name) {
- Object.defineProperty(errToThrow, 'name', {
- value: name,
- enumerable: true,
+ if (event.type === GeminiEventType.Content) {
+ const isRaw =
+ config.getRawOutput() || config.getAcceptRawOutputRisk();
+ const output = isRaw ? event.value : stripAnsi(event.value);
+ if (streamFormatter) {
+ streamFormatter.emitEvent({
+ type: JsonStreamEventType.MESSAGE,
+ timestamp: new Date().toISOString(),
+ role: 'assistant',
+ content: output,
+ delta: true,
});
- }
- break;
- }
-
- if (errorMeta?.['exitCode'] !== undefined) {
- Object.defineProperty(errToThrow, 'exitCode', {
- value: errorMeta['exitCode'],
- enumerable: true,
- });
- }
- if (errorMeta?.['code'] !== undefined) {
- Object.defineProperty(errToThrow, 'code', {
- value: errorMeta['code'],
- enumerable: true,
- });
- }
- if (errorMeta?.['status'] !== undefined) {
- Object.defineProperty(errToThrow, 'status', {
- value: errorMeta['status'],
- enumerable: true,
- });
- }
- return errToThrow;
- };
-
- const runTerminalExitHandler = (handler: () => never): never => {
- terminalProcessExitHandled = true;
- return handler();
- };
-
- // Consume AgentEvents for output formatting
- let responseText = '';
- let preToolResponseText: string | undefined;
- let streamEnded = false;
- for await (const event of session.stream({ streamId })) {
- if (streamEnded) break;
- switch (event.type) {
- case 'message': {
- if (event.role === 'agent') {
- for (const part of event.content) {
- if (part.type === 'text') {
- const isRaw =
- config.getRawOutput() || config.getAcceptRawOutputRisk();
- const output = isRaw ? part.text : stripAnsi(part.text);
- if (streamFormatter) {
- streamFormatter.emitEvent({
- type: JsonStreamEventType.MESSAGE,
- timestamp: new Date().toISOString(),
- role: 'assistant',
- content: output,
- delta: true,
- });
- } else if (config.getOutputFormat() === OutputFormat.JSON) {
- responseText += output;
- } else {
- if (part.text) {
- textOutput.write(output);
- }
- }
- }
+ } else if (config.getOutputFormat() === OutputFormat.JSON) {
+ responseText += output;
+ } else {
+ if (event.value) {
+ textOutput.write(output);
}
}
- break;
- }
- case 'tool_request': {
- if (config.getOutputFormat() === OutputFormat.JSON) {
- // Final JSON output should reflect the last assistant answer after
- // any tool orchestration, not intermediate pre-tool text.
- preToolResponseText = responseText || preToolResponseText;
- responseText = '';
- }
+ } else if (event.type === GeminiEventType.ToolCallRequest) {
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.TOOL_USE,
timestamp: new Date().toISOString(),
- tool_name: event.name,
- tool_id: event.requestId,
- parameters: event.args,
+ tool_name: event.value.name,
+ tool_id: event.value.callId,
+ parameters: event.value.args,
});
}
- break;
- }
- case 'tool_response': {
- textOutput.ensureTrailingNewline();
- if (streamFormatter) {
- const displayText = getTextContent(event.displayContent);
- const errorMsg = getTextContent(event.content) ?? 'Tool error';
- streamFormatter.emitEvent({
- type: JsonStreamEventType.TOOL_RESULT,
- timestamp: new Date().toISOString(),
- tool_id: event.requestId,
- status: event.isError ? 'error' : 'success',
- output: displayText,
- error: event.isError
- ? {
- type:
- typeof event.data?.['errorType'] === 'string'
- ? event.data['errorType']
- : 'TOOL_EXECUTION_ERROR',
- message: errorMsg,
- }
- : undefined,
- });
- }
- if (event.isError) {
- const displayText = getTextContent(event.displayContent);
- const errorMsg = getTextContent(event.content) ?? 'Tool error';
-
- if (event.data?.['errorType'] === ToolErrorType.STOP_EXECUTION) {
- if (
- config.getOutputFormat() === OutputFormat.JSON &&
- !responseText &&
- preToolResponseText
- ) {
- responseText = preToolResponseText;
- }
- const stopMessage = `Agent execution stopped: ${errorMsg}`;
- if (config.getOutputFormat() === OutputFormat.TEXT) {
- process.stderr.write(`${stopMessage}\n`);
- }
- }
-
- if (event.data?.['errorType'] === ToolErrorType.NO_SPACE_LEFT) {
- terminalProcessExitHandled = true;
- handleToolError(
- event.name,
- new Error(errorMsg),
- config,
- typeof event.data?.['errorType'] === 'string'
- ? event.data['errorType']
- : undefined,
- displayText,
- );
- return;
- }
- handleToolError(
- event.name,
- new Error(errorMsg),
- config,
- typeof event.data?.['errorType'] === 'string'
- ? event.data['errorType']
- : undefined,
- displayText,
- );
- }
- break;
- }
- case 'error': {
- if (event.fatal) {
- throw reconstructFatalError(event);
- }
-
- const errorCode = event._meta?.['code'];
-
- if (errorCode === 'AGENT_EXECUTION_BLOCKED') {
- if (config.getOutputFormat() === OutputFormat.TEXT) {
- process.stderr.write(`[WARNING] ${event.message}\n`);
- }
- break;
- }
-
- const severity =
- event.status === 'RESOURCE_EXHAUSTED' ? 'error' : 'warning';
- if (config.getOutputFormat() === OutputFormat.TEXT) {
- process.stderr.write(`[WARNING] ${event.message}\n`);
- }
+ toolCallRequests.push(event.value);
+ } else if (event.type === GeminiEventType.LoopDetected) {
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.ERROR,
timestamp: new Date().toISOString(),
- severity,
- message: event.message,
+ severity: 'warning',
+ message: 'Loop detected, stopping execution',
});
}
- break;
+ } else if (event.type === GeminiEventType.MaxSessionTurns) {
+ if (streamFormatter) {
+ streamFormatter.emitEvent({
+ type: JsonStreamEventType.ERROR,
+ timestamp: new Date().toISOString(),
+ severity: 'error',
+ message: 'Maximum session turns exceeded',
+ });
+ }
+ } else if (event.type === GeminiEventType.Error) {
+ throw event.value.error;
+ } else if (event.type === GeminiEventType.AgentExecutionStopped) {
+ const stopMessage = `Agent execution stopped: ${event.value.systemMessage?.trim() || event.value.reason}`;
+ if (config.getOutputFormat() === OutputFormat.TEXT) {
+ process.stderr.write(`${stopMessage}\n`);
+ }
+ // Emit final result event for streaming JSON if needed
+ if (streamFormatter) {
+ const metrics = uiTelemetryService.getMetrics();
+ const durationMs = Date.now() - startTime;
+ streamFormatter.emitEvent({
+ type: JsonStreamEventType.RESULT,
+ timestamp: new Date().toISOString(),
+ status: 'success',
+ stats: streamFormatter.convertToStreamStats(
+ metrics,
+ durationMs,
+ ),
+ });
+ }
+ return;
+ } else if (event.type === GeminiEventType.AgentExecutionBlocked) {
+ const blockMessage = `Agent execution blocked: ${event.value.systemMessage?.trim() || event.value.reason}`;
+ if (config.getOutputFormat() === OutputFormat.TEXT) {
+ process.stderr.write(`[WARNING] ${blockMessage}\n`);
+ }
}
- case 'agent_end': {
- if (event.reason === 'aborted') {
- runTerminalExitHandler(() => handleCancellationError(config));
- } else if (event.reason === 'max_turns') {
- const isConfiguredTurnLimit =
- typeof event.data?.['maxTurns'] === 'number' ||
- typeof event.data?.['turnCount'] === 'number';
+ }
- if (isConfiguredTurnLimit) {
- runTerminalExitHandler(() =>
- handleMaxTurnsExceededError(config),
- );
- } else if (streamFormatter) {
- streamFormatter.emitEvent({
- type: JsonStreamEventType.ERROR,
- timestamp: new Date().toISOString(),
- severity: 'error',
- message: 'Maximum session turns exceeded',
- });
- }
+ if (toolCallRequests.length > 0) {
+ textOutput.ensureTrailingNewline();
+ const completedToolCalls = await scheduler.schedule(
+ toolCallRequests,
+ abortController.signal,
+ );
+ const toolResponseParts: Part[] = [];
+
+ for (const completedToolCall of completedToolCalls) {
+ const toolResponse = completedToolCall.response;
+ const requestInfo = completedToolCall.request;
+
+ if (streamFormatter) {
+ streamFormatter.emitEvent({
+ type: JsonStreamEventType.TOOL_RESULT,
+ timestamp: new Date().toISOString(),
+ tool_id: requestInfo.callId,
+ status:
+ completedToolCall.status === 'error' ? 'error' : 'success',
+ output:
+ typeof toolResponse.resultDisplay === 'string'
+ ? toolResponse.resultDisplay
+ : undefined,
+ error: toolResponse.error
+ ? {
+ type: toolResponse.errorType || 'TOOL_EXECUTION_ERROR',
+ message: toolResponse.error.message,
+ }
+ : undefined,
+ });
}
- const stopMessage =
- typeof event.data?.['message'] === 'string'
- ? event.data['message']
- : '';
- if (stopMessage && config.getOutputFormat() === OutputFormat.TEXT) {
- process.stderr.write(`Agent execution stopped: ${stopMessage}\n`);
+ if (toolResponse.error) {
+ handleToolError(
+ requestInfo.name,
+ toolResponse.error,
+ config,
+ toolResponse.errorType || 'TOOL_EXECUTION_ERROR',
+ typeof toolResponse.resultDisplay === 'string'
+ ? toolResponse.resultDisplay
+ : undefined,
+ );
}
- emitFinalSuccessResult();
- streamEnded = true;
- break;
+ if (toolResponse.responseParts) {
+ toolResponseParts.push(...toolResponse.responseParts);
+ }
}
- case 'initialize':
- case 'session_update':
- case 'agent_start':
- case 'tool_update':
- case 'elicitation_request':
- case 'elicitation_response':
- case 'usage':
- // TODO: We should think about converting this into the usage event.
- // fallthrough
- case 'custom':
- // Explicitly ignore these non-interactive events
- break;
- default:
- debugLogger.error('Unknown agent event type:', event);
- event satisfies never;
- break;
+
+ // Record tool calls with full metadata before sending responses to Gemini
+ try {
+ const currentModel =
+ geminiClient.getCurrentSequenceModel() ?? config.getModel();
+ geminiClient
+ .getChat()
+ .recordCompletedToolCalls(currentModel, completedToolCalls);
+
+ await recordToolCallInteractions(config, completedToolCalls);
+ } catch (error) {
+ debugLogger.error(
+ `Error recording completed tool call information: ${error}`,
+ );
+ }
+
+ // Check if any tool requested to stop execution immediately
+ const stopExecutionTool = completedToolCalls.find(
+ (tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION,
+ );
+
+ if (stopExecutionTool && stopExecutionTool.response.error) {
+ const stopMessage = `Agent execution stopped: ${stopExecutionTool.response.error.message}`;
+
+ if (config.getOutputFormat() === OutputFormat.TEXT) {
+ process.stderr.write(`${stopMessage}\n`);
+ }
+
+ // Emit final result event for streaming JSON
+ if (streamFormatter) {
+ const metrics = uiTelemetryService.getMetrics();
+ const durationMs = Date.now() - startTime;
+ streamFormatter.emitEvent({
+ type: JsonStreamEventType.RESULT,
+ timestamp: new Date().toISOString(),
+ status: 'success',
+ stats: streamFormatter.convertToStreamStats(
+ metrics,
+ durationMs,
+ ),
+ });
+ } else if (config.getOutputFormat() === OutputFormat.JSON) {
+ const formatter = new JsonFormatter();
+ const stats = uiTelemetryService.getMetrics();
+ textOutput.write(
+ formatter.format(config.getSessionId(), responseText, stats),
+ );
+ } else {
+ textOutput.ensureTrailingNewline(); // Ensure a final newline
+ }
+ return;
+ }
+
+ currentMessages = [{ role: 'user', parts: toolResponseParts }];
+ } else {
+ // Emit final result event for streaming JSON
+ if (streamFormatter) {
+ const metrics = uiTelemetryService.getMetrics();
+ const durationMs = Date.now() - startTime;
+ streamFormatter.emitEvent({
+ type: JsonStreamEventType.RESULT,
+ timestamp: new Date().toISOString(),
+ status: 'success',
+ stats: streamFormatter.convertToStreamStats(metrics, durationMs),
+ });
+ } else if (config.getOutputFormat() === OutputFormat.JSON) {
+ const formatter = new JsonFormatter();
+ const stats = uiTelemetryService.getMetrics();
+ textOutput.write(
+ formatter.format(config.getSessionId(), responseText, stats),
+ );
+ } else {
+ textOutput.ensureTrailingNewline(); // Ensure a final newline
+ }
+ return;
}
}
} catch (error) {
@@ -630,16 +527,12 @@ export async function runNonInteractive(
} finally {
// Cleanup stdin cancellation before other cleanup
cleanupStdinCancellation();
- abortController.signal.removeEventListener('abort', abortSession);
consolePatcher.cleanup();
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
}
if (errorToHandle) {
- if (terminalProcessExitHandled) {
- throw errorToHandle;
- }
handleError(errorToHandle, config);
}
});
diff --git a/packages/cli/src/nonInteractiveCliAgentSession.ts b/packages/cli/src/nonInteractiveCliAgentSession.ts
index 78fc18be4e..fe5fbceba2 100644
--- a/packages/cli/src/nonInteractiveCliAgentSession.ts
+++ b/packages/cli/src/nonInteractiveCliAgentSession.ts
@@ -37,6 +37,7 @@ import {
LegacyAgentSession,
ToolErrorType,
geminiPartsToContentParts,
+ debugLogger,
} from '@google/gemini-cli-core';
import type { Part } from '@google/genai';
@@ -599,6 +600,7 @@ export async function runNonInteractive({
// Explicitly ignore these non-interactive events
break;
default:
+ debugLogger.error('Unknown agent event type:', event);
event satisfies never;
break;
}
diff --git a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
index cdc060d9d7..a630cb690b 100644
--- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
@@ -11,6 +11,17 @@ Enter to submit · Esc to cancel
"
`;
+exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 2`] = `
+"Select your preferred language:
+
+● 1. TypeScript
+ 2. JavaScript
+ 3. Enter a custom value
+
+Enter to select · ↑/↓ to navigate · Esc to cancel
+"
+`;
+
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = `
"Select your preferred language:
@@ -22,6 +33,17 @@ Enter to submit · Esc to cancel
"
`;
+exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 2`] = `
+"Select your preferred language:
+
+● 1. TypeScript
+ 2. JavaScript
+ 3. Type another language...
+
+Enter to select · ↑/↓ to navigate · Esc to cancel
+"
+`;
+
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
"Choose an option
@@ -181,6 +203,19 @@ Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
"
`;
+exports[`AskUserDialog > shows warning for unanswered questions on Review tab 2`] = `
+"← □ License │ □ README │ ≡ Review →
+
+Which license?
+
+● 1. MIT
+ Permissive license
+ 2. Enter a custom value
+
+Enter to select · ←/→ to switch questions · Esc to cancel
+"
+`;
+
exports[`AskUserDialog > verifies "All of the above" visual state with snapshot 1`] = `
"Which features?
(Select all that apply)
diff --git a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap
index 073c106ceb..6dab42cb2b 100644
--- a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap
@@ -140,6 +140,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
"
`;
+exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
+"Overview
+
+Add user authentication to the CLI application.
+
+Implementation Steps
+
+ 1. Create src/auth/AuthService.ts with login/logout methods
+ 2. Add session storage in src/storage/SessionStore.ts
+ 3. Update src/commands/index.ts to check auth status
+ 4. Add tests in src/auth/__tests__/
+
+Files to Modify
+
+ - src/index.ts - Add auth middleware
+ - src/config.ts - Add auth configuration options
+
+● 1. Yes, automatically accept edits
+ Approves plan and allows tools to run automatically
+ 2. Yes, manually accept edits
+ Approves plan but requires confirmation for each tool
+ 3. Type your feedback...
+
+Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
+"
+`;
+
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap
index 18f5f93a9f..88cc58f86e 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/DenseToolMessage.test.tsx.snap
@@ -24,6 +24,11 @@ exports[`DenseToolMessage > flattens newlines in string results 1`] = `
"
`;
+exports[`DenseToolMessage > flattens newlines in string results 2`] = `
+" ✓ test-tool Test description → Line 1 Line 2
+"
+`;
+
exports[`DenseToolMessage > renders correctly for Edit tool using confirmationDetails 1`] = `
" ? Edit styles.scss → Confirming
"
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap
index fed8b32bd0..2f3ac55790 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap
@@ -151,6 +151,15 @@ exports[` > with useAlterna
"
`;
+exports[` > with useAlternateBuffer = true > should handle diff with only header and no changes 2`] = `
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ │
+│ No changes detected. │
+│ │
+╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+"
+`;
+
exports[` > with useAlternateBuffer = true > should handle empty diff content 1`] = `
"No diff content.
"