Suppress duplicate Assistant thinking status

This commit is contained in:
rcourtman
2026-06-05 20:40:59 +01:00
parent 3d8a926acc
commit 5e4b720d5f
3 changed files with 65 additions and 9 deletions
@@ -177,11 +177,14 @@ runtime cost control, and shared AI transport surfaces.
frontend rendering. Compacted no-whitespace internal prelude text attached
to a leaked tool invocation is part of that same artifact and must be
suppressed or retracted from the current stream segment instead of rendered
as assistant prose. Completed tool rows in the drawer may show compact tool
name, action summary, status, and an explicit details affordance, but raw
tool input/output JSON must not render in the default transcript. Token
accounting and other provider metadata remain runtime/accounting data, not
normal transcript prose.
as assistant prose. Drawer-level loading/progress status must not duplicate
transcript output once assistant content, governed tool progress, approvals,
questions, errors, or restored tool evidence are already visible; progress
stays in the transcript row that owns the active turn. Completed tool rows
in the drawer may show compact tool name, action summary, status, and an
explicit details affordance, but raw tool input/output JSON must not render
in the default transcript. Token accounting and other provider metadata
remain runtime/accounting data, not normal transcript prose.
4. Add or change Patrol, alert-analysis, or remediation transport through `internal/api/ai_handlers.go`, `internal/api/ai_intelligence_handlers.go`, and `frontend-modern/src/api/patrol.ts`
Provider preflight diagnostics returned from `internal/api/ai_handlers.go`
must reuse the Patrol runtime failure classifier in `internal/ai/` and
@@ -518,7 +518,8 @@ describe('AIChat', () => {
id: 'assistant-error-openrouter',
role: 'assistant',
content: '',
error: 'The AI provider rejected the credentials. Check your AI provider API key in Settings.',
error:
'The AI provider rejected the credentials. Check your AI provider API key in Settings.',
timestamp: new Date('2026-06-05T10:00:00Z'),
model: 'openrouter:deepseek/deepseek-v4-pro',
};
@@ -2925,6 +2926,7 @@ describe('AIChat', () => {
]);
renderChat();
expect(screen.queryByText('Running get nodes...')).not.toBeInTheDocument();
expect(screen.queryByText('Thinking...')).not.toBeInTheDocument();
});
it('does not duplicate status in the footer when assistant content is streaming', () => {
@@ -2943,6 +2945,23 @@ describe('AIChat', () => {
expect(screen.queryByText('Thinking...')).not.toBeInTheDocument();
});
it('does not fall back to thinking when loading outlives visible assistant content', () => {
mockChat.isLoading.mockReturnValue(true);
mockChat.messages.mockReturnValue([
{
id: 'msg-1',
role: 'assistant' as const,
content: 'UI_PARITY_OK',
timestamp: new Date(),
isStreaming: false,
streamEvents: [{ type: 'content', content: 'UI_PARITY_OK' }],
},
]);
renderChat();
expect(screen.queryByText('Thinking...')).not.toBeInTheDocument();
expect(screen.queryByText('Generating response...')).not.toBeInTheDocument();
});
it('keeps queued follow-up status without duplicating active assistant streaming status', () => {
mockChat.isLoading.mockReturnValue(true);
mockChat.queuedFollowUpCount.mockReturnValue(1);
@@ -227,9 +227,8 @@ const findProviderReadinessAlternative = (args: {
});
const candidate =
sortedCandidates.find(
({ model }) => normalizeComparableModelKey(model.id) === selectedKey,
) || sortedCandidates[0];
sortedCandidates.find(({ model }) => normalizeComparableModelKey(model.id) === selectedKey) ||
sortedCandidates[0];
if (!candidate) return null;
return {
@@ -1059,6 +1058,36 @@ export const AIChat: Component<AIChatProps> = (props) => {
}),
);
const assistantHasVisibleTranscriptOutput = (message: ChatMessage) => {
if (message.role !== 'assistant') return false;
if ((message.content || '').trim() || message.error) return true;
if (
message.pendingTools?.length ||
message.pendingApprovals?.length ||
message.pendingQuestions?.length ||
message.toolCalls?.length
) {
return true;
}
return (message.streamEvents || []).some((event) => {
switch (event.type) {
case 'content':
return !!event.content?.trim();
case 'tool':
return !!event.tool;
case 'pending_tool':
return !!event.pendingTool;
case 'approval':
return !!event.approval;
case 'question':
return !!event.question;
default:
return false;
}
});
};
// Compute current status for display
const currentStatus = createMemo(() => {
if (!chat.isLoading()) return null;
@@ -1071,6 +1100,11 @@ export const AIChat: Component<AIChatProps> = (props) => {
return null;
}
const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant');
if (lastAssistant && assistantHasVisibleTranscriptOutput(lastAssistant)) {
return null;
}
const lastMessage = messages[messages.length - 1];
if (!lastMessage || lastMessage.role !== 'assistant') {