Reapply: central unmount for ink render tree + fix render lifecycle race#7217
Merged
ryancbahan merged 3 commits intomainfrom Apr 9, 2026
Merged
Reapply: central unmount for ink render tree + fix render lifecycle race#7217ryancbahan merged 3 commits intomainfrom
ryancbahan merged 3 commits intomainfrom
Conversation
…ode's event loop" This reverts commit f3a5b94.
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
renderTasks and renderSingleTask resolved their promises via
onComplete={resolve}, which fires inside the React tree before
ink unmounts. This caused the caller to write to stdout while
ink was still tearing down, resulting in loading bars staying
visible and last lines of output being cut off.
Changed both functions to await render() (which awaits
waitUntilExit()) before returning, matching the pattern the
prompt functions already use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
Differences in type declarationsWe 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:
New type declarationsWe found no new type declarations in this PR Existing type declarationspackages/cli-kit/dist/private/node/ui.d.ts import { Logger, LogLevel } from '../../public/node/output.js';
+import React from 'react';
import { Key, RenderOptions } from 'ink';
import { EventEmitter } from 'events';
+/**
+ * Signal that the current Ink tree is done. Must be called within an
+ * InkLifecycleRoot — throws if the provider is missing so lifecycle
+ * bugs surface immediately instead of silently hanging.
+ */
+export declare function useComplete(): (error?: Error) => void;
+/**
+ * Root wrapper for Ink trees. Owns the single `exit()` call site — children
+ * signal completion via `useComplete()`, which sets state here. The `useEffect`
+ * fires post-render, guaranteeing all batched state updates have been flushed
+ * before the tree is torn down.
+ */
+export declare function InkLifecycleRoot({ children }: {
+ children: React.ReactNode;
+}): React.JSX.Element;
interface RenderOnceOptions {
logLevel?: LogLevel;
logger?: Logger;
renderOptions?: RenderOptions;
}
export declare function renderOnce(element: JSX.Element, { logLevel, renderOptions }: RenderOnceOptions): string | undefined;
-export declare function render(element: JSX.Element, options?: RenderOptions): Promise<unknown>;
+export declare function render(element: JSX.Element, options?: RenderOptions): Promise<void>;
export declare class Stdout extends EventEmitter {
columns: number;
rows: number;
readonly frames: string[];
private _lastFrame?;
constructor(options: {
columns?: number;
rows?: number;
});
write: (frame: string) => void;
lastFrame: () => string | undefined;
}
export declare function handleCtrlC(input: string, key: Key, exit?: () => void): void;
export {};
packages/cli-kit/dist/public/node/ui.d.ts@@ -34,7 +34,7 @@ export interface RenderConcurrentOptions extends PartialBy<ConcurrentOutputProps
* 00:00:00 │ frontend │ third frontend message
*
*/
-export declare function renderConcurrent({ renderOptions, ...props }: RenderConcurrentOptions): Promise<unknown>;
+export declare function renderConcurrent({ renderOptions, ...props }: RenderConcurrentOptions): Promise<void>;
export type AlertCustomSection = CustomSection;
export type RenderAlertOptions = Omit<AlertOptions, 'type'>;
/**
|
Contributor
gonzaloriestra
left a comment
There was a problem hiding this comment.
Review assisted by pair-review
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Reapplies #7153 (reverted in #7157) with a fix for the two bugs that caused the revert:
deploy/store executecompletedRoot cause
renderTasksandrenderSingleTaskresolved their promises viaonComplete={resolve}— a callback that fires inside the React tree before ink unmounts. The caller resumed and wrote to stdout while ink was still tearing down, causing ink's cleanup to overwrite the caller's output.Fix
Changed
renderTasksandrenderSingleTaskto use the same pattern the prompt functions (renderSelectPrompt,renderTextPrompt, etc.) already use:This guarantees the caller only resumes after ink has fully unmounted and flushed its final frame to stdout. This aligns with Ink's rendering approach when concurrent mode is disabled.
Tophatting
shopify app deploy— loading bar disappears after completionpnpm shopify store execute— loading bars disappear after completionshopify app init— no render issuesshopify app generate extension— no render issuesshopify kitchen-sink async— loading bar clears properlyshopify app devorg selection)