Skip to content

Commit e5d1c57

Browse files
authored
🤖 fix: soften delayed-start retry banner (#2839)
## Summary Avoid showing the RetryBarrier's "Stream interrupted" copy while the first response is still waiting for stream-start. ## Background In SSH workspaces with long init hooks, the first send can stay in pre-stream startup for a while before the backend emits `stream-start`. During that window the frontend still mounted `RetryBarrier`, which made the warning copy misleading even though the response would eventually begin. ## Implementation - detect the delayed-start case when the last actionable row is still the trailing user message and the workspace remains in `isStreamStarting` - render "Response startup is taking longer than expected" for that state instead of claiming the stream was interrupted - add a RetryBarrier regression test that covers the delayed-start copy path ## Validation - `make static-check` ## Risks Low. This only changes RetryBarrier copy for the pre-stream startup state; retry behavior and the actual interruption paths are unchanged. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$2.42`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=2.42 -->
1 parent db6b776 commit e5d1c57

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

src/browser/features/Messages/ChatBarrier/RetryBarrier.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,26 @@ describe("RetryBarrier", () => {
136136
globalThis.document = undefined as unknown as Document;
137137
});
138138

139+
test("uses delayed-start copy while the first response is still starting", () => {
140+
currentWorkspaceState = createWorkspaceState({
141+
isStreamStarting: true,
142+
messages: [
143+
{
144+
type: "user",
145+
id: "user-1",
146+
historyId: "user-1",
147+
content: "Hello",
148+
historySequence: 1,
149+
},
150+
],
151+
});
152+
153+
const view = render(<RetryBarrier workspaceId="ws-1" />);
154+
155+
expect(view.getByText("Response startup is taking longer than expected")).toBeTruthy();
156+
expect(view.queryByText("Stream interrupted")).toBeNull();
157+
});
158+
139159
test("shows error details when manual resume fails before stream events", async () => {
140160
resumeStreamResult = {
141161
success: false,

src/browser/features/Messages/ChatBarrier/RetryBarrier.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,23 @@ export const RetryBarrier: React.FC<RetryBarrierProps> = (props) => {
242242
const lastMessage = getLastNonDecorativeMessage(workspaceState.messages);
243243
const lastStreamError = lastMessage?.type === "stream-error" ? lastMessage : null;
244244
const interruptionReason = lastStreamError?.errorType === "rate_limit" ? "Rate limited" : null;
245+
const isWaitingForInitialResponse =
246+
lastMessage?.type === "user" && workspaceState.isStreamStarting;
245247

246248
let statusIcon: React.ReactNode = (
247249
<AlertTriangle aria-hidden="true" className="text-warning h-4 w-4 shrink-0" />
248250
);
249-
let statusText: React.ReactNode = <>{interruptionReason ?? "Stream interrupted"}</>;
251+
let statusText: React.ReactNode = (
252+
<>
253+
{interruptionReason ??
254+
// A trailing user message means the backend has not emitted stream-start yet.
255+
// Long init hooks (for example over SSH) can legitimately keep us here, so avoid
256+
// claiming the stream was interrupted until we have evidence that it actually was.
257+
(isWaitingForInitialResponse
258+
? "Response startup is taking longer than expected"
259+
: "Stream interrupted")}
260+
</>
261+
);
250262
let actionButton: React.ReactNode = (
251263
<button
252264
className="bg-warning font-primary text-background cursor-pointer rounded border-none px-4 py-2 text-xs font-semibold whitespace-nowrap transition-all duration-200 hover:-translate-y-px hover:brightness-120 active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-50"

0 commit comments

Comments
 (0)