Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions packages/web-shell/client/components/MessageList.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2793,6 +2793,96 @@ describe('MessageList — turn collapse (DOM)', () => {
expect(assistantActions(c, 'summary')).toBe('true');
});

it('does not render final actions while AskUserQuestion is waiting', () => {
const renderAssistantTurnFooter = vi.fn(() => (
<span data-testid="assistant-turn-footer">footer</span>
));
const c = mount(
[
userMsg('review-request'),
asstMsg('critical-findings'),
standaloneToolMsg('ask-user', 'AskUserQuestion'),
],
undefined,
{ customization: { renderAssistantTurnFooter } },
);

expect(assistantActions(c, 'critical-findings')).toBe('false');
expect(renderAssistantTurnFooter).not.toHaveBeenCalled();
expect(c.querySelector('[data-testid="assistant-turn-footer"]')).toBeNull();
});

it('restores final actions after matched agent notifications even when launch rows stay pending', () => {
const firstAgent = agentMsg('agent-1');
const secondAgent = agentMsg('agent-2');
firstAgent.tools[0]!.status = 'pending';
secondAgent.tools[0]!.status = 'pending';
const renderAssistantTurnFooter = vi.fn(() => (
<span data-testid="assistant-turn-footer">footer</span>
));

const c = mount(
[
userMsg('review-request'),
asstMsg('critical-findings'),
standaloneToolMsg('ask-user', 'AskUserQuestion'),
userMsg('ask-user-answer'),
firstAgent,
secondAgent,
asstMsg('report'),
backgroundNotificationMsg('bg-1', 'call-agent-1'),
backgroundNotificationMsg('bg-2', 'call-agent-2'),
thinkingMsg('late-thinking'),
asstMsg('final-supplement'),
],
undefined,
{ customization: { renderAssistantTurnFooter } },
);

expect(assistantActions(c, 'report')).toBe('false');
expect(assistantActions(c, 'final-supplement')).toBe('true');
expect(renderAssistantTurnFooter.mock.calls.map(([info]) => info)).toEqual(
expect.arrayContaining([
{
turnId: 'ask-user-answer',
message: {
id: 'final-supplement',
content: 'answer',
isStreaming: undefined,
timestamp: undefined,
},
},
]),
);
expect(
renderAssistantTurnFooter.mock.calls.every(
([info]) => info.message.id === 'final-supplement',
),
).toBe(true);
expect(
c.querySelectorAll('[data-testid="assistant-turn-footer"]'),
).toHaveLength(1);
});

it('does not release an older turn for another agent completion', () => {
const firstAgent = agentMsg('agent-1');
const secondAgent = agentMsg('agent-2');
firstAgent.tools[0]!.status = 'pending';
secondAgent.tools[0]!.status = 'pending';
const c = mount([
userMsg('u1'),
firstAgent,
asstMsg('a1'),
userMsg('u2'),
secondAgent,
backgroundNotificationMsg('bg-2', 'call-agent-2'),
asstMsg('a2'),
]);

expect(assistantActions(c, 'a1')).toBe('false');
expect(assistantActions(c, 'a2')).toBe('true');
});

it('keeps actions suppressed for stale agents until they reconcile terminal', () => {
const firstAgent = agentMsg('agent-1');
const secondAgent = agentMsg('agent-2');
Expand Down Expand Up @@ -2828,6 +2918,32 @@ describe('MessageList — turn collapse (DOM)', () => {
expect(assistantActions(c, 'a1')).toBe('true');
});

it('restores the custom footer during readonly transcript replay', () => {
const staleAgent = agentMsg('agent-1');
staleAgent.tools[0]!.status = 'pending';
const renderAssistantTurnFooter = vi.fn(() => (
<span data-testid="assistant-turn-footer">footer</span>
));
const c = mount([userMsg('u1'), staleAgent, asstMsg('a1')], undefined, {
transcriptRenderMode: 'readonly',
customization: { renderAssistantTurnFooter },
});

expect(assistantActions(c, 'a1')).toBe('true');
expect(renderAssistantTurnFooter).toHaveBeenCalledWith({
turnId: 'u1',
message: {
id: 'a1',
content: 'answer',
isStreaming: undefined,
timestamp: undefined,
},
});
expect(
c.querySelectorAll('[data-testid="assistant-turn-footer"]'),
).toHaveLength(1);
});

it('keeps final actions for a pending foreground agent in a completed turn', () => {
const foregroundAgent = agentMsg('agent-1');
foregroundAgent.tools[0]!.status = 'pending';
Expand Down
19 changes: 17 additions & 2 deletions packages/web-shell/client/components/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,7 @@ function collectFinalAssistantTurnIds(
}

const turnIdByAssistantId = new Map<string, string>();
const completedAgentCallIds = completedBackgroundAgentCallIds(items);
for (let k = 0; k < userIdxs.length; k++) {
const start = userIdxs[k];
const end = (k + 1 < userIdxs.length ? userIdxs[k + 1] : items.length) - 1;
Expand All @@ -808,7 +809,7 @@ function collectFinalAssistantTurnIds(
// whether it is the latest turn or the user has moved on to a newer one.
if (
gateBackgroundAgentStatus &&
turnHasActiveBackgroundAgent(items, start, end)
turnHasActiveBackgroundAgent(items, start, end, completedAgentCallIds)
) {
continue;
}
Expand Down Expand Up @@ -1667,13 +1668,16 @@ function turnHasActiveBackgroundAgent(
items: readonly DisplayItem[],
start: number,
end: number,
completedAgentCallIds = completedBackgroundAgentCallIds(items),
Comment thread
dreamWB marked this conversation as resolved.
Outdated
Comment thread
dreamWB marked this conversation as resolved.
Outdated
): boolean {
return someTurnToolCall(
items,
start,
end,
(tool) =>
isBackgroundSubAgentToolCall(tool) && isActiveToolStatus(tool.status),
isBackgroundSubAgentToolCall(tool) &&
isActiveToolStatus(tool.status) &&
!completedAgentCallIds.has(tool.callId),
Comment thread
dreamWB marked this conversation as resolved.
Outdated
Comment thread
dreamWB marked this conversation as resolved.
Outdated
);
}

Expand Down Expand Up @@ -1753,6 +1757,17 @@ function backgroundAgentCompletion(
: null;
}

function completedBackgroundAgentCallIds(
items: readonly DisplayItem[],
): ReadonlySet<string> {
const callIds = new Set<string>();
for (const item of items) {
const callId = backgroundAgentCompletion(item)?.callId;
if (callId) callIds.add(callId);
}
return callIds;
}

interface BackgroundAgentSummaryState {
lastNotificationIndex: number;
sawAgentCompletion: boolean;
Expand Down
Loading