fix: ACP: separate conversational text from execute tool command title (#23179)

This commit is contained in:
Sri Pasumarthi
2026-03-23 17:39:15 -07:00
committed by GitHub
parent a7bed2cc4c
commit 84caf00cd4
7 changed files with 247 additions and 19 deletions

View File

@@ -1080,6 +1080,70 @@ describe('Session', () => {
);
});
it('should split getDisplayTitle and getExplanation for title and content in permission request', async () => {
const confirmationDetails = {
type: 'info',
onConfirm: vi.fn(),
};
mockTool.build.mockReturnValue({
getDescription: () => 'Original Description',
getDisplayTitle: () => 'Display Title Only',
getExplanation: () => 'A detailed explanation text',
toolLocations: () => [],
shouldConfirmExecute: vi.fn().mockResolvedValue(confirmationDetails),
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
});
mockConnection.requestPermission.mockResolvedValue({
outcome: {
outcome: 'selected',
optionId: ToolConfirmationOutcome.ProceedOnce,
},
});
const stream1 = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
functionCalls: [{ name: 'test_tool', args: {} }],
},
},
]);
const stream2 = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream
.mockResolvedValueOnce(stream1)
.mockResolvedValueOnce(stream2);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Call tool' }],
});
expect(mockConnection.requestPermission).toHaveBeenCalledWith(
expect.objectContaining({
toolCall: expect.objectContaining({
title: 'Display Title Only',
content: [],
}),
}),
);
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'agent_thought_chunk',
content: { type: 'text', text: 'A detailed explanation text' },
}),
}),
);
});
it('should use filePath for ACP diff content in tool result', async () => {
mockTool.build.mockReturnValue({
getDescription: () => 'Test Tool',

View File

@@ -947,6 +947,23 @@ export class Session {
try {
const invocation = tool.build(args);
const displayTitle =
typeof invocation.getDisplayTitle === 'function'
? invocation.getDisplayTitle()
: invocation.getDescription();
const explanation =
typeof invocation.getExplanation === 'function'
? invocation.getExplanation()
: '';
if (explanation) {
await this.sendUpdate({
sessionUpdate: 'agent_thought_chunk',
content: { type: 'text', text: explanation },
});
}
const confirmationDetails =
await invocation.shouldConfirmExecute(abortSignal);
@@ -978,7 +995,7 @@ export class Session {
toolCall: {
toolCallId: callId,
status: 'pending',
title: invocation.getDescription(),
title: displayTitle,
content,
locations: invocation.toolLocations(),
kind: toAcpToolKind(tool.kind),
@@ -1014,12 +1031,14 @@ export class Session {
}
}
} else {
const content: acp.ToolCallContent[] = [];
await this.sendUpdate({
sessionUpdate: 'tool_call',
toolCallId: callId,
status: 'in_progress',
title: invocation.getDescription(),
content: [],
title: displayTitle,
content,
locations: invocation.toolLocations(),
kind: toAcpToolKind(tool.kind),
});
@@ -1028,12 +1047,14 @@ export class Session {
const toolResult: ToolResult = await invocation.execute(abortSignal);
const content = toToolCallContent(toolResult);
const updateContent: acp.ToolCallContent[] = content ? [content] : [];
await this.sendUpdate({
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
title: displayTitle,
content: updateContent,
locations: invocation.toolLocations(),
kind: toAcpToolKind(tool.kind),
});