@@ -10,6 +10,7 @@ import (
1010 "io"
1111 "log/slog"
1212 "os"
13+ "strings"
1314 "sync"
1415 "time"
1516
@@ -30,13 +31,13 @@ import (
3031 "github.com/charmbracelet/crush/internal/pubsub"
3132 "github.com/charmbracelet/crush/internal/session"
3233 "github.com/charmbracelet/crush/internal/shell"
33− "github.com/charmbracelet/crush/internal/term"
3434 "github.com/charmbracelet/crush/internal/tui/components/anim"
3535 "github.com/charmbracelet/crush/internal/tui/styles"
3636 "github.com/charmbracelet/crush/internal/update"
3737 "github.com/charmbracelet/crush/internal/version"
3838 "github.com/charmbracelet/x/ansi"
3939 "github.com/charmbracelet/x/exp/charmtone"
40+ "github.com/charmbracelet/x/term"
4041)
4142
4243type App struct {
@@ -61,7 +62,7 @@ type App struct {
6162 cleanupFuncs []func() error
6263}
6364
64−// New initializes a new applcation instance.
65+// New initializes a new application instance.
6566func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
6667 q := db.New(conn)
6768 sessions := session.NewService(q)
@@ -129,15 +130,27 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
129130 ctx, cancel := context.WithCancel(ctx)
130131 defer cancel()
131132
132− var spinner *format.Spinner
133− if !quiet {
133+ var (
134+ spinner *format.Spinner
135+ stdoutTTY bool
136+ stderrTTY bool
137+ stdinTTY bool
138+ )
139+
140+ if f, ok := output.(*os.File); ok {
141+ stdoutTTY = term.IsTerminal(f.Fd())
142+ }
143+ stderrTTY = term.IsTerminal(os.Stderr.Fd())
144+ stdinTTY = term.IsTerminal(os.Stdin.Fd())
145+
146+ if !quiet && stderrTTY {
134147 t := styles.CurrentTheme()
135148
136149 // Detect background color to set the appropriate color for the
137150 // spinner's 'Generating...' text. Without this, that text would be
138151 // unreadable in light terminals.
139152 hasDarkBG := true
140− if f, ok := output.(*os.File); ok {
153+ if f, ok := output.(*os.File); ok && stdinTTY && stdoutTTY {
141154 hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, f)
142155 }
143156 defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.FgBase)
@@ -203,10 +216,9 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
203216
204217 messageEvents := app.Messages.Subscribe(ctx)
205218 messageReadBytes := make(map[string]int)
206− supportsProgressBar := term.SupportsProgressBar()
207219
208220 defer func() {
209− if supportsProgressBar {
221+ if stderrTTY {
210222 _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar)
211223 }
212224
@@ -216,9 +228,9 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
216228 }()
217229
218230 for {
219− if supportsProgressBar {
220− // HACK: Reinitialize the terminal progress bar on every iteration so
221− // it doesn't get hidden by the terminal due to inactivity.
231+ if stderrTTY {
232+ // HACK: Reinitialize the terminal progress bar on every iteration
233+ // so it doesn't get hidden by the terminal due to inactivity.
222234 _, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
223235 }
224236
@@ -248,6 +260,11 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt
248260 }
249261
250262 part := content[readBytes:]
263+ // Trim leading whitespace. Sometimes the LLM includes leading
264+ // formatting and intentation, which we don't want here.
265+ if readBytes == 0 {
266+ part = strings.TrimLeft(part, " \t")
267+ }
251268 fmt.Fprint(output, part)
252269 messageReadBytes[msg.ID] = len(content)
253270 }