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

@@ -16,6 +16,13 @@ import {
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { stripUnsafeCharacters } from '../ui/utils/textUtils.js';
import type { Part } from '@google/genai';
import { checkExhaustive } from './checks.js';
import {
MessageType,
ToolCallStatus,
type HistoryItemWithoutId,
} from '../ui/types.js';
/**
* Constant for the resume "latest" identifier.
@@ -514,3 +521,190 @@ export class SessionSelector {
}
}
}
/**
* 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,
};
}