From e2ae2caacae6b1d07528eaf79cf08f2286640a9a Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Tue, 14 Apr 2026 18:50:02 +0300 Subject: [PATCH 1/3] Hide animated loading bar in non-TTY environments 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. --- .../node/ui/components/LoadingBar.test.tsx | 193 +++++++++--------- .../private/node/ui/components/LoadingBar.tsx | 25 ++- 2 files changed, 116 insertions(+), 102 deletions(-) diff --git a/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx b/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx index e4ca3078906..ca9592725ad 100644 --- a/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx @@ -1,4 +1,5 @@ import {LoadingBar} from './LoadingBar.js' +import {Stdout} from '../../ui.js' import {render} from '../../testing/ui.js' import {shouldDisplayColors, unstyled} from '../../../../public/node/output.js' import useLayout from '../hooks/use-layout.js' @@ -25,15 +26,41 @@ beforeEach(() => { vi.mocked(shouldDisplayColors).mockReturnValue(true) }) +/** + * Creates a Stdout test double simulating a TTY stream (the default for + * interactive terminals). On real Node streams, `isTTY` is only defined + * as an own property when the stream IS a TTY — it's absent otherwise. + */ +function createTTYStdout(columns = 100) { + const stdout = new Stdout({columns}) as Stdout & {isTTY: boolean} + stdout.isTTY = true + return stdout +} + +/** + * Creates a Stdout test double simulating a non-TTY environment + * (piped output, CI without a pseudo-TTY, AI coding agents). + */ +function createNonTTYStdout(columns = 100) { + const stdout = new Stdout({columns}) as Stdout & {isTTY: boolean} + stdout.isTTY = false + return stdout +} + +/** + * Renders LoadingBar with a TTY stdout and returns the last frame. + * Most tests need a TTY to verify the animated progress bar renders. + */ +function renderWithTTY(element: React.ReactElement) { + const stdout = createTTYStdout() + const instance = render(element, {stdout}) + return {lastFrame: stdout.lastFrame, unmount: instance.unmount, stdout} +} + describe('LoadingBar', () => { test('renders loading bar with default colored characters', async () => { - // Given - const title = 'Loading content' - - // When - const {lastFrame} = render() + const {lastFrame} = renderWithTTY() - // Then expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Loading content ..." @@ -41,13 +68,8 @@ describe('LoadingBar', () => { }) test('renders loading bar with hill pattern when noColor prop is true', async () => { - // Given - const title = 'Processing files' + const {lastFrame} = renderWithTTY() - // When - const {lastFrame} = render() - - // Then expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅ Processing files ..." @@ -55,14 +77,10 @@ describe('LoadingBar', () => { }) test('renders loading bar with hill pattern when shouldDisplayColors returns false', async () => { - // Given vi.mocked(shouldDisplayColors).mockReturnValue(false) - const title = 'Downloading packages' - // When - const {lastFrame} = render() + const {lastFrame} = renderWithTTY() - // Then expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅ Downloading packages ..." @@ -70,18 +88,10 @@ describe('LoadingBar', () => { }) test('handles narrow terminal width correctly', async () => { - // Given - vi.mocked(useLayout).mockReturnValue({ - twoThirds: 20, - oneThird: 10, - fullWidth: 30, - }) - const title = 'Building app' - - // When - const {lastFrame} = render() - - // Then + vi.mocked(useLayout).mockReturnValue({twoThirds: 20, oneThird: 10, fullWidth: 30}) + + const {lastFrame} = renderWithTTY() + expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Building app ..." @@ -89,18 +99,10 @@ describe('LoadingBar', () => { }) test('handles narrow terminal width correctly in no-color mode', async () => { - // Given - vi.mocked(useLayout).mockReturnValue({ - twoThirds: 15, - oneThird: 8, - fullWidth: 23, - }) - const title = 'Installing' - - // When - const {lastFrame} = render() - - // Then + vi.mocked(useLayout).mockReturnValue({twoThirds: 15, oneThird: 8, fullWidth: 23}) + + const {lastFrame} = renderWithTTY() + expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇ Installing ..." @@ -108,18 +110,10 @@ describe('LoadingBar', () => { }) test('handles very narrow terminal width in no-color mode', async () => { - // Given - vi.mocked(useLayout).mockReturnValue({ - twoThirds: 5, - oneThird: 3, - fullWidth: 8, - }) - const title = 'Wait' - - // When - const {lastFrame} = render() - - // Then + vi.mocked(useLayout).mockReturnValue({twoThirds: 5, oneThird: 3, fullWidth: 8}) + + const {lastFrame} = renderWithTTY() + expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▁▁▁▂▂ Wait ..." @@ -127,18 +121,10 @@ describe('LoadingBar', () => { }) test('handles wide terminal width correctly', async () => { - // Given - vi.mocked(useLayout).mockReturnValue({ - twoThirds: 100, - oneThird: 50, - fullWidth: 150, - }) - const title = 'Synchronizing data' - - // When - const {lastFrame} = render() - - // Then + vi.mocked(useLayout).mockReturnValue({twoThirds: 100, oneThird: 50, fullWidth: 150}) + + const {lastFrame} = renderWithTTY() + expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Synchronizing data ..." @@ -146,18 +132,10 @@ describe('LoadingBar', () => { }) test('handles wide terminal width correctly in no-color mode with pattern repetition', async () => { - // Given - vi.mocked(useLayout).mockReturnValue({ - twoThirds: 90, - oneThird: 45, - fullWidth: 135, - }) - const title = 'Analyzing dependencies' - - // When - const {lastFrame} = render() - - // Then + vi.mocked(useLayout).mockReturnValue({twoThirds: 90, oneThird: 45, fullWidth: 135}) + + const {lastFrame} = renderWithTTY() + expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁ Analyzing dependencies ..." @@ -165,13 +143,8 @@ describe('LoadingBar', () => { }) test('renders correctly with empty title', async () => { - // Given - const title = '' - - // When - const {lastFrame} = render() + const {lastFrame} = renderWithTTY() - // Then expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ..." @@ -179,14 +152,10 @@ describe('LoadingBar', () => { }) test('noColor prop overrides shouldDisplayColors when both would show colors', async () => { - // Given vi.mocked(shouldDisplayColors).mockReturnValue(true) - const title = 'Testing override' - // When - const {lastFrame} = render() + const {lastFrame} = renderWithTTY() - // Then expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅ Testing override ..." @@ -194,27 +163,51 @@ describe('LoadingBar', () => { }) test('renders consistently with same props', async () => { - // Given - const title = 'Consistent test' - const props = {title, noColor: false} + const props = {title: 'Consistent test', noColor: false} - // When - const {lastFrame: frame1} = render() - const {lastFrame: frame2} = render() + const {lastFrame: frame1} = renderWithTTY() + const {lastFrame: frame2} = renderWithTTY() - // Then expect(frame1()).toBe(frame2()) }) test('hides progress bar when noProgressBar is true', async () => { - // Given vi.mocked(shouldDisplayColors).mockReturnValue(true) - const title = 'task 1' - // When - const {lastFrame} = render() + const {lastFrame} = renderWithTTY() - // Then expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(`"task 1 ..."`) }) + + test('renders only title text without animated progress bar in non-TTY environments', async () => { + const stdout = createNonTTYStdout() + + const renderInstance = render(, {stdout}) + + expect(unstyled(stdout.lastFrame()!)).toMatchInlineSnapshot(`"Installing dependencies ..."`) + renderInstance.unmount() + }) + + test('renders only title text in non-TTY even when noColor and noProgressBar are not set', async () => { + const stdout = createNonTTYStdout() + vi.mocked(shouldDisplayColors).mockReturnValue(true) + + const renderInstance = render(, {stdout}) + + expect(unstyled(stdout.lastFrame()!)).toMatchInlineSnapshot(`"Generating extension ..."`) + renderInstance.unmount() + }) + + test('keeps animated progress bar when Ink renders to a TTY stream (e.g. renderTasksToStdErr)', async () => { + // renderTasksToStdErr passes process.stderr as Ink's stdout option. + // useStdout() returns that stream, so the TTY check uses the correct stream. + const ttyStream = createTTYStdout() + + const renderInstance = render(, {stdout: ttyStream}) + + const frame = unstyled(ttyStream.lastFrame()!) + expect(frame).toContain('▀') + expect(frame).toContain('Uploading theme ...') + renderInstance.unmount() + }) }) diff --git a/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx b/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx index 42278012be4..9b8749fe1dd 100644 --- a/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx +++ b/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx @@ -3,7 +3,7 @@ import useLayout from '../hooks/use-layout.js' import {shouldDisplayColors} from '../../../../public/node/output.js' import React from 'react' -import {Box, Text} from 'ink' +import {Box, Text, useStdout} from 'ink' const loadingBarChar = '▀' const hillString = '▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁' @@ -14,8 +14,29 @@ interface LoadingBarProps { noProgressBar?: boolean } +/** + * Checks whether the stream Ink is rendering to supports cursor movement. + * When it doesn't (piped stdout, dumb terminal, non-TTY CI runner, AI coding + * agents), every animation frame would be appended as a new line instead of + * overwriting the previous one. + * + * We inspect the stdout object Ink is actually using (via `useStdout`) so the + * check stays accurate even when a custom stream is provided through + * `renderOptions` (e.g. `renderTasksToStdErr` passes `process.stderr`). + * + * On real Node streams, `isTTY` is only defined as an own property when the + * stream IS a TTY — it's completely absent otherwise, not set to `false`. + * So we check the value directly: truthy means TTY, falsy/missing means not. + */ +function useOutputSupportsCursor(stdout: NodeJS.WriteStream | Record): boolean { + return Boolean((stdout as Record).isTTY) +} + const LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren) => { const {twoThirds} = useLayout() + const {stdout} = useStdout() + const supportsCursor = useOutputSupportsCursor(stdout) + let loadingBar = new Array(twoThirds).fill(loadingBarChar).join('') if (noColor ?? !shouldDisplayColors()) { loadingBar = hillString.repeat(Math.ceil(twoThirds / hillString.length)) @@ -23,7 +44,7 @@ const LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren - {!noProgressBar && } + {supportsCursor && !noProgressBar && } {title} ... ) From f5e48c58bb30d3ab5c86156f66f875a4e53bc396 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Tue, 14 Apr 2026 21:59:47 +0300 Subject: [PATCH 2/3] Render task progress bars to stderr by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/render-tasks-to-stderr.md | 5 ++ .../node/ui/components/LoadingBar.test.tsx | 62 ++++--------------- .../private/node/ui/components/LoadingBar.tsx | 28 +++------ packages/cli-kit/src/public/node/ui.tsx | 2 + 4 files changed, 27 insertions(+), 70 deletions(-) create mode 100644 .changeset/render-tasks-to-stderr.md diff --git a/.changeset/render-tasks-to-stderr.md b/.changeset/render-tasks-to-stderr.md new file mode 100644 index 00000000000..a4b28530898 --- /dev/null +++ b/.changeset/render-tasks-to-stderr.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Render task progress bars to stderr to reduce output noise in non-TTY environments diff --git a/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx b/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx index ca9592725ad..b318b02c02a 100644 --- a/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx @@ -17,7 +17,6 @@ vi.mock('../../../../public/node/output.js', async () => { }) beforeEach(() => { - // Default terminal width vi.mocked(useLayout).mockReturnValue({ twoThirds: 53, oneThird: 27, @@ -27,9 +26,9 @@ beforeEach(() => { }) /** - * Creates a Stdout test double simulating a TTY stream (the default for - * interactive terminals). On real Node streams, `isTTY` is only defined - * as an own property when the stream IS a TTY — it's absent otherwise. + * Creates a Stdout test double simulating a TTY stream. + * On real Node streams, isTTY is only present as an own property when the + * stream IS a TTY. */ function createTTYStdout(columns = 100) { const stdout = new Stdout({columns}) as Stdout & {isTTY: boolean} @@ -38,23 +37,12 @@ function createTTYStdout(columns = 100) { } /** - * Creates a Stdout test double simulating a non-TTY environment - * (piped output, CI without a pseudo-TTY, AI coding agents). - */ -function createNonTTYStdout(columns = 100) { - const stdout = new Stdout({columns}) as Stdout & {isTTY: boolean} - stdout.isTTY = false - return stdout -} - -/** - * Renders LoadingBar with a TTY stdout and returns the last frame. - * Most tests need a TTY to verify the animated progress bar renders. + * Renders LoadingBar with a TTY stdout so the animated progress bar renders. */ function renderWithTTY(element: React.ReactElement) { const stdout = createTTYStdout() const instance = render(element, {stdout}) - return {lastFrame: stdout.lastFrame, unmount: instance.unmount, stdout} + return {lastFrame: stdout.lastFrame, unmount: instance.unmount} } describe('LoadingBar', () => { @@ -78,7 +66,6 @@ describe('LoadingBar', () => { test('renders loading bar with hill pattern when shouldDisplayColors returns false', async () => { vi.mocked(shouldDisplayColors).mockReturnValue(false) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -89,7 +76,6 @@ describe('LoadingBar', () => { test('handles narrow terminal width correctly', async () => { vi.mocked(useLayout).mockReturnValue({twoThirds: 20, oneThird: 10, fullWidth: 30}) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -100,7 +86,6 @@ describe('LoadingBar', () => { test('handles narrow terminal width correctly in no-color mode', async () => { vi.mocked(useLayout).mockReturnValue({twoThirds: 15, oneThird: 8, fullWidth: 23}) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -111,7 +96,6 @@ describe('LoadingBar', () => { test('handles very narrow terminal width in no-color mode', async () => { vi.mocked(useLayout).mockReturnValue({twoThirds: 5, oneThird: 3, fullWidth: 8}) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -122,7 +106,6 @@ describe('LoadingBar', () => { test('handles wide terminal width correctly', async () => { vi.mocked(useLayout).mockReturnValue({twoThirds: 100, oneThird: 50, fullWidth: 150}) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -133,7 +116,6 @@ describe('LoadingBar', () => { test('handles wide terminal width correctly in no-color mode with pattern repetition', async () => { vi.mocked(useLayout).mockReturnValue({twoThirds: 90, oneThird: 45, fullWidth: 135}) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -153,7 +135,6 @@ describe('LoadingBar', () => { test('noColor prop overrides shouldDisplayColors when both would show colors', async () => { vi.mocked(shouldDisplayColors).mockReturnValue(true) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(` @@ -164,7 +145,6 @@ describe('LoadingBar', () => { test('renders consistently with same props', async () => { const props = {title: 'Consistent test', noColor: false} - const {lastFrame: frame1} = renderWithTTY() const {lastFrame: frame2} = renderWithTTY() @@ -173,41 +153,23 @@ describe('LoadingBar', () => { test('hides progress bar when noProgressBar is true', async () => { vi.mocked(shouldDisplayColors).mockReturnValue(true) - const {lastFrame} = renderWithTTY() expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(`"task 1 ..."`) }) - test('renders only title text without animated progress bar in non-TTY environments', async () => { - const stdout = createNonTTYStdout() - - const renderInstance = render(, {stdout}) + test('shows only static title text when output stream is not a TTY', async () => { + // Default test Stdout has no isTTY property, simulating a non-TTY stream + const {lastFrame} = render() - expect(unstyled(stdout.lastFrame()!)).toMatchInlineSnapshot(`"Installing dependencies ..."`) - renderInstance.unmount() + expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(`"Installing dependencies ..."`) }) - test('renders only title text in non-TTY even when noColor and noProgressBar are not set', async () => { - const stdout = createNonTTYStdout() - vi.mocked(shouldDisplayColors).mockReturnValue(true) - - const renderInstance = render(, {stdout}) - - expect(unstyled(stdout.lastFrame()!)).toMatchInlineSnapshot(`"Generating extension ..."`) - renderInstance.unmount() - }) - - test('keeps animated progress bar when Ink renders to a TTY stream (e.g. renderTasksToStdErr)', async () => { - // renderTasksToStdErr passes process.stderr as Ink's stdout option. - // useStdout() returns that stream, so the TTY check uses the correct stream. - const ttyStream = createTTYStdout() - - const renderInstance = render(, {stdout: ttyStream}) + test('shows animated progress bar when output stream is a TTY', async () => { + const {lastFrame} = renderWithTTY() - const frame = unstyled(ttyStream.lastFrame()!) + const frame = unstyled(lastFrame()!) expect(frame).toContain('▀') expect(frame).toContain('Uploading theme ...') - renderInstance.unmount() }) }) diff --git a/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx b/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx index 9b8749fe1dd..0091cb6da44 100644 --- a/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx +++ b/packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx @@ -14,28 +14,16 @@ interface LoadingBarProps { noProgressBar?: boolean } -/** - * Checks whether the stream Ink is rendering to supports cursor movement. - * When it doesn't (piped stdout, dumb terminal, non-TTY CI runner, AI coding - * agents), every animation frame would be appended as a new line instead of - * overwriting the previous one. - * - * We inspect the stdout object Ink is actually using (via `useStdout`) so the - * check stays accurate even when a custom stream is provided through - * `renderOptions` (e.g. `renderTasksToStdErr` passes `process.stderr`). - * - * On real Node streams, `isTTY` is only defined as an own property when the - * stream IS a TTY — it's completely absent otherwise, not set to `false`. - * So we check the value directly: truthy means TTY, falsy/missing means not. - */ -function useOutputSupportsCursor(stdout: NodeJS.WriteStream | Record): boolean { - return Boolean((stdout as Record).isTTY) -} - const LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren) => { const {twoThirds} = useLayout() const {stdout} = useStdout() - const supportsCursor = useOutputSupportsCursor(stdout) + + // On real Node streams, isTTY is only present as an own property when the + // stream IS a TTY. When Ink's output stream is not a TTY (e.g. AI agents + // capturing stderr via 2>&1), the animated progress bar can't overwrite + // previous frames and would flood the output. Show only the static title + // in that case. + const isTTY = Boolean((stdout as unknown as Record).isTTY) let loadingBar = new Array(twoThirds).fill(loadingBarChar).join('') if (noColor ?? !shouldDisplayColors()) { @@ -44,7 +32,7 @@ const LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren - {supportsCursor && !noProgressBar && } + {isTTY && !noProgressBar && } {title} ... ) diff --git a/packages/cli-kit/src/public/node/ui.tsx b/packages/cli-kit/src/public/node/ui.tsx index a6fdb7797c0..27cbc8a8063 100644 --- a/packages/cli-kit/src/public/node/ui.tsx +++ b/packages/cli-kit/src/public/node/ui.tsx @@ -497,6 +497,7 @@ export async function renderTasks( noProgressBar={noProgressBar} />, { + stdout: process.stderr as unknown as NodeJS.WriteStream, ...renderOptions, exitOnCtrlC: false, }, @@ -539,6 +540,7 @@ export async function renderSingleTask({ onAbort={onAbort} />, { + stdout: process.stderr as unknown as NodeJS.WriteStream, ...renderOptions, exitOnCtrlC: false, }, From 9270a004e96f699953bfbf92cbf7c5f93606a20c Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 15 Apr 2026 00:04:50 +0300 Subject: [PATCH 3/3] Refresh code documentation --- packages/cli-kit/src/public/node/ui.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli-kit/src/public/node/ui.tsx b/packages/cli-kit/src/public/node/ui.tsx index 27cbc8a8063..35c6bab6a7a 100644 --- a/packages/cli-kit/src/public/node/ui.tsx +++ b/packages/cli-kit/src/public/node/ui.tsx @@ -479,7 +479,6 @@ interface RenderTasksOptions { /** * Runs async tasks and displays their progress to the console. * @example - * ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ * Installing dependencies ... */ @@ -520,7 +519,6 @@ export interface RenderSingleTaskOptions { * @param options.renderOptions - Optional render configuration * @returns The result of the task * @example - * ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ * Loading app ... */ export async function renderSingleTask({