feat(acp): add session resume support (#18043)

Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
This commit is contained in:
Bryan Morgan
2026-02-01 14:16:57 -05:00
committed by GitHub
parent cb4f0c6fa4
commit 7d36cc004f
6 changed files with 651 additions and 213 deletions

View File

@@ -24,7 +24,14 @@ import { coreEvents } from '@google/gemini-cli-core';
// Mock modules
vi.mock('fs/promises');
vi.mock('path');
vi.mock('../../utils/sessionUtils.js');
vi.mock('../../utils/sessionUtils.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../utils/sessionUtils.js')>();
return {
...actual,
getSessionFiles: vi.fn(),
};
});
const MOCKED_PROJECT_TEMP_DIR = '/test/project/temp';
const MOCKED_CHATS_DIR = '/test/project/temp/chats';

View File

@@ -13,11 +13,12 @@ import type {
ConversationRecord,
ResumedSessionData,
} from '@google/gemini-cli-core';
import type { Part } from '@google/genai';
import { partListUnionToString, coreEvents } from '@google/gemini-cli-core';
import { checkExhaustive } from '../../utils/checks.js';
import { coreEvents } from '@google/gemini-cli-core';
import type { SessionInfo } from '../../utils/sessionUtils.js';
import { MessageType, ToolCallStatus } from '../types.js';
import { convertSessionToHistoryFormats } from '../../utils/sessionUtils.js';
import type { Part } from '@google/genai';
export { convertSessionToHistoryFormats };
export const useSessionBrowser = (
config: Config,
@@ -112,190 +113,3 @@ export const useSessionBrowser = (
),
};
};
/**
* Converts session/conversation data into UI history and Gemini client history formats.
*/
export function convertSessionToHistoryFormats(
messages: ConversationRecord['messages'],
): {
uiHistory: HistoryItemWithoutId[];
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>;
} {
const uiHistory: HistoryItemWithoutId[] = [];
for (const msg of messages) {
// Add the message only if it has content
const displayContentString = msg.displayContent
? partListUnionToString(msg.displayContent)
: undefined;
const contentString = partListUnionToString(msg.content);
const uiText = displayContentString || contentString;
if (uiText.trim()) {
let messageType: MessageType;
switch (msg.type) {
case 'user':
messageType = MessageType.USER;
break;
case 'info':
messageType = MessageType.INFO;
break;
case 'error':
messageType = MessageType.ERROR;
break;
case 'warning':
messageType = MessageType.WARNING;
break;
case 'gemini':
messageType = MessageType.GEMINI;
break;
default:
checkExhaustive(msg);
messageType = MessageType.GEMINI;
break;
}
uiHistory.push({
type: messageType,
text: uiText,
});
}
// Add tool calls if present
if (
msg.type !== 'user' &&
'toolCalls' in msg &&
msg.toolCalls &&
msg.toolCalls.length > 0
) {
uiHistory.push({
type: 'tool_group',
tools: msg.toolCalls.map((tool) => ({
callId: tool.id,
name: tool.displayName || tool.name,
description: tool.description || '',
renderOutputAsMarkdown: tool.renderOutputAsMarkdown ?? true,
status:
tool.status === 'success'
? ToolCallStatus.Success
: ToolCallStatus.Error,
resultDisplay: tool.resultDisplay,
confirmationDetails: undefined,
})),
});
}
}
// Convert to Gemini client history format
const clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }> = [];
for (const msg of messages) {
// Skip system/error messages and user slash commands
if (msg.type === 'info' || msg.type === 'error' || msg.type === 'warning') {
continue;
}
if (msg.type === 'user') {
// Skip user slash commands
const contentString = partListUnionToString(msg.content);
if (
contentString.trim().startsWith('/') ||
contentString.trim().startsWith('?')
) {
continue;
}
// Add regular user message
clientHistory.push({
role: 'user',
parts: Array.isArray(msg.content)
? (msg.content as Part[])
: [{ text: contentString }],
});
} else if (msg.type === 'gemini') {
// Handle Gemini messages with potential tool calls
const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0;
if (hasToolCalls) {
// Create model message with function calls
const modelParts: Part[] = [];
// Add text content if present
const contentString = partListUnionToString(msg.content);
if (msg.content && contentString.trim()) {
modelParts.push({ text: contentString });
}
// Add function calls
for (const toolCall of msg.toolCalls!) {
modelParts.push({
functionCall: {
name: toolCall.name,
args: toolCall.args,
...(toolCall.id && { id: toolCall.id }),
},
});
}
clientHistory.push({
role: 'model',
parts: modelParts,
});
// Create single function response message with all tool call responses
const functionResponseParts: Part[] = [];
for (const toolCall of msg.toolCalls!) {
if (toolCall.result) {
// Convert PartListUnion result to function response format
let responseData: Part;
if (typeof toolCall.result === 'string') {
responseData = {
functionResponse: {
id: toolCall.id,
name: toolCall.name,
response: {
output: toolCall.result,
},
},
};
} else if (Array.isArray(toolCall.result)) {
// toolCall.result is an array containing properly formatted
// function responses
functionResponseParts.push(...(toolCall.result as Part[]));
continue;
} else {
// Fallback for non-array results
responseData = toolCall.result;
}
functionResponseParts.push(responseData);
}
}
// Only add user message if we have function responses
if (functionResponseParts.length > 0) {
clientHistory.push({
role: 'user',
parts: functionResponseParts,
});
}
} else {
// Regular Gemini message without tool calls
const contentString = partListUnionToString(msg.content);
if (msg.content && contentString.trim()) {
clientHistory.push({
role: 'model',
parts: [{ text: contentString }],
});
}
}
}
}
return {
uiHistory,
clientHistory,
};
}