diff --git a/.changeset/cool-papers-behave.md b/.changeset/cool-papers-behave.md new file mode 100644 index 00000000000..5cd3c20c869 --- /dev/null +++ b/.changeset/cool-papers-behave.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +[fix] Task progress bars once again clear when complete diff --git a/packages/cli-kit/src/private/node/ui/components/SingleTask.tsx b/packages/cli-kit/src/private/node/ui/components/SingleTask.tsx index a58113df20f..e0a931721c9 100644 --- a/packages/cli-kit/src/private/node/ui/components/SingleTask.tsx +++ b/packages/cli-kit/src/private/node/ui/components/SingleTask.tsx @@ -9,11 +9,12 @@ interface SingleTaskProps { title: TokenizedString task: (updateStatus: (status: TokenizedString) => void) => Promise onComplete?: (result: T) => void + onError?: (error: Error) => void onAbort?: () => void noColor?: boolean } -const SingleTask = ({task, title, onComplete, onAbort, noColor}: SingleTaskProps) => { +const SingleTask = ({task, title, onComplete, onError, onAbort, noColor}: SingleTaskProps) => { const [status, setStatus] = useState(title) const [isDone, setIsDone] = useState(false) const {exit: unmountInk} = useApp() @@ -35,13 +36,16 @@ const SingleTask = ({task, title, onComplete, onAbort, noColor}: SingleTaskP .then((result) => { setIsDone(true) onComplete?.(result) - unmountInk() + // Defer unmount so React 19 can flush batched state updates + // before the component tree is torn down. + setImmediate(() => unmountInk()) }) .catch((error) => { setIsDone(true) - unmountInk(error) + onError?.(error) + setImmediate(() => unmountInk(error)) }) - }, [task, unmountInk, onComplete]) + }, [task, unmountInk, onComplete, onError]) if (isDone) { // clear things once done diff --git a/packages/cli-kit/src/private/node/ui/hooks/use-async-and-unmount.ts b/packages/cli-kit/src/private/node/ui/hooks/use-async-and-unmount.ts index d2ed970f157..e9a38890478 100644 --- a/packages/cli-kit/src/private/node/ui/hooks/use-async-and-unmount.ts +++ b/packages/cli-kit/src/private/node/ui/hooks/use-async-and-unmount.ts @@ -16,11 +16,13 @@ export default function useAsyncAndUnmount( asyncFunction() .then(() => { onFulfilled() - unmountInk() + // Defer unmount so React 19 can flush batched state updates + // before the component tree is torn down. + setImmediate(() => unmountInk()) }) .catch((error) => { onRejected(error) - unmountInk(error) + setImmediate(() => unmountInk(error)) }) }, []) } diff --git a/packages/cli-kit/src/public/node/ui.tsx b/packages/cli-kit/src/public/node/ui.tsx index c9ac3e6b3c7..c9d4da69dd4 100644 --- a/packages/cli-kit/src/public/node/ui.tsx +++ b/packages/cli-kit/src/public/node/ui.tsx @@ -522,7 +522,7 @@ export async function renderSingleTask({ renderOptions, }: RenderSingleTaskOptions): Promise { return new Promise((resolve, reject) => { - render(, { + render(, { ...renderOptions, exitOnCtrlC: false, }).catch(reject)