Skip to content

Fix progress bars not clearing on completion after React 19 upgrade#7062

Open
nickwesselman wants to merge 3 commits intomainfrom
fix/progress-bar-clearing-react19
Open

Fix progress bars not clearing on completion after React 19 upgrade#7062
nickwesselman wants to merge 3 commits intomainfrom
fix/progress-bar-clearing-react19

Conversation

@nickwesselman
Copy link
Contributor

@nickwesselman nickwesselman commented Mar 20, 2026

Summary

  • The Ink 6 / React 19 upgrade (Upgrade Ink to v6 and React to v19 #6831, 269f3aa6) deferred unmountInk() in ConcurrentOutput with setImmediate() to let React 19 flush batched state updates, but missed the same pattern in useAsyncAndUnmount (used by Tasks) and SingleTask
  • Without the deferral, unmountInk() fires before the setState that triggers return null is flushed, so the final render still contains the LoadingBar and it is never erased from the terminal
  • Wraps unmountInk() in setImmediate() in both places, matching the existing fix in ConcurrentOutput
  • Fix error swallowing on sequential renderSingleTask calls: The setImmediate deferral introduced a race condition — when renderSingleTask is called sequentially, the first instance's deferred unmountInk() can fire after the second instance starts, causing the second waitUntilExit() to resolve prematurely. If the second task throws, the error is silently swallowed (the promise never settles). Fix: add onError callback to SingleTask (mirroring onComplete) so errors propagate directly rather than relying on waitUntilExit() rejection.

Repro: shopify kitchen-sink async — progress bars remain on screen after completing.

See the equivalent change in ConcurrentOutput.tsx from #6831 (269f3aa6).

Test plan

  • Tasks.test.tsx — 13 tests pass
  • SingleTask.test.tsx — 11 tests pass
  • ui.test.ts — 16 tests pass
  • Verified sequential renderSingleTask error path works (previously hung indefinitely)
  • Manual: run shopify kitchen-sink async and verify progress bars clear on completion

🤖 Generated with Claude Code

@nickwesselman nickwesselman marked this pull request as ready for review March 20, 2026 16:20
@nickwesselman nickwesselman requested a review from a team as a code owner March 20, 2026 16:20
@github-actions

This comment has been minimized.

The Ink 6 / React 19 upgrade (269f3aa) deferred unmountInk() in
ConcurrentOutput to let React 19 flush batched state updates, but
missed the same pattern in useAsyncAndUnmount (used by Tasks) and
SingleTask. Without the deferral, unmountInk() fires before the
setState that triggers `return null` is flushed, so the final render
still contains the LoadingBar and it is never erased.

Wrap unmountInk() in setImmediate() in both places, matching the
existing fix in ConcurrentOutput.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nickwesselman nickwesselman force-pushed the fix/progress-bar-clearing-react19 branch from 465d331 to 55dbfc8 Compare March 20, 2026 16:24
@nickwesselman nickwesselman requested a review from a team as a code owner March 20, 2026 16:26
Copy link
Contributor

@ryancbahan ryancbahan left a comment

Choose a reason for hiding this comment

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

Let a note in Slack -- we should merge to fix regression, since this matches code we shipped elsewhere. but this manual management of Node event loop within a React side effect to handle mounting/unmounting feels...very cursed. I'd like us to come back to this.

The setImmediate deferral of unmountInk() introduced a race condition
when renderSingleTask is called sequentially: the first instance's
deferred unmountInk() can fire after the second instance starts,
causing the second waitUntilExit() to resolve prematurely. If the
second task then throws, the error is silently swallowed because
render().catch(reject) never fires.

Fix: add onError callback to SingleTask (mirroring onComplete) so
errors are propagated directly via the callback rather than relying
on waitUntilExit() rejection, which is unreliable across sequential
ink renders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/ui/components/SingleTask.d.ts
@@ -4,8 +4,9 @@ interface SingleTaskProps<T> {
     title: TokenizedString;
     task: (updateStatus: (status: TokenizedString) => void) => Promise<T>;
     onComplete?: (result: T) => void;
+    onError?: (error: Error) => void;
     onAbort?: () => void;
     noColor?: boolean;
 }
-declare const SingleTask: <T>({ task, title, onComplete, onAbort, noColor }: SingleTaskProps<T>) => React.JSX.Element | null;
+declare const SingleTask: <T>({ task, title, onComplete, onError, onAbort, noColor }: SingleTaskProps<T>) => React.JSX.Element | null;
 export { SingleTask };
\ No newline at end of file

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