v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes

Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch
rewrite with streaming/persist, permission prompts, and agent commands.
Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline,
WS user-delta replace, and inference orphan tool_call stripping.
Archive openspec v2-2; update CHANGELOG and CURRENT.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 15:18:31 +00:00
parent 04673eaf59
commit 93d3f86c2b
96 changed files with 6694 additions and 1329 deletions

View File

@@ -226,6 +226,76 @@ describe('buildMessagesPayload', async () => {
expect(result[4]).toMatchObject({ role: 'assistant', content: 'here it is' });
});
it('strips assistant tool_calls when matching tool results are missing', async () => {
const session = makeSession();
const project = makeProject();
const toolCall: ToolCall = {
id: 'call_orphan',
name: 'grep',
args: { pattern: 'foo' },
};
const history: Message[] = [
makeMessage('user', 'search'),
makeMessage('assistant', 'partial answer', { tool_calls: [toolCall] }),
makeMessage('assistant', 'final answer'),
];
const result = await buildMessagesPayload(session, project, history);
// tool_calls stripped from the orphan turn; text content kept.
expect(result).toHaveLength(4);
expect(result[1]).toMatchObject({ role: 'user', content: 'search' });
expect(result[2]).toMatchObject({ role: 'assistant', content: 'partial answer' });
expect(result[2]!.tool_calls).toBeUndefined();
expect(result[3]).toMatchObject({ role: 'assistant', content: 'final answer' });
});
it('drops tool-call-only assistant rows when tool results never arrived', async () => {
const session = makeSession();
const project = makeProject();
const toolCall: ToolCall = {
id: 'call_orphan_only',
name: 'grep',
args: { pattern: 'foo' },
};
const history: Message[] = [
makeMessage('user', 'search'),
makeMessage('assistant', '', { tool_calls: [toolCall] }),
makeMessage('assistant', 'final answer'),
];
const result = await buildMessagesPayload(session, project, history);
expect(result).toHaveLength(3);
expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
});
it('skips stray tool rows when the owning assistant tool_calls were stripped', async () => {
const session = makeSession();
const project = makeProject();
const toolCallA: ToolCall = {
id: 'call_a',
name: 'grep',
args: { pattern: 'foo' },
};
const toolCallB: ToolCall = {
id: 'call_b',
name: 'read',
args: { path: 'x' },
};
const toolResult: ToolResult = {
tool_call_id: 'call_a',
output: 'match',
truncated: false,
};
const history: Message[] = [
makeMessage('user', 'search'),
makeMessage('assistant', '', { tool_calls: [toolCallA, toolCallB] }),
makeMessage('tool', '', { tool_results: toolResult }),
makeMessage('assistant', 'final answer'),
];
const result = await buildMessagesPayload(session, project, history);
expect(result).toHaveLength(3);
expect(result.find((m) => m.role === 'tool')).toBeUndefined();
expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
});
it('skips tool rows with no tool_results', async () => {
const session = makeSession();
const project = makeProject();