Skip to content

fix(client): clear a poll's own error once the poll recovers - #75

Closed
aniruddhaadak80 wants to merge 1 commit into
CopilotKit:mainfrom
aniruddhaadak80:fix/clear-a-poll-error-on-recovery
Closed

aniruddhaadak80 wants to merge 1 commit into
CopilotKit:mainfrom
aniruddhaadak80:fix/clear-a-poll-error-on-recovery

Conversation

@aniruddhaadak80

Copy link
Copy Markdown

What this changes

The red banner in the app shell is fed by three things: the state poll, the capture poll, and the actions the owner takes. Only the actions ever cleared it.

The state poll (src/client/App.tsx) looked like this:

const [s, w] = await Promise.all([api<State>('/state'), api<WorkspaceState>('/workspace')]);
setState(s);
setWorkspace(w);
setNeedsAuth(false);
setSelectedDot((previous) => previous || w.dots[0]?.id || '');
// no setError('')

So once the poll failed, its message was latched for the life of the page. Restart the server, or lose the network for five seconds, and the app comes back visibly working — the sidebar, the task list and the chat all repaint from the very request that succeeded — while a red banner still says it is broken. The only way out is the Dismiss button, on an error that is no longer true.

The capture poll twenty lines below had the same shape and the same gap, so it is fixed here as well rather than leaving one copy of the bug behind.

Neither poll now clears the banner unconditionally. They remember the message they last failed with and clear only that. This matters because the banner is shared: a bare setError('') on poll success would take away the message from a failed action the owner had not finished reading, one 3-second tick after it appeared. afterPollSuccess states that rule on its own so it can be tested without a DOM.

The sibling poller in SpaceWorkspace.tsx:46 already clears on success at the same 3-second cadence, which is why this reads as an oversight rather than a decision.

Closes the behaviour reported in #51.

Why the logic is extracted rather than inlined

This repo's component tests render with renderToStaticMarkup (tests/controls.test.tsx, tests/transcript.test.tsx), which never runs an effect — so a useEffect + setInterval + async path cannot be asserted that way, and reaching for a DOM environment would mean adding test infrastructure this PR has no business adding. Extracting the one rule into src/client/poll-error.ts matches how the client already factors out pure logic (mergePageSnapshot in SpaceWorkspace, PageChatRequests in page-chat-requests.ts, covered by tests/page-chat-request.test.ts).

What I did not change, and why

The capture poll also has no in-flight guard, so when a response is slow the 3-second ticks overlap and an older capture can overwrite a newer one. That is a real defect, but it is a different root cause from the latched error and mixing them would make this harder to review. ComputerPanel.tsx:40-120 already solves it with a busy flag plus a revision counter. I left it alone deliberately rather than bundling it — happy to send it separately.

Verification

Ran on a clean checkout of main with dependencies installed:

  • npm test — baseline before my change: 35 files, 164 tests, all passing. After: 36 files, 169 tests, all passing. The five new ones are in tests/poll-error.test.ts and cover: clearing the poll's own error; leaving an action error alone; leaving an error alone when the poll has not failed; staying empty after a manual dismiss; and not clearing a different message that happens to be showing.
  • I confirmed the tests go red first — before src/client/poll-error.ts existed, tests/poll-error.test.ts failed to resolve its import, which is the honest version of "red" for an extracted rule.
  • npm run typecheck — exit 0. This caught a real mistake: I had first written const captureError = useRef<string>() inside the useEffect body, which is a Rules-of-Hooks violation, and then useRef<string>() with no argument, which React 19's types reject. Both are fixed; the refs are now declared at component top level.
  • npm run lint — exit 0.
  • npx tsc -p tsconfig.server.json --noEmit — exit 0, the production build's type half.

Per CONTRIBUTING.md I also exercised the changed UI behaviour in a browser: I have not done that, and I want to be plain about it rather than imply otherwise. What I can say concretely is that the rule is unit-tested and that the change is confined to when an existing message is cleared — no request, response shape, or rendered text changes. npm run check-format is also worth a note: it reports ~105 files in my working tree, including files I never touched, because core.autocrlf=true checks them out with CRLF while prettier expects LF. That is pre-existing and local, not something this PR introduces — the three files I touched contain zero CRLF pairs and pass the check individually. It looks like the same class of Windows issue #34 and PR #35 describe.

The banner is fed by three things: the state poll, the capture poll, and the
actions the owner takes. Only the actions ever cleared it.

  const [s, w] = await Promise.all([api('/state'), api('/workspace')]);
  setState(s);
  setWorkspace(w);
  // no setError('')

So a failure the poll reported was latched for the life of the page. Restart
the server, or lose the network for five seconds, and the app comes back
visibly working - the sidebar, the task list and the chat all repaint from
the very request that succeeded - while a red banner still says it is broken.
The only way out was the Dismiss button, on an error that was no longer true.

The capture poll twenty lines below had the same shape and the same gap, so
it is fixed here too rather than leaving one copy of the bug behind.

Neither poll clears the banner unconditionally. They remember the message
they last failed with and clear only that, because the banner is shared: an
unconditional setError('') would take away the message from a failed action
the owner had not finished reading, one poll tick after it appeared. That is
the same rule SpaceWorkspace's own poll already follows by clearing on
success, and it is what afterPollSuccess states on its own so it can be
tested without a DOM.
Copilot AI balanced review requested due to automatic review settings October 4, 2026 09:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aniruddhaadak80

Copy link
Copy Markdown
Author

Closing this in favour of #73, which is the same fix and reached it first.

I worked this from issue #51 independently and landed the same rule: a poll should clear the failure it raised, and nothing else, because the banner is shared with errors from actions the owner just took, and a blanket setError('') would take those away a tick after they appeared. I extracted it to a small module with unit tests for the same reason.

Having now read #73, it is the better version of that idea and I would rather not compete with it:

  • it models connection and action as separate notice slots, where mine held the last poll-raised message in a ref and compared strings - two different sources producing identical text could clear the wrong one
  • it covers the capture poll and the 401 path explicitly
  • it carries noticeably more test coverage for the same surface

No need to merge mine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants