diff --git a/canvas/src/components/tabs/ChatTab.tsx b/canvas/src/components/tabs/ChatTab.tsx index 9f2ae508..af3f421c 100644 --- a/canvas/src/components/tabs/ChatTab.tsx +++ b/canvas/src/components/tabs/ChatTab.tsx @@ -243,6 +243,22 @@ function MyChatPanel({ workspaceId, data }: Props) { } }, [data.status, clearSendError]); + // Clear any stale "Failed to send — agent may be unreachable" banner the + // moment the agent is demonstrably WORKING (core#2697). `thinking` is true + // when the user's send is in flight OR the workspace heartbeat reports an + // in-flight task — either way the agent is reachable, so an "unreachable" + // banner is self-contradictory (reported: banner shown beside a live + // "●●● 102s" timer + streaming tool calls on a long poll-mode turn). + // #2736 only cleared on a reply LANDING; this also clears the instant the + // agent starts/continues working, so the banner can't linger through a + // multi-minute turn that hasn't replied yet. + useEffect(() => { + if (thinking) { + setError(null); + clearSendError(); + } + }, [thinking, clearSendError]); + // Scroll behavior across messages updates: // - Prepend (loadOlder landed) → restore the user's saved // distance-from-bottom so their reading position is unchanged. diff --git a/canvas/src/components/tabs/__tests__/ChatTab.errorClearOnReply.test.tsx b/canvas/src/components/tabs/__tests__/ChatTab.errorClearOnReply.test.tsx index 5802a20c..bce1739b 100644 --- a/canvas/src/components/tabs/__tests__/ChatTab.errorClearOnReply.test.tsx +++ b/canvas/src/components/tabs/__tests__/ChatTab.errorClearOnReply.test.tsx @@ -75,4 +75,14 @@ describe("ChatTab — stale error banner clears on a successful reply (core#2697 act(() => { captured.onSendComplete?.(); }); expect(clearErrorSpy).toHaveBeenCalled(); }); + + it("clears the 'unreachable' banner while the agent is THINKING (currentTask set), before any reply", () => { + // The reported bug: banner shown beside a live "●●● 102s" timer on a long + // poll-mode turn that hadn't replied yet. data.currentTask set => thinking + // => the agent is reachable => the unreachable banner must clear on its own. + const busy = { status: "online" as const, runtime: "claude-code", currentTask: "downloading assets" } as unknown as Parameters[0]["data"]; + render(); + // Mount with currentTask set => the thinking-clears-error effect fires. + expect(clearErrorSpy).toHaveBeenCalled(); + }); });