Skip to content

Commit 3bfb874

Browse files
committed
🤖 fix: avoid retry banner during normal TTFT
1 parent 11a3acc commit 3bfb874

3 files changed

Lines changed: 53 additions & 46 deletions

File tree

src/browser/components/AIView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ const AIViewInner: React.FC<AIViewProps> = ({
460460
// Track if last message was interrupted or errored (for RetryBarrier)
461461
// Uses same logic as useResumeManager for DRY
462462
const showRetryBarrier = workspaceState
463-
? hasInterruptedStream(workspaceState.messages, workspaceState.pendingStreamStartTime) ||
463+
? (!workspaceState.canInterrupt &&
464+
hasInterruptedStream(workspaceState.messages, workspaceState.pendingStreamStartTime)) ||
464465
shouldKeepRetryBarrierVisibleDuringRetry(workspaceState.messages, retryAttempt)
465466
: false;
466467

src/browser/utils/messages/retryEligibility.test.ts

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -328,23 +328,12 @@ describe("shouldKeepRetryBarrierVisibleDuringRetry", () => {
328328
errorType: "network",
329329
historySequence: 1,
330330
},
331-
{
332-
type: "assistant",
333-
id: "assistant-2",
334-
historyId: "assistant-2",
335-
content: "",
336-
historySequence: 2,
337-
isStreaming: true,
338-
isPartial: false,
339-
isCompacted: false,
340-
isIdleCompacted: false,
341-
},
342331
];
343332

344333
expect(shouldKeepRetryBarrierVisibleDuringRetry(messages, 0)).toBe(false);
345334
});
346335

347-
it("returns true when attempt > 0 and stream has started but no content yet", () => {
336+
it("returns true when attempt > 0 and we're still displaying the interrupted message", () => {
348337
const messages: DisplayedMessage[] = [
349338
{
350339
type: "stream-error",
@@ -354,14 +343,21 @@ describe("shouldKeepRetryBarrierVisibleDuringRetry", () => {
354343
errorType: "network",
355344
historySequence: 1,
356345
},
346+
];
347+
348+
expect(shouldKeepRetryBarrierVisibleDuringRetry(messages, 1)).toBe(true);
349+
});
350+
351+
it("returns true for partial assistant messages", () => {
352+
const messages: DisplayedMessage[] = [
357353
{
358354
type: "assistant",
359-
id: "assistant-2",
360-
historyId: "assistant-2",
361-
content: "",
362-
historySequence: 2,
363-
isStreaming: true,
364-
isPartial: false,
355+
id: "assistant-1",
356+
historyId: "assistant-1",
357+
content: "Incomplete response",
358+
historySequence: 1,
359+
isStreaming: false,
360+
isPartial: true,
365361
isCompacted: false,
366362
isIdleCompacted: false,
367363
},
@@ -396,7 +392,7 @@ describe("shouldKeepRetryBarrierVisibleDuringRetry", () => {
396392
expect(shouldKeepRetryBarrierVisibleDuringRetry(messages, 1)).toBe(false);
397393
});
398394

399-
it("returns false when the stream was started by a fresh user message", () => {
395+
it("returns false for fresh user messages", () => {
400396
const messages: DisplayedMessage[] = [
401397
{
402398
type: "user",
@@ -405,17 +401,6 @@ describe("shouldKeepRetryBarrierVisibleDuringRetry", () => {
405401
content: "Hello",
406402
historySequence: 1,
407403
},
408-
{
409-
type: "assistant",
410-
id: "assistant-2",
411-
historyId: "assistant-2",
412-
content: "",
413-
historySequence: 2,
414-
isStreaming: true,
415-
isPartial: false,
416-
isCompacted: false,
417-
isIdleCompacted: false,
418-
},
419404
];
420405

421406
expect(shouldKeepRetryBarrierVisibleDuringRetry(messages, 1)).toBe(false);

src/browser/utils/messages/retryEligibility.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -132,31 +132,52 @@ export function hasInterruptedStream(
132132
* In that window, AIView previously hid RetryBarrier because canInterrupt flipped to true,
133133
* causing the banner to flicker on every retry attempt.
134134
*
135-
* This helper keeps RetryBarrier visible while the new stream is active but has not produced
136-
* any visible assistant content yet.
135+
* Note: A new stream-start doesn't always immediately produce a new assistant DisplayedMessage
136+
* (we don't render empty assistant messages). During TTFT gaps, the last displayed message can
137+
* still be the prior interruption.
137138
*/
138139
export function shouldKeepRetryBarrierVisibleDuringRetry(
139140
messages: DisplayedMessage[],
140141
retryAttempt: number
141142
): boolean {
142143
if (retryAttempt <= 0) return false;
143-
if (messages.length < 2) return false;
144+
if (messages.length === 0) return false;
144145

145146
const lastMessage = messages[messages.length - 1];
146-
if (lastMessage.type !== "assistant") return false;
147-
if (!lastMessage.isStreaming) return false;
148-
if (lastMessage.content.trim().length > 0) return false;
149147

150-
const previousMessage = messages[messages.length - 2];
148+
// If we're still showing the interruption message while a new retry stream is active,
149+
// keep the banner visible so it doesn't flicker away on stream-start.
150+
if (lastMessage.type === "stream-error") {
151+
return true;
152+
}
151153

152-
// Only keep the banner sticky when this stream is a retry/resume attempt.
153-
// If the previous message is a fresh user message, we want normal streaming UX.
154-
return (
155-
previousMessage.type === "stream-error" ||
156-
(previousMessage.type === "assistant" && previousMessage.isPartial === true) ||
157-
(previousMessage.type === "tool" && previousMessage.isPartial === true) ||
158-
(previousMessage.type === "reasoning" && previousMessage.isPartial === true)
159-
);
154+
if (
155+
(lastMessage.type === "assistant" ||
156+
lastMessage.type === "tool" ||
157+
lastMessage.type === "reasoning") &&
158+
lastMessage.isPartial === true
159+
) {
160+
return true;
161+
}
162+
163+
// If we do have a streaming assistant block but it hasn't produced any visible content yet,
164+
// keep the banner sticky until the first non-empty delta arrives.
165+
if (
166+
lastMessage.type === "assistant" &&
167+
lastMessage.isStreaming &&
168+
lastMessage.content.trim().length === 0 &&
169+
messages.length >= 2
170+
) {
171+
const previousMessage = messages[messages.length - 2];
172+
return (
173+
previousMessage.type === "stream-error" ||
174+
(previousMessage.type === "assistant" && previousMessage.isPartial === true) ||
175+
(previousMessage.type === "tool" && previousMessage.isPartial === true) ||
176+
(previousMessage.type === "reasoning" && previousMessage.isPartial === true)
177+
);
178+
}
179+
180+
return false;
160181
}
161182

162183
/**

0 commit comments

Comments
 (0)