mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
Suppress duplicate Assistant thinking status
This commit is contained in:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user