Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
Build: cbuild.NewModel(client, ctx, *build, cmd.String("branch"), downloadPaths),
WaitMode: waitMode,
})
model, err = tea.NewProgram(model).Run()
model, err = console.NewProgram(model).Run()
if err != nil {
console.Warn("%s", err.Error())
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"strings"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/stainless-api/stainless-api-cli/pkg/components/build"
"github.com/stainless-api/stainless-api-cli/pkg/components/dev"
Expand Down Expand Up @@ -255,7 +254,7 @@ func runDevBuild(ctx context.Context, client stainless.Client, wc workspace.Conf
cmd.Bool("watch"),
)

p := tea.NewProgram(model)
p := console.NewProgram(model)
finalModel, err := p.Run()

if err != nil {
Expand Down
3 changes: 1 addition & 2 deletions pkg/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/stainless-api/stainless-api-cli/pkg/workspace"
"github.com/stainless-api/stainless-api-go/option"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/term"
Expand Down Expand Up @@ -426,7 +425,7 @@ func initializeWorkspace(ctx context.Context, cmd *cli.Command, client stainless
Build: cbuild.NewModel(client, ctx, *build, "main", downloadPaths),
}

_, err = tea.NewProgram(model).Run()
_, err = console.NewProgram(model).Run()
if err != nil {
console.Warn("%s", err.Error())
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/stainless-api/stainless-api-cli/pkg/components/build"
"github.com/stainless-api/stainless-api-cli/pkg/console"
"github.com/stainless-api/stainless-api-cli/pkg/workspace"
"github.com/stainless-api/stainless-api-go"
"github.com/urfave/cli/v3"
Expand Down Expand Up @@ -211,7 +212,7 @@ func runLinter(ctx context.Context, cmd *cli.Command, canSkip bool) error {
help: help.New(),
}

p := tea.NewProgram(m, tea.WithContext(ctx))
p := console.NewProgram(m, tea.WithContext(ctx))

// Start the diagnostics process
go func() {
Expand Down
19 changes: 18 additions & 1 deletion pkg/console/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,25 @@ import (
"github.com/charmbracelet/x/ansi"
"github.com/logrusorgru/aurora/v4"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)

// NewProgram wraps tea.NewProgram with better handling for tty environments
func NewProgram(model tea.Model, opts ...tea.ProgramOption) *tea.Program {
// Always output to stderr, in case we want to also output JSON so that the json is redirectable e.g. to jq.
opts = append(opts, tea.WithOutput(os.Stderr))

// If not a TTY, use stdin and disable renderer
if !term.IsTerminal(int(os.Stderr.Fd())) {
opts = append(opts,
tea.WithInput(os.Stdin),
tea.WithoutRenderer(),
)
}
Comment on lines +20 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is a bit suspicious to me. I think it would be helpful to expand on the comments here and/or rework the logic a bit.

Scenario 1: using the tool with no pipes -> output to stderr?
Scenario 2: using the tool and piping stdout somewhere -> output charm stuff to stderr?
Scenario 3: using the tool and piping stderr somewhere -> read from stdin, output charm stuff to stderr, and no charm rendering?

I would expect the logic to look more like:

if !isatty(stdout) { // stdout is being piped somewhere
  if isatty(stderr) { // stderr is still a tty, we can use that for TUI stuff
    // output = stderr
  } else { // no tty on either stdout/stderr, this would be the agent case
    // disable output, force stdin as input instead of tty input
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand, I think as the comment is saying I want the output to always be to stderr regardless of stdin piping status?

Copy link
Contributor

@bruce-hill bruce-hill Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think your original logic checks out to me after looking at it harder for a bit.

One thing I want to check: How does this logic interact with the CLI tool's ability to pipe data in to stdin? You should be able to test pretty easily by doing something like stl cmd 2>&1 | cat and also cat /dev/stdin | stl cmd 2>&1 | cat. I'm slightly concerned that if you simulate being an LLM using the second command, it would read in the bubbletea prompts from stdin, then make the API request and try to read in a JSON object from stdin at that point, which would and either hang without a prompt or error.

Copy link
Member Author

@yjp20 yjp20 Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I need to merge this to support a customer before OOO, but very much intend to address this comment.

Copy link
Contributor

@bruce-hill bruce-hill Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, all I'd ask is that you test cat | stl cmd 2>&1 | cat (simulate being an LLM by forcing stdin/stdout/stderr to be non-TTY) for some stl command that both uses tea and makes an API request, just to confirm that it doesn't hang or error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave this a quick test and had to also type {}<ctrl-D> and it seems to work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I'm worried about. Will that work with an LLM, or will that cause the LLM to not know how to proceed? It might be best to close stdin after gathering all the necessary user input and before making a request. Or somehow otherwise let the CLI know to not read request data from stdin in this scenario.


return tea.NewProgram(model, opts...)
}

// Group represents a nested logging group
type Group struct {
prefix string
Expand Down Expand Up @@ -255,7 +272,7 @@ func spinnerWithIndent(indent int, message string, operation func() error) error
execute: operation,
}

finalModel, err := tea.NewProgram(model).Run()
finalModel, err := NewProgram(model).Run()
if err != nil {
return err
}
Expand Down
Loading