Render task progress bars to stderr by default#7295
Conversation
craigmichaelmartin
left a comment
There was a problem hiding this comment.
Suggestion, but thanks for knocking this out!
There was a problem hiding this comment.
I think it'd be cleaner like this, rather than having an early return.
| {(!interactive || !noProgressBar) && <TextAnimation text={loadingBar} maxWidth={twoThirds} />} |
There was a problem hiding this comment.
Pull request overview
This PR reduces CLI output noise in non-interactive environments by preventing LoadingBar’s animated TextAnimation from rendering when the terminal isn’t interactive, outputting only the static title line instead.
Changes:
- Add an interactivity check to
LoadingBarand skip rendering the animated progress bar when non-interactive. - Add unit tests to validate the non-interactive rendering behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx | Adds non-interactive rendering path that omits TextAnimation to avoid flooding output. |
| packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx | Mocks isTerminalInteractive and adds snapshot tests for the non-interactive output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const interactive = isTerminalInteractive() | ||
|
|
||
| // In non-TTY environments (CI, piped output, AI agents), skip the animated | ||
| // progress bar entirely to avoid flooding the output with animation frames. | ||
| if (!interactive) { | ||
| return ( | ||
| <Box flexDirection="column"> | ||
| <Text>{title} ...</Text> | ||
| </Box> | ||
| ) | ||
| } |
There was a problem hiding this comment.
isTerminalInteractive() checks process.stdout.isTTY, but Ink may be rendering to a different stream via renderOptions.stdout (e.g. renderTasksToStdErr passes process.stderr). In those cases stdout can be non-TTY while the Ink output stream is still interactive, so this change will incorrectly disable the progress bar (regression for renderTasks(..., {renderOptions: {stdout: process.stderr}})). Consider basing the check on Ink’s actual stdout from useStdout() (or plumbing an interactive flag down from the render call) rather than process.stdout.
packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx
Outdated
Show resolved
Hide resolved
| if (!interactive) { | ||
| return ( | ||
| <Box flexDirection="column"> | ||
| <Text>{title} ...</Text> | ||
| </Box> | ||
| ) | ||
| } |
There was a problem hiding this comment.
can we animate safely here at least a simple spinner or dot animation? Wouldelp when piping a command :thinking_face:
There was a problem hiding this comment.
Can we try with just a less intrusive animation? There are "interactive" no-TTY cases (like piping output to jq) where the animation is still useful for letting a user know that something is happening
or maybe we should consider just going to a simpler, no-color animation for everyone?
2129f47 to
b4273e8
Compare
In non-TTY environments (CI, piped output, AI coding agents), the TextAnimation component produces a new frame every ~35ms. Since Ink can't overwrite previous lines without a TTY, every frame gets appended as new output, flooding logs with thousands of animation lines. Use isTerminalInteractive() to detect non-TTY environments and render only the static title text (e.g. 'Loading ...') without the animated progress bar. Interactive TTY behavior is completely unchanged. This affects both Tasks and SingleTask components since they both render through LoadingBar.
b4273e8 to
e2ae2ca
Compare
|
@nickwesselman I'm trying to make it that it checks whether the output stream actually used by Ink (in this case, should be stderr) is TTY or not. If you pipe to jq, that only should pipe stdout. I don't think this PR works yet, so I'm currently debugging, I think the wrong approach was chosen. |
7681be7 to
c85ebba
Compare
c85ebba to
add3b9c
Compare
Progress bars are status feedback, not program output — they belong on stderr per Unix convention. This change makes renderTasks and renderSingleTask default to rendering on stderr. This fixes output flooding in non-TTY environments (AI coding agents, CI) where Ink's ANSI escape codes can't overwrite previous frames, causing each animation frame to append as a new line. The LoadingBar component also now detects whether Ink's output stream is a TTY (via useStdout). When it's not — e.g. AI agents that merge stderr via 2>&1 — it shows only the static task title instead of the animated progress bar. This handles all cases: - Interactive terminal: animated progress bar on stderr - Piped stdout (| cat): stderr still a TTY, animation works - AI agent (stdout capture): stderr animation, stdout clean - AI agent (2>&1): stderr non-TTY, static title only - CI: Ink's built-in isCi already suppresses dynamic rendering
add3b9c to
f5e48c5
Compare
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/public/node/ui.d.ts@@ -328,7 +328,6 @@ interface RenderTasksOptions {
/**
* Runs async tasks and displays their progress to the console.
* @example
- * ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
* Installing dependencies ...
*/
export declare function renderTasks<TContext>(tasks: Task<TContext>[], { renderOptions, noProgressBar }?: RenderTasksOptions): Promise<TContext>;
@@ -346,7 +345,6 @@ export interface RenderSingleTaskOptions<T> {
* @param options.renderOptions - Optional render configuration
* @returns The result of the task
* @example
- * ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
* Loading app ...
*/
export declare function renderSingleTask<T>({ title, task, onAbort, renderOptions, }: RenderSingleTaskOptions<T>): Promise<T>;
|
|
OK, I think I have this in order:
Hence: # displays animation
shopify app generate extension --path ~/dev/experiments/lean-supply-app/ --template checkout_ui --name checkout_ui_final_test
# displays animation
shopify app generate extension --path ~/dev/experiments/lean-supply-app/ --template checkout_ui --name checkout_ui_final_test | cat
# doesn't display animation because stderr redirected to stdout which is piped - this is the common AI use case
shopify app generate extension --path ~/dev/experiments/lean-supply-app/ --template checkout_ui --name checkout_ui_final_test 2>&1 | cat |
Problem
The animated loading bar renders to stdout, which causes output flooding in non-TTY environments (AI coding agents, CI) where Ink's ANSI escape codes can't overwrite previous frames. Each animation frame (~every 35ms) gets appended as a new line, producing hundreds of lines of noise for commands like
app generate extension.Solution
Progress bars are status feedback, not program output — they belong on stderr per Unix convention. This change makes
renderTasksandrenderSingleTaskdefault to rendering on stderr.The
LoadingBarcomponent also now detects whether Ink's output stream is a TTY (viauseStdout). When it's not — e.g. AI agents that merge stderr via2>&1— it shows only the static task title instead of the animated progress bar.Only 3 files changed — no per-package wrappers needed. Every command using
renderTasksorrenderSingleTaskgets the fix automatically.This handles all cases:
| cat: stderr is still a TTY → animated bar visible; stdout is clean ✅2>&1): stderr is non-TTY → static title only, no flooding ✅isCialready suppresses dynamic rendering ✅Before (AI agent)
After (AI agent)
Note on the theme package
The theme package already has its own
renderTasksToStdErrhelper that passes a custom stderr stream. That helper can be simplified in a follow-up sincerenderTasksnow defaults to stderr, but the theme callers sometimes passcontext.stderr(a custom writable), so they're left as-is for now.Type of change