diff --git a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts
index 6b0e6449e11..6acc3022213 100644
--- a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts
+++ b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts
@@ -233,6 +233,41 @@ describe('ToolCallEmitter', () => {
);
});
+ it('should not replay truncated session previews as full diffs', async () => {
+ await emitter.emitResult({
+ toolName: 'edit_file',
+ callId: 'call-edit',
+ success: true,
+ message: [],
+ resultDisplay: {
+ fileName: '/test/file.ts',
+ originalContent: 'old preview',
+ newContent: 'new preview',
+ truncatedForSession: true,
+ fileDiffLength: 200000,
+ fileDiffTruncated: true,
+ },
+ });
+
+ expect(sendUpdateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sessionUpdate: 'tool_call_update',
+ toolCallId: 'call-edit',
+ status: 'completed',
+ content: [
+ {
+ type: 'content',
+ content: {
+ type: 'text',
+ text: 'Full diff omitted from saved session history for /test/file.ts. Original fileDiff length: 200000 chars.',
+ },
+ },
+ ],
+ _meta: { toolName: 'edit_file' },
+ }),
+ );
+ });
+
it('should transform message parts to content', async () => {
await emitter.emitResult({
toolName: 'test_tool',
diff --git a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts
index a84e4f4e7da..92f66ee4740 100644
--- a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts
+++ b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts
@@ -20,6 +20,7 @@ import type {
} from '@agentclientprotocol/sdk';
import type { Part } from '@google/genai';
import { ToolNames, Kind } from '@qwen-code/qwen-code-core';
+import { buildTruncatedDiffPreviewText } from '../../../utils/truncatedDiffPreview.js';
/**
* Unified tool call event emitter.
@@ -273,6 +274,16 @@ export class ToolCallEmitter extends BaseEmitter {
// Check if this is a diff display (edit tool result)
if ('fileName' in obj && 'newContent' in obj) {
+ if (obj['truncatedForSession'] === true) {
+ return {
+ type: 'content',
+ content: {
+ type: 'text',
+ text: buildTruncatedDiffPreviewText(obj),
+ },
+ };
+ }
+
return {
type: 'diff',
path: obj['fileName'] as string,
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
index 568c9c12c13..274b391487b 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
@@ -274,6 +274,27 @@ describe('', () => {
expect(lastFrame()).toMatch(/MockDiff:--- a\/file\.txt/);
});
+ it('renders a saved-session preview notice for truncated diff results', () => {
+ const diffResult = {
+ fileDiff: '--- file.txt\n+++ file.txt\n@@ -1 +1 @@\n-omitted\n+preview',
+ fileName: 'file.txt',
+ originalContent: 'old preview',
+ newContent: 'new preview',
+ truncatedForSession: true,
+ fileDiffLength: 123456,
+ fileDiffTruncated: true,
+ };
+ const { lastFrame } = renderWithContext(
+ ,
+ StreamingState.Idle,
+ );
+
+ expect(lastFrame()).toContain(
+ 'Saved session preview only; full diff omitted from JSONL (123456 chars).',
+ );
+ expect(lastFrame()).toContain('MockDiff:--- file.txt');
+ });
+
it('renders emphasis correctly', () => {
const { lastFrame: highEmphasisFrame } = renderWithContext(
,
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index ebdfed81211..314ded5acdb 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -22,6 +22,7 @@ import type {
AnsiOutputDisplay,
Config,
McpToolProgressData,
+ FileDiff,
} from '@qwen-code/qwen-code-core';
import { AgentExecutionDisplay } from '../subagents/index.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
@@ -49,6 +50,14 @@ const DEFAULT_SHELL_OUTPUT_MAX_LINES = 5;
// outputs that will get truncated further MaxSizedBox anyway.
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
export type TextEmphasis = 'high' | 'medium' | 'low';
+type DiffResultDisplay = Pick<
+ FileDiff,
+ | 'fileDiff'
+ | 'fileName'
+ | 'truncatedForSession'
+ | 'fileDiffLength'
+ | 'fileDiffTruncated'
+>;
function sliceTextForMaxHeight(
text: string,
@@ -175,7 +184,7 @@ const useResultDisplayRenderer = (
) {
return {
type: 'diff',
- data: resultDisplay as { fileDiff: string; fileName: string },
+ data: resultDisplay as DiffResultDisplay,
};
}
@@ -378,19 +387,38 @@ const StringResultRenderer: React.FC<{
* Component to render diff results
*/
const DiffResultRenderer: React.FC<{
- data: { fileDiff: string; fileName: string };
+ data: DiffResultDisplay;
availableHeight?: number;
childWidth: number;
settings?: LoadedSettings;
-}> = ({ data, availableHeight, childWidth, settings }) => (
-
-);
+}> = ({ data, availableHeight, childWidth, settings }) => {
+ const diffHeight =
+ data.truncatedForSession && availableHeight !== undefined
+ ? Math.max(1, availableHeight - 1)
+ : availableHeight;
+
+ return (
+
+ {data.truncatedForSession && (
+
+ {data.fileDiffTruncated
+ ? 'Saved session preview only; full diff omitted from JSONL'
+ : 'Saved session preview only; full file contents truncated in JSONL'}
+ {data.fileDiffTruncated && typeof data.fileDiffLength === 'number'
+ ? ` (${data.fileDiffLength} chars).`
+ : '.'}
+
+ )}
+
+
+ );
+};
export interface ToolMessageProps extends IndividualToolCallDisplay {
availableTerminalHeight?: number;
diff --git a/packages/cli/src/ui/utils/export/collect.test.ts b/packages/cli/src/ui/utils/export/collect.test.ts
new file mode 100644
index 00000000000..0789bc11f56
--- /dev/null
+++ b/packages/cli/src/ui/utils/export/collect.test.ts
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, expect, it, vi } from 'vitest';
+import type { ChatRecord, Config } from '@qwen-code/qwen-code-core';
+import { collectSessionData } from './collect.js';
+
+describe('collectSessionData', () => {
+ const config = {
+ getToolRegistry: vi.fn().mockReturnValue({
+ getTool: vi.fn().mockReturnValue(null),
+ }),
+ } as unknown as Config;
+
+ it('skips line-count fallback for truncated saved-session previews', async () => {
+ const records: ChatRecord[] = [
+ {
+ uuid: 'assistant-1',
+ parentUuid: null,
+ sessionId: 'session-1',
+ timestamp: '2025-01-01T00:00:00.000Z',
+ type: 'assistant',
+ cwd: '',
+ version: '1.0.0',
+ message: {
+ role: 'model',
+ parts: [
+ {
+ functionCall: {
+ id: 'call-1',
+ name: 'edit_file',
+ args: { file_path: '/test/file.ts' },
+ },
+ },
+ ],
+ },
+ },
+ {
+ uuid: 'tool-1',
+ parentUuid: 'assistant-1',
+ sessionId: 'session-1',
+ timestamp: '2025-01-01T00:00:01.000Z',
+ type: 'tool_result',
+ cwd: '',
+ version: '1.0.0',
+ message: {
+ role: 'user',
+ parts: [
+ {
+ functionResponse: {
+ id: 'call-1',
+ name: 'edit_file',
+ response: { output: 'ok' },
+ },
+ },
+ ],
+ },
+ toolCallResult: {
+ callId: 'call-1',
+ resultDisplay: {
+ fileName: 'file.ts',
+ fileDiff:
+ '--- file.ts\n+++ file.ts\n@@ -1,2 +1,2 @@\n-old\n-preview\n+new\n+preview',
+ originalContent: 'old\npreview',
+ newContent: 'new\npreview',
+ truncatedForSession: true,
+ },
+ },
+ },
+ ];
+
+ const data = await collectSessionData(
+ {
+ sessionId: 'session-1',
+ startTime: '2025-01-01T00:00:00.000Z',
+ messages: records,
+ },
+ config,
+ );
+
+ expect(data.metadata?.filesWritten).toBe(1);
+ expect(data.metadata?.uniqueFiles).toEqual(['/test/file.ts']);
+ expect(data.metadata?.linesAdded).toBe(0);
+ expect(data.metadata?.linesRemoved).toBe(0);
+ });
+});
diff --git a/packages/cli/src/ui/utils/export/collect.ts b/packages/cli/src/ui/utils/export/collect.ts
index d929e0b41e9..04c374031c3 100644
--- a/packages/cli/src/ui/utils/export/collect.ts
+++ b/packages/cli/src/ui/utils/export/collect.ts
@@ -171,6 +171,7 @@ function calculateFileStats(records: ChatRecord[]): FileOperationStats {
originalContent?: string | null;
newContent?: string;
diffStat?: { model_added_lines?: number; model_removed_lines?: number };
+ truncatedForSession?: boolean;
};
// Determine operation type based on content fields
@@ -197,7 +198,7 @@ function calculateFileStats(records: ChatRecord[]): FileOperationStats {
// Use diffStat if available for accurate counts
stats.linesAdded += display.diffStat.model_added_lines ?? 0;
stats.linesRemoved += display.diffStat.model_removed_lines ?? 0;
- } else {
+ } else if (!display.truncatedForSession) {
// Fallback: count lines in content
const oldText = String(display.originalContent ?? '');
const newText = String(display.newContent ?? '');
diff --git a/packages/cli/src/ui/utils/export/normalize.test.ts b/packages/cli/src/ui/utils/export/normalize.test.ts
new file mode 100644
index 00000000000..ba2c362d08e
--- /dev/null
+++ b/packages/cli/src/ui/utils/export/normalize.test.ts
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, expect, it, vi } from 'vitest';
+import type { ChatRecord, Config } from '@qwen-code/qwen-code-core';
+import { normalizeSessionData } from './normalize.js';
+
+describe('normalizeSessionData', () => {
+ const config = {
+ getToolRegistry: vi.fn().mockReturnValue(undefined),
+ } as unknown as Config;
+
+ it('does not export truncated saved-session previews as full diffs', () => {
+ const record: ChatRecord = {
+ uuid: 'tool-1',
+ parentUuid: null,
+ sessionId: 'session-1',
+ timestamp: '2025-01-01T00:00:00.000Z',
+ type: 'tool_result',
+ cwd: '',
+ version: '1.0.0',
+ message: {
+ role: 'user',
+ parts: [
+ {
+ functionResponse: {
+ id: 'call-1',
+ name: 'edit_file',
+ response: { output: 'ok' },
+ },
+ },
+ ],
+ },
+ toolCallResult: {
+ callId: 'call-1',
+ resultDisplay: {
+ fileName: '/test/file.ts',
+ fileDiff:
+ '--- /test/file.ts\n+++ /test/file.ts\n@@ -1 +1 @@\n-omitted\n+preview',
+ originalContent: 'old preview',
+ newContent: 'new preview',
+ truncatedForSession: true,
+ fileDiffLength: 200000,
+ fileDiffTruncated: true,
+ },
+ },
+ };
+
+ const normalized = normalizeSessionData(
+ {
+ sessionId: 'session-1',
+ startTime: '2025-01-01T00:00:00.000Z',
+ messages: [],
+ },
+ [record],
+ config,
+ );
+
+ expect(normalized.messages[0].toolCall?.content).toEqual([
+ {
+ type: 'content',
+ content: {
+ type: 'text',
+ text: 'Full diff omitted from saved session history for /test/file.ts. Original fileDiff length: 200000 chars.',
+ },
+ },
+ ]);
+ });
+});
diff --git a/packages/cli/src/ui/utils/export/normalize.ts b/packages/cli/src/ui/utils/export/normalize.ts
index 99ab62c329d..44a90418b88 100644
--- a/packages/cli/src/ui/utils/export/normalize.ts
+++ b/packages/cli/src/ui/utils/export/normalize.ts
@@ -7,6 +7,7 @@
import type { Part } from '@google/genai';
import { ToolNames } from '@qwen-code/qwen-code-core';
import type { ChatRecord, Config, Kind } from '@qwen-code/qwen-code-core';
+import { buildTruncatedDiffPreviewText } from '../../../utils/truncatedDiffPreview.js';
import type { ExportMessage, ExportSessionData } from './types.js';
/**
@@ -283,6 +284,18 @@ function extractDiffContent(
const display = resultDisplay as Record;
if ('fileName' in display && 'newContent' in display) {
+ if (display['truncatedForSession'] === true) {
+ return [
+ {
+ type: 'content',
+ content: {
+ type: 'text',
+ text: buildTruncatedDiffPreviewText(display),
+ },
+ },
+ ];
+ }
+
return [
{
type: 'diff',
diff --git a/packages/cli/src/utils/truncatedDiffPreview.ts b/packages/cli/src/utils/truncatedDiffPreview.ts
new file mode 100644
index 00000000000..7b2510dd341
--- /dev/null
+++ b/packages/cli/src/utils/truncatedDiffPreview.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export function buildTruncatedDiffPreviewText(
+ display: Record,
+): string {
+ const fileName =
+ typeof display['fileName'] === 'string'
+ ? display['fileName']
+ : 'the edited file';
+ const fileDiffLength =
+ typeof display['fileDiffLength'] === 'number'
+ ? ` Original fileDiff length: ${display['fileDiffLength']} chars.`
+ : '';
+
+ if (display['fileDiffTruncated'] === true) {
+ return `Full diff omitted from saved session history for ${fileName}.${fileDiffLength}`;
+ }
+
+ return `Saved session preview only for ${fileName}; full original and new file contents are unavailable.`;
+}
diff --git a/packages/core/src/services/chatRecordingService.test.ts b/packages/core/src/services/chatRecordingService.test.ts
index 15173c32bc9..2bbc6143e23 100644
--- a/packages/core/src/services/chatRecordingService.test.ts
+++ b/packages/core/src/services/chatRecordingService.test.ts
@@ -17,6 +17,7 @@ import {
} from './chatRecordingService.js';
import * as jsonl from '../utils/jsonl-utils.js';
import type { Part } from '@google/genai';
+import type { FileDiff } from '../tools/tools.js';
vi.mock('node:path');
vi.mock('node:child_process');
@@ -290,6 +291,170 @@ describe('ChatRecordingService', () => {
expect(record.toolCallResult?.callId).toBe('call-1');
});
+ it('should keep small file diff resultDisplay unchanged', async () => {
+ const toolResultParts: Part[] = [
+ {
+ functionResponse: {
+ id: 'call-1',
+ name: 'edit',
+ response: { output: 'ok' },
+ },
+ },
+ ];
+ const resultDisplay: FileDiff = {
+ fileName: 'file.txt',
+ fileDiff: '--- file.txt\n+++ file.txt\n@@ -1 +1 @@\n-old\n+new',
+ originalContent: 'old',
+ newContent: 'new',
+ diffStat: {
+ model_added_lines: 1,
+ model_removed_lines: 1,
+ model_added_chars: 3,
+ model_removed_chars: 3,
+ user_added_lines: 0,
+ user_removed_lines: 0,
+ user_added_chars: 0,
+ user_removed_chars: 0,
+ },
+ };
+ const metadata = {
+ callId: 'call-1',
+ status: 'success' as const,
+ responseParts: toolResultParts,
+ resultDisplay,
+ error: undefined,
+ errorType: undefined,
+ };
+
+ chatRecordingService.recordToolResult(toolResultParts, metadata);
+ await chatRecordingService.flush();
+
+ const record = vi.mocked(jsonl.writeLine).mock.calls[0][1] as ChatRecord;
+
+ expect(record.toolCallResult?.resultDisplay).toBe(resultDisplay);
+ expect(
+ (record.toolCallResult?.resultDisplay as FileDiff).truncatedForSession,
+ ).toBeUndefined();
+ });
+
+ it('should shrink large file diff resultDisplay without mutating input', async () => {
+ const toolResultParts: Part[] = [
+ {
+ functionResponse: {
+ id: 'call-1',
+ name: 'write_file',
+ response: { output: 'ok' },
+ },
+ },
+ ];
+ const largeDiff = 'd'.repeat(70_000);
+ const largeOriginal = 'a'.repeat(20_000);
+ const largeNew = 'b'.repeat(20_000);
+ const resultDisplay: FileDiff = {
+ fileName: 'large.txt',
+ fileDiff: largeDiff,
+ originalContent: largeOriginal,
+ newContent: largeNew,
+ diffStat: {
+ model_added_lines: 1,
+ model_removed_lines: 1,
+ model_added_chars: largeNew.length,
+ model_removed_chars: largeOriginal.length,
+ user_added_lines: 0,
+ user_removed_lines: 0,
+ user_added_chars: 0,
+ user_removed_chars: 0,
+ },
+ };
+ const metadata = {
+ callId: 'call-1',
+ status: 'success' as const,
+ responseParts: toolResultParts,
+ resultDisplay,
+ error: undefined,
+ errorType: undefined,
+ };
+
+ chatRecordingService.recordToolResult(toolResultParts, metadata);
+ await chatRecordingService.flush();
+
+ const record = vi.mocked(jsonl.writeLine).mock.calls[0][1] as ChatRecord;
+ const savedDisplay = record.toolCallResult?.resultDisplay as FileDiff;
+
+ expect(savedDisplay).not.toBe(resultDisplay);
+ expect(savedDisplay.truncatedForSession).toBe(true);
+ expect(savedDisplay.fileDiffLength).toBe(largeDiff.length);
+ expect(savedDisplay.originalContentLength).toBe(largeOriginal.length);
+ expect(savedDisplay.newContentLength).toBe(largeNew.length);
+ expect(savedDisplay.fileDiffTruncated).toBe(true);
+ expect(savedDisplay.originalContentTruncated).toBe(true);
+ expect(savedDisplay.newContentTruncated).toBe(true);
+ expect(savedDisplay.fileDiff).toContain(
+ 'Full diff omitted from saved session history',
+ );
+ expect(savedDisplay.fileDiff).not.toBe(largeDiff);
+ expect(savedDisplay.originalContent?.length).toBeLessThanOrEqual(16_000);
+ expect(savedDisplay.originalContent).toContain(
+ 'truncated for saved session preview',
+ );
+ expect(savedDisplay.newContent.length).toBeLessThanOrEqual(16_000);
+ expect(savedDisplay.newContent).toContain(
+ 'truncated for saved session preview',
+ );
+ expect(savedDisplay.diffStat).toEqual(resultDisplay.diffStat);
+
+ expect(resultDisplay.fileDiff).toBe(largeDiff);
+ expect(resultDisplay.originalContent).toBe(largeOriginal);
+ expect(resultDisplay.newContent).toBe(largeNew);
+ expect(resultDisplay.truncatedForSession).toBeUndefined();
+ });
+
+ it('should continue stripping nested tool calls from task execution results', async () => {
+ const toolResultParts: Part[] = [
+ {
+ functionResponse: {
+ id: 'call-1',
+ name: 'task',
+ response: { output: 'ok' },
+ },
+ },
+ ];
+ const metadata = {
+ callId: 'call-1',
+ status: 'success' as const,
+ responseParts: toolResultParts,
+ resultDisplay: {
+ type: 'task_execution' as const,
+ subagentName: 'Task',
+ taskDescription: 'Run task',
+ taskPrompt: 'Run task',
+ status: 'completed' as const,
+ result: 'done',
+ toolCalls: [
+ {
+ callId: 'nested-call',
+ name: 'read_file',
+ status: 'success' as const,
+ args: {},
+ result: 'nested result',
+ },
+ ],
+ },
+ error: undefined,
+ errorType: undefined,
+ };
+
+ chatRecordingService.recordToolResult(toolResultParts, metadata);
+ await chatRecordingService.flush();
+
+ const record = vi.mocked(jsonl.writeLine).mock.calls[0][1] as ChatRecord;
+
+ expect(record.toolCallResult?.resultDisplay).toMatchObject({
+ type: 'task_execution',
+ toolCalls: [],
+ });
+ });
+
it('should chain tool result correctly with parentUuid', async () => {
chatRecordingService.recordUserMessage([{ text: 'Hello' }]);
chatRecordingService.recordAssistantTurn({
diff --git a/packages/core/src/services/chatRecordingService.ts b/packages/core/src/services/chatRecordingService.ts
index 35be0ed5f00..2404983728f 100644
--- a/packages/core/src/services/chatRecordingService.ts
+++ b/packages/core/src/services/chatRecordingService.ts
@@ -25,7 +25,7 @@ import type {
ToolCallResponseInfo,
} from '../core/turn.js';
import type { Status } from '../core/coreToolScheduler.js';
-import type { AgentResultDisplay } from '../tools/tools.js';
+import type { AgentResultDisplay, FileDiff } from '../tools/tools.js';
import type { UiEvent } from '../telemetry/uiTelemetry.js';
const debugLogger = createDebugLogger('CHAT_RECORDING');
@@ -36,6 +36,132 @@ const debugLogger = createDebugLogger('CHAT_RECORDING');
* retrying across turns.
*/
const AUTO_TITLE_ATTEMPT_CAP = 3;
+const SESSION_FILE_DIFF_AGGREGATE_CHAR_LIMIT = 100_000;
+const SESSION_FILE_DIFF_CHAR_LIMIT = 50_000;
+const SESSION_FILE_CONTENT_CHAR_LIMIT = 16_000;
+
+function isFileDiffDisplay(resultDisplay: unknown): resultDisplay is FileDiff {
+ if (
+ typeof resultDisplay !== 'object' ||
+ resultDisplay === null ||
+ !('fileDiff' in resultDisplay) ||
+ !('fileName' in resultDisplay) ||
+ !('originalContent' in resultDisplay) ||
+ !('newContent' in resultDisplay)
+ ) {
+ return false;
+ }
+
+ const display = resultDisplay as Record;
+ const originalContent = display['originalContent'];
+ return (
+ typeof display['fileDiff'] === 'string' &&
+ typeof display['fileName'] === 'string' &&
+ typeof display['newContent'] === 'string' &&
+ (originalContent === null || typeof originalContent === 'string')
+ );
+}
+
+function stringLength(value: string | null | undefined): number {
+ return typeof value === 'string' ? value.length : 0;
+}
+
+function truncateMiddleForSession(value: string, limit: number): string {
+ if (value.length <= limit) {
+ return value;
+ }
+
+ const marker = `\n[... truncated for saved session preview; original length: ${value.length} characters ...]\n`;
+ const contentBudget = Math.max(0, limit - marker.length);
+ const headLength = Math.ceil(contentBudget * 0.6);
+ const tailLength = contentBudget - headLength;
+
+ return (
+ value.slice(0, headLength) +
+ marker +
+ (tailLength > 0 ? value.slice(value.length - tailLength) : '')
+ );
+}
+
+function buildSyntheticDiffPreview(display: FileDiff): string {
+ const originalLength = stringLength(display.originalContent);
+ return [
+ `--- ${display.fileName}`,
+ `+++ ${display.fileName}`,
+ '@@ -1 +1 @@',
+ `-Full diff omitted from saved session history; original fileDiff length: ${display.fileDiff.length} characters.`,
+ `+Saved session preview only; originalContent length: ${originalLength} characters, newContent length: ${display.newContent.length} characters.`,
+ ].join('\n');
+}
+
+function sanitizeFileDiffForRecording(display: FileDiff): FileDiff {
+ const fileDiffLength = display.fileDiff.length;
+ const originalContentLength = stringLength(display.originalContent);
+ const newContentLength = display.newContent.length;
+ const aggregateLength =
+ fileDiffLength + originalContentLength + newContentLength;
+
+ const fileDiffTruncated = fileDiffLength > SESSION_FILE_DIFF_CHAR_LIMIT;
+ const originalContentTruncated =
+ originalContentLength > SESSION_FILE_CONTENT_CHAR_LIMIT;
+ const newContentTruncated =
+ newContentLength > SESSION_FILE_CONTENT_CHAR_LIMIT;
+
+ if (
+ aggregateLength <= SESSION_FILE_DIFF_AGGREGATE_CHAR_LIMIT &&
+ !fileDiffTruncated &&
+ !originalContentTruncated &&
+ !newContentTruncated
+ ) {
+ return display;
+ }
+
+ return {
+ ...display,
+ fileDiff: fileDiffTruncated
+ ? buildSyntheticDiffPreview(display)
+ : display.fileDiff,
+ originalContent:
+ display.originalContent !== null && originalContentTruncated
+ ? truncateMiddleForSession(
+ display.originalContent,
+ SESSION_FILE_CONTENT_CHAR_LIMIT,
+ )
+ : display.originalContent,
+ newContent: newContentTruncated
+ ? truncateMiddleForSession(
+ display.newContent,
+ SESSION_FILE_CONTENT_CHAR_LIMIT,
+ )
+ : display.newContent,
+ truncatedForSession: true,
+ fileDiffLength,
+ originalContentLength,
+ newContentLength,
+ fileDiffTruncated,
+ originalContentTruncated,
+ newContentTruncated,
+ };
+}
+
+export function sanitizeToolCallResultForRecording<
+ T extends Partial,
+>(toolCallResult: T): T {
+ const resultDisplay = toolCallResult.resultDisplay;
+ if (!isFileDiffDisplay(resultDisplay)) {
+ return toolCallResult;
+ }
+
+ const sanitizedResultDisplay = sanitizeFileDiffForRecording(resultDisplay);
+ if (sanitizedResultDisplay === resultDisplay) {
+ return toolCallResult;
+ }
+
+ return {
+ ...toolCallResult,
+ resultDisplay: sanitizedResultDisplay,
+ } as T;
+}
/**
* Users who don't want the fast model silently generating titles can opt
@@ -712,23 +838,27 @@ export class ChatRecordingService {
};
if (toolCallResult) {
+ const recordingToolCallResult =
+ sanitizeToolCallResultForRecording(toolCallResult);
+
// special case for task executions - we don't want to record the tool calls
if (
- typeof toolCallResult.resultDisplay === 'object' &&
- toolCallResult.resultDisplay !== null &&
- 'type' in toolCallResult.resultDisplay &&
- toolCallResult.resultDisplay.type === 'task_execution'
+ typeof recordingToolCallResult.resultDisplay === 'object' &&
+ recordingToolCallResult.resultDisplay !== null &&
+ 'type' in recordingToolCallResult.resultDisplay &&
+ recordingToolCallResult.resultDisplay.type === 'task_execution'
) {
- const taskResult = toolCallResult.resultDisplay as AgentResultDisplay;
+ const taskResult =
+ recordingToolCallResult.resultDisplay as AgentResultDisplay;
record.toolCallResult = {
- ...toolCallResult,
+ ...recordingToolCallResult,
resultDisplay: {
...taskResult,
toolCalls: [],
},
};
} else {
- record.toolCallResult = toolCallResult;
+ record.toolCallResult = recordingToolCallResult;
}
}
diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts
index 65ec2fcef27..04f3a055cd4 100644
--- a/packages/core/src/tools/tools.ts
+++ b/packages/core/src/tools/tools.ts
@@ -561,6 +561,13 @@ export interface FileDiff {
originalContent: string | null;
newContent: string;
diffStat?: DiffStat;
+ truncatedForSession?: boolean;
+ fileDiffLength?: number;
+ originalContentLength?: number;
+ newContentLength?: number;
+ fileDiffTruncated?: boolean;
+ originalContentTruncated?: boolean;
+ newContentTruncated?: boolean;
}
export interface DiffStat {