Skip to content

feat: agent and scripting friendly non-TTY prompt errors#587

Open
mwbrooks wants to merge 4 commits into
mainfrom
mwbrooks-agent-friendly-prompts
Open

feat: agent and scripting friendly non-TTY prompt errors#587
mwbrooks wants to merge 4 commits into
mainfrom
mwbrooks-agent-friendly-prompts

Conversation

@mwbrooks

@mwbrooks mwbrooks commented Jun 11, 2026

Copy link
Copy Markdown
Member

Changelog

In a non-TTY environment (AI agents, devops scripts, etc), when the CLI errors because a prompt must be shown, the error message now displays the prompt and matching flags to select an option.

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Select a team for authentication
       mbrooks-dev T0139LMAF8T          --team=T0139LMAF8T
       mbrooks-prod T017U9RG1C4         --team=T017U9RG1C4
       mbrooks-sandbox-org E094JKVJ8KT  --team=E094JKVJ8KT

Suggestion
   Re-run with one of the `--team` values shown above

Added support for the App Prompt and Team Prompt. All other prompts display the question, but not the options. The options will be added as upcoming changes.

Summary

This pull request adds a non-TTY error renderer that re-creates the interactive prompt and annotates each option with the --flag=value invocation that would pick it.

The motivation is non-human callers - AI agents and devops scripts. Today, when one of these callers triggers a required prompt without a TTY, the CLI returns a generic "input device is not a TTY" error and a flag name. The caller has no way to know what the prompt was about to ask, what options it would have shown, or which concrete value to pass. The new error puts all of that in the body, so the caller can read it and re-run with the right flag.

This PR lands the infrastructure plus two reference call-site migrations:

  • New PromptOption struct (Label, Flag *pflag.Flag, Value string) and PromptOptionsConfig interface.
  • New Options []PromptOption field on SelectPromptConfig and MultiSelectPromptConfig.
  • errInteractivityFlags now renders the prompt question, an option list and a short single-line Suggestion.
  • Two reference migrations: internal/prompts/team_select.go and internal/prompts/app_select.go. Every other prompt site continues to work, with a graceful degraded render (see below).

Follow-up PRs

See Coverage Strategy section below for a suggested strategy to migrate the remaining prompts. This pull request is focused on the infrastructure changes and migrating App Select and Team Select as examples.

Preview

Video

2026-06-11-agent-friendly-prompts.mov

Migrated team_select and app_select (e.g. slack sandbox list)

Before:

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity

Suggestion
   Try running the command with the `--team` flag included
   Learn more about this flag with `--help`

After:

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Select a team for authentication
       mbrooks-dev T0139LMAF8T          --team=T0139LMAF8T
       mbrooks-prod T017U9RG1C4         --team=T017U9RG1C4
       mbrooks-sandbox-org E094JKVJ8KT  --team=E094JKVJ8KT

Suggestion
   Re-run with one of the `--team` values shown above

Non-migrated prompt sites (e.g. slack auth logout)

auth logout is one of the 36 SelectPrompt sites this PR does not migrate. It still renders the prompt question and the visible options, but without per-option flag mappings. The original "Try --team" suggestion is preserved.

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Select an authorization to revoke
       mbrooks-dev T0139LMAF8T (workspace)
       mbrooks-prod T017U9RG1C4 (workspace)
       mbrooks-sandbox-org E094JKVJ8KT (organization)

Suggestion
   Try running the command with the `--team` flag included
   Learn more about this flag with `--help`

Why surface the question + options for non-migrated sites instead of skipping them?

  • The question has meaningful information that the old error did not have. "Select an authorization to revoke" tells a caller what the CLI was about to ask; today's bare TTY error doesn't.
  • Showing the options makes the prompt parseable from logs even before the per-option flag mapping is wired up.
  • It signals to maintainers what's left to migrate. A prompt that visibly has options but no flag values next to them is a clear cue.

InputPrompt with no options (e.g. slack login)

Free-text prompts have no enumerable options, so the visualization is just the question. Suggestion remains the existing per-config text.

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Enter challenge code

Suggestion
   Learn more about this command with `--help`

The CLI supports slack login --challenge <code>, so savvy AI agents could start a login, ask the human to paste the slash command and provide the LLM with the 2FA code.

Testing

All commands below should be run with stdout piped (e.g. | cat) so IsTTY() returns false.

Migrated SelectPrompt:

$ ./bin/slack sandbox list 2>&1 | cat

Expect prompt rendering with --team flags:

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Select a team for authentication
       mbrooks-dev T0139LMAF8T          --team=T0139LMAF8T
       mbrooks-prod T017U9RG1C4         --team=T017U9RG1C4
       mbrooks-sandbox-org E094JKVJ8KT  --team=E094JKVJ8KT

Suggestion
   Re-run with one of the `--team` values shown above

Migrated SelectPrompt for apps:

# Change into a project
$ cd <some slack project>

# Install at least 1 app
$ /path/to/bin/slack install

# List the apps
$ /path/to/bin/slack app list 2>&1 | cat

Expect prompt rendering with --app flags:

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Select an app
       A0B9Z075C30 mbrooks-sandbox-org E094JKVJ8KT  --app=A0B9Z075C30

Suggestion
   Re-run with one of the `--app` values shown above

Non-migrated SelectPrompt - degraded path still works:

$ ./bin/slack auth logout 2>&1 | cat

Expect prompt rendering and suggestion to use --team flag but no flag values:

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Select an authorization to revoke
       mbrooks-dev T0139LMAF8T (workspace)
       mbrooks-prod T017U9RG1C4 (workspace)
       mbrooks-sandbox-org E094JKVJ8KT (organization)

Suggestion
   Try running the command with the `--team` flag included
   Learn more about this flag with `--help`

InputPrompt - no flag, just the question:

$ ./bin/slack login 2>&1 | cat

Expect › Enter challenge code:

Run the following slash command in any Slack channel or DM
   This will open a modal with user permissions for you to approve
   Once approved, a challenge code will be generated in Slack

/slackauthticket XYZ....

An error occurred while executing prompts (prompt_error)
   The input device is not a TTY or does not support interactivity
   The prompt that would have been shown is below:

   › Enter challenge code

Suggestion
   Learn more about this command with `--help`

TTY path is unchanged - interactive prompt should still appear:

$ ./bin/slack sandbox list   # no pipe

Notes

Coverage Strategy

The CLI has 38 SelectPrompt call sites. This PR migrates 2 (the high-traffic team and app selects) and ships infrastructure that the remaining 36 can adopt incrementally. Rough breakdown of the 36:

  • ~25 clean additions. The site already declares a single Flag (e.g. --datastore, --trigger-id, --template, --provider, --name).
  • ~6 multi-flag clean additions.
  • ~5 require a small refactor. No flag is wired today (e.g. auth logout's --team exists but isn't on the prompt config) or the option set is conceptual rather than flag-mapped (e.g. triggers access's permission levels). Need a small flag wiring decision before migration.

ConfirmPrompt, InputPrompt, PasswordPrompt, and MultiSelectPrompt sites are not migrated in this PR.

What a future migration looks like

Migrating cmd/triggers/triggers.go:388 (the simplest single-flag shape):

Before:

var selectedTriggerID string
selection, err := clients.IO.SelectPrompt(ctx, "Choose a trigger:", triggerLabels, iostreams.SelectPromptConfig{
    Flag:     clients.Config.Flags.Lookup("trigger-id"),
    Required: true,
    PageSize: 4,
})

After:

triggerIDFlag := clients.Config.Flags.Lookup("trigger-id")

triggerOptions := make([]iostreams.PromptOption, len(triggers))
for i, tr := range triggers {
    triggerOptions[i] = iostreams.PromptOption{
        Label: triggerLabels[i],
        Flag:  triggerIDFlag,
        Value: tr.ID,
    }
}

var selectedTriggerID string
selection, err := clients.IO.SelectPrompt(ctx, "Choose a trigger:", triggerLabels, iostreams.SelectPromptConfig{
    Flag:     triggerIDFlag,
    Required: true,
    PageSize: 4,
    Options:  triggerOptions,
})

Design choices worth flagging

  • PromptOption.Flag is *pflag.Flag, not a string. Using the registered flag pointer means the flag name appears once per call site (in Lookup) instead of being duplicated in every option literal - a flag rename is then a one-line change.
  • The visualization lives in the error body, not the Suggestion. Suggestion is reserved for short, actionable directives ("Re-run with one of the --team values shown above"). The prompt mirror is context, not advice.

Requirements

mwbrooks added 3 commits June 11, 2026 09:29
Re-render the prompt question and annotate each option with the
equivalent --flag=value invocation when a required prompt is reached
without a TTY. Lets agents and devops scripts read the error and
re-run with the right flag.

Adds an optional Options []PromptOption field on SelectPromptConfig
and MultiSelectPromptConfig; configs that don't set it keep the
prior "Try --flag" remediation. Migrates the team_select and
app_select reference sites; other prompt sites are unchanged.
Move the prompt-mirror (question + per-option `--flag=value`) out of
the Remediation block and into the error body via Details, so the
Suggestion header stops framing the visualization as advice. The
Suggestion now carries a short, single-line directive.

UX tweaks: replace the leading `?` with `›`, prepend a context line
("The prompt that would have been shown is below:"), and align the
`--flag=value` column using lipgloss.Width so labels with embedded
ANSI escapes line up.

Non-migrated prompts still render the question (and any options),
just without per-option flag values, preserving the existing
`Try --flag` Suggestion.
Drop the hand-typed flag-name string in favor of the registered
flag pointer. Call sites now mention the flag name once (via
Flags.Lookup) instead of duplicating it across the prompt config
and each PromptOption, so a flag rename is a one-line change.
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.72289% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.71%. Comparing base (6dc5f98) to head (e1e48ef).

Files with missing lines Patch % Lines
internal/prompts/team_select.go 0.00% 11 Missing ⚠️
internal/iostreams/prompts.go 91.93% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #587      +/-   ##
==========================================
+ Coverage   71.66%   71.71%   +0.04%     
==========================================
  Files         226      226              
  Lines       19176    19234      +58     
==========================================
+ Hits        13743    13794      +51     
- Misses       4222     4229       +7     
  Partials     1211     1211              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mwbrooks mwbrooks changed the title feat: agent and devops friendly non-TTY prompt errors feat: agent and scripting friendly non-TTY prompt errors Jun 11, 2026
@mwbrooks mwbrooks self-assigned this Jun 11, 2026
@mwbrooks mwbrooks added enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment labels Jun 11, 2026
@mwbrooks mwbrooks added this to the Next Release milestone Jun 11, 2026
@mwbrooks mwbrooks marked this pull request as ready for review June 11, 2026 22:12
@mwbrooks mwbrooks requested a review from a team as a code owner June 11, 2026 22:12

@mwbrooks mwbrooks left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comments for the wise minds 🧠 ✨

Comment on lines +36 to +52
// PromptOption pairs an interactive option label with the flag invocation
// that picks the same option non-interactively. When a prompt is reached in
// a non-TTY context, the resulting error renders one of these per option so
// agents and scripts can re-run with the right --flag=value.
type PromptOption struct {
Label string // The option as rendered in the interactive list
Flag *pflag.Flag // The flag substitute for this option
Value string // The value to pass, e.g. "T0123" or "A0ABCD"
}

// PromptOptionsConfig is optionally implemented by prompt configs that can
// enumerate options as flag invocations. Configs that do not implement it
// (or return an empty slice) keep the simpler "Try --flag" remediation.
type PromptOptionsConfig interface {
GetPromptOptions() []PromptOption
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: New structs to allow us to pass flag names and values that correspond to each option. For now, I've kept it simple with just 1 flag. If we discover prompts that require multiple flags (e.g. --template <url> --subdir <path> then we can refactor later. For now, I'm leaning toward setting up the essentials to keep it readable.

Comment on lines +197 to +202
// errInteractivityFlags formats an error for when flag substitutes are needed.
// It re-renders the prompt question and any enumerable options (with their
// equivalent --flag=value invocations) as part of the error body so agents
// and devops scripts can read the error and re-run with the right flags.
// The Suggestion remains a short, single-line directive.
func errInteractivityFlags(cfg PromptConfig, message string, options []string) error {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: This is the meat and potatoes of the PR 🥔 🍠 It handles rendering the error message, which now support displaying the original prompt that should have been shown.

Comment on lines +604 to +616
appFlag := clients.Config.Flags.Lookup("app")
labels := []string{}
for _, label := range options {
labels = append(labels, label.label)
appOptions := []iostreams.PromptOption{}
for _, opt := range options {
labels = append(labels, opt.label)
// Synthetic entries ("Create a new app", "No app") have no AppID and
// are emitted as label-only so the option list stays 1:1 with labels.
promptOpt := iostreams.PromptOption{Label: opt.label}
if opt.app.App.AppID != "" {
promptOpt.Flag = appFlag
promptOpt.Value = opt.app.App.AppID
}
appOptions = append(appOptions, promptOpt)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: This is the migration of the App Select prompt. We have 36 total call sites, so this gives you an idea of what a straight-forward migration will look like. I've scoped this PR to just migrate 2 call sites to allow us to focus on the design and infrastructure changes.

Comment on lines +53 to +63
teamFlag := clients.Config.Flags.Lookup("team")
var teamLabels []string
var teamOptions []iostreams.PromptOption
for _, auth := range allAuths {
teamLabels = append(
teamLabels,
style.TeamSelectLabel(auth.TeamDomain, auth.TeamID),
)
label := style.TeamSelectLabel(auth.TeamDomain, auth.TeamID)
teamLabels = append(teamLabels, label)
teamOptions = append(teamOptions, iostreams.PromptOption{
Label: label,
Flag: teamFlag,
Value: auth.TeamID,
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: This is the migration of the Team Select prompt. We have 36 total call sites, so this gives you an idea of what a straight-forward migration will look like. I've scoped this PR to just migrate 2 call sites to allow us to focus on the design and infrastructure changes.

@zimeg zimeg added the changelog Use on updates to be included in the release notes label Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog Use on updates to be included in the release notes enhancement M-T: A feature request for new functionality semver:minor Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants