Skip to content

Expand Huh forms usage for interactive CLI operations#14357

Merged
pelikhan merged 2 commits intomainfrom
copilot/expand-huh-forms-usage
Feb 7, 2026
Merged

Expand Huh forms usage for interactive CLI operations#14357
pelikhan merged 2 commits intomainfrom
copilot/expand-huh-forms-usage

Conversation

Copy link
Contributor

Copilot AI commented Feb 7, 2026

Huh forms library was underutilized (only confirmation dialogs). Manual bufio.Scanner input left secrets visible on screen, lacked validation, and had inconsistent UX.

Changes

New Console Helpers (pkg/console/)

Input functions:

  • PromptInput() - text input with validation
  • PromptSecretInput() - password-masked input using EchoMode(huh.EchoModePassword)
  • PromptInputWithValidation() - custom validation support

Selection functions:

  • PromptSelect() - single-select menu
  • PromptMultiSelect() - multi-select with configurable limit

Form builder:

  • RunForm() - compose multi-field forms (input/password/confirm/select)
  • Validates configuration before TTY checks for better error messages

Refactored Commands

pkg/cli/secret_set_command.go:

  • Replaced manual bufio.ReadString() loop with console.PromptSecretInput()
  • Adds password masking in interactive TTY mode
  • Falls back to io.ReadAll() for piped/non-TTY input (backward compatible)

Example Usage

// Secret input with masking
secret, err := console.PromptSecretInput(
    "Enter API key",
    "Will be encrypted and stored",
)

// Multi-select menu
workflows, err := console.PromptMultiSelect(
    "Select workflows to compile",
    "Space to select, Enter to confirm",
    options,
    10, // limit visible items
)

// Complex forms
fields := []console.FormField{
    {Type: "input", Title: "Name", Value: &name},
    {Type: "password", Title: "Key", Value: &key},
    {Type: "select", Title: "Engine", Value: &engine, Options: opts},
}
err := console.RunForm(fields)

Technical Details

  • TTY detection via tty.IsStderrTerminal() + os.Stdin.Stat()
  • All forms respect IsAccessibleMode() (ACCESSIBLE, NO_COLOR, TERM=dumb)
  • Modern Huh API: EchoMode(huh.EchoModePassword) not deprecated Password()
  • Zero breaking changes: piped input, flags, and env vars unchanged
Original prompt

This section details on the original issue you should resolve

<issue_title>[Code Quality] Expand Huh forms usage for interactive CLI operations</issue_title>
<issue_description>## Description

The Huh forms library is currently underutilized - only confirmation dialogs are implemented. Expanding Huh usage to other interactive CLI operations (secret input, workflow selection, configuration forms) will improve user experience and reduce input errors.

Current Problem

From the Terminal Stylist Report (Jan 31, 2026):

  • Current Huh usage: Only pkg/console/confirm.go (confirmation dialogs)
  • Missing opportunities: Input fields, multi-select menus, form validation
  • Current approach: Manual fmt.Scanf and text-based prompts
  • Impact: Less polished UX, prone to input validation errors

Current State

// Only confirmation dialogs use Huh
func ConfirmAction(title, affirmative, negative string) (bool, error) {
    var confirmed bool
    confirmForm := huh.NewForm(
        huh.NewGroup(
            huh.NewConfirm().
                Title(title).
                Affirmative(affirmative).
                Negative(negative).
                Value(&confirmed),
        ),
    ).WithAccessible(IsAccessibleMode())
    // ...
}

Suggested Changes

Priority 1: Secret Input (High Impact)

Replace fmt.Scanf in pkg/cli/secret_set_command.go:

// BEFORE
fmt.Print("Enter secret value: ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
secretValue := scanner.Text()

// AFTER
var secretValue string
huh.NewForm(
    huh.NewGroup(
        huh.NewInput().
            Title("Enter secret value").
            Password(true).  // Masks input
            Validate(func(s string) error {
                if len(s) == 0 {
                    return fmt.Errorf("secret value required")
                }
                return nil
            }).
            Value(&secretValue),
    ),
).WithAccessible(IsAccessibleMode()).Run()

Benefits:

  • Password masking (security)
  • Built-in validation
  • Better error messages
  • Accessible mode support

Priority 2: Workflow Multi-Select (Medium Impact)

Add workflow selection for compilation:

var selectedWorkflows []string
options := make([]huh.Option[string], len(workflows))
for i, wf := range workflows {
    options[i] = huh.NewOption(wf.Title, wf.Path)
}

huh.NewForm(
    huh.NewGroup(
        huh.NewMultiSelect[string]().
            Title("Select workflows to compile").
            Description("Use space to select, enter to confirm").
            Options(options...).
            Limit(10).  // Show 10 at a time
            Value(&selectedWorkflows),
    ),
).WithAccessible(IsAccessibleMode()).Run()

Use cases:

  • gh aw compile --interactive (select multiple workflows)
  • gh aw validate --interactive (select workflows to validate)
  • gh aw run --interactive (select workflow to run)

Priority 3: Configuration Forms (Lower Impact)

Interactive workflow scaffolding:

type WorkflowConfig struct {
    Name    string
    Engine  string
    Verbose bool
}

var config WorkflowConfig

huh.NewForm(
    huh.NewGroup(
        huh.NewInput().
            Title("Workflow name").
            Validate(validateWorkflowName).
            Value(&config.Name),
        
        huh.NewSelect[string]().
            Title("Select AI engine").
            Options(
                huh.NewOption("Copilot", "copilot"),
                huh.NewOption("Claude", "claude"),
                huh.NewOption("Codex", "codex"),
            ).
            Value(&config.Engine),
        
        huh.NewConfirm().
            Title("Enable verbose mode?").
            Value(&config.Verbose),
    ),
).WithAccessible(IsAccessibleMode()).Run()

Use cases:

  • gh aw init (create new workflow interactively)
  • gh aw config (configure global settings)

Files Affected

New functionality:

  • pkg/console/input.go (input helpers)
  • pkg/console/select.go (select/multi-select helpers)
  • pkg/console/form.go (form composition helpers)

Refactor existing:

  • pkg/cli/secret_set_command.go (secret input)
  • pkg/cli/compile_command.go (workflow selection)
  • pkg/cli/init_command.go (interactive scaffolding)

Success Criteria

  • Secret input uses Huh with password masking
  • Workflow multi-select available in compile/validate commands
  • Interactive forms available for at least 3 CLI commands
  • All forms respect accessibility mode (ACCESSIBLE, NO_COLOR)
  • TTY fallback works correctly (text-based prompts)
  • No regressions in non-interactive usage
  • User feedback indicates improved UX

Estimated Effort

Medium (1-2 days)

  • Secret input: 2-3 hours
  • Multi-select: 3-4 hours
  • Configuration forms: 4-6 hours
  • Testing and polish: 2-3 hours

Source

Extracted from Terminal Stylist Report: Console Output Analysis (Discussion #12889)

Priority

Medium - Imp...


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Expand Huh forms usage for interactive CLI operations Expand Huh forms usage for interactive CLI operations Feb 7, 2026
Copilot AI requested a review from pelikhan February 7, 2026 14:29
@pelikhan pelikhan marked this pull request as ready for review February 7, 2026 14:32
Copilot AI review requested due to automatic review settings February 7, 2026 14:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands the console layer’s interactive capabilities by introducing reusable Huh-based prompt helpers (input/select/form) and refactoring secrets set to use masked secret entry in interactive terminals while preserving non-interactive stdin behavior.

Changes:

  • Added new console helpers for interactive input, secret input, select, multi-select, and multi-field forms.
  • Added basic unit tests for the new console helper APIs/validation.
  • Updated pkg/cli/secret_set_command.go to use masked interactive secret entry with a non-interactive stdin fallback.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pkg/console/input.go Adds interactive input and secret input helpers using Huh forms.
pkg/console/input_test.go Adds basic tests validating API presence and non-TTY behavior.
pkg/console/select.go Adds interactive single- and multi-select helpers.
pkg/console/select_test.go Adds basic tests for select helper validation and non-TTY behavior.
pkg/console/form.go Adds a higher-level form runner to compose multiple field types with pre-validation.
pkg/console/form_test.go Adds tests for form field validation and non-TTY behavior.
pkg/cli/secret_set_command.go Refactors secret value input to use masked Huh prompt when interactive, otherwise reads from stdin.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +13 to +16
// Check if stdin is a TTY - if not, we can't show interactive forms
if !tty.IsStderrTerminal() {
return "", fmt.Errorf("interactive input not available (not a TTY)")
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

The code comment says this is checking whether stdin is a TTY, but the actual check is tty.IsStderrTerminal() (stderr). This can incorrectly attempt an interactive Huh prompt when stdin is piped but stderr is still a TTY, which can block or fail. Consider adding a stdin TTY check (e.g., term.IsTerminal(int(os.Stdin.Fd())) or os.Stdin.Stat()), and/or updating the tty package with IsStdinTerminal() and using that here (potentially requiring both stdin and stderr to be terminals).

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +27
// Check if stdin is a TTY - if not, we can't show interactive forms
if !tty.IsStderrTerminal() {
return "", fmt.Errorf("interactive selection not available (not a TTY)")
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Same issue as input helpers: the code says it checks whether stdin is a TTY, but it only checks tty.IsStderrTerminal(). If stdin is redirected/piped while stderr remains a TTY, this can still attempt to run an interactive selector and block waiting for terminal input. Recommend checking stdin TTY status (and possibly requiring both stdin+stderr to be terminals) before running Huh forms.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +42
// Check if stdin is a TTY - if not, we can't show interactive forms
if !tty.IsStderrTerminal() {
return fmt.Errorf("interactive forms not available (not a TTY)")
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

RunForm gates interactivity on tty.IsStderrTerminal() only, but Huh forms require interactive stdin for key events. If stdin is not a terminal (e.g., piped input) while stderr is a terminal, RunForm can still try to run interactively and block/fail. Consider checking stdin TTY status (and updating the tty package with an IsStdinTerminal() helper) before running the form.

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +162
// If we're in an interactive terminal, use Huh for a better UX with password masking
if isTerminal && tty.IsStderrTerminal() {
secretSetLog.Print("Using interactive password prompt with Huh")
value, err := console.PromptSecretInput(
"Enter secret value",
"The value will be encrypted and stored in the repository",
)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return "", err
secretSetLog.Printf("Interactive prompt failed: %v", err)
return "", fmt.Errorf("failed to read secret value: %w", err)
}
return value, nil
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Switching interactive input from multi-line stdin read (Ctrl+D) to a single-line Huh password prompt removes the ability to enter multi-line secrets interactively (e.g., PEM keys/certificates). If multi-line secrets are a supported use case, consider preserving the previous Ctrl+D multi-line path (or offering an explicit multiline mode) in interactive terminals, while still supporting masked single-line entry when appropriate.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +176
// Check if stdin is connected to a terminal (interactive mode)
info, err := os.Stdin.Stat()
if err != nil {
return "", err
}

if info.Mode()&os.ModeCharDevice != 0 {
fmt.Fprintln(os.Stderr, "Enter secret value, then press Ctrl+D:")
}

reader := bufio.NewReader(os.Stdin)
var b strings.Builder
isTerminal := (info.Mode() & os.ModeCharDevice) != 0

for {
line, err := reader.ReadString('\n')
b.WriteString(line)
// If we're in an interactive terminal, use Huh for a better UX with password masking
if isTerminal && tty.IsStderrTerminal() {
secretSetLog.Print("Using interactive password prompt with Huh")
value, err := console.PromptSecretInput(
"Enter secret value",
"The value will be encrypted and stored in the repository",
)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return "", err
secretSetLog.Printf("Interactive prompt failed: %v", err)
return "", fmt.Errorf("failed to read secret value: %w", err)
}
return value, nil
}

// Fallback to non-interactive stdin reading (piped input or non-TTY)
secretSetLog.Print("Using non-interactive stdin reading")
if isTerminal {
fmt.Fprintln(os.Stderr, "Enter secret value, then press Ctrl+D:")
}

reader := io.Reader(os.Stdin)
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}

value := strings.TrimRight(b.String(), "\r\n")
value := strings.TrimRight(string(data), "\r\n")
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

resolveSecretValueForSet now has additional branches for interactive prompting and stdin ReadAll fallback, but the existing tests (in secret_set_command_test.go) only cover the env/flag cases. Adding unit tests for the stdin paths (e.g., temporarily swapping os.Stdin with an os.Pipe/temp file and asserting trimming/empty handling) would help prevent regressions in non-interactive and terminal scenarios.

Copilot uses AI. Check for mistakes.
@pelikhan pelikhan merged commit ab4e0ff into main Feb 7, 2026
131 checks passed
@pelikhan pelikhan deleted the copilot/expand-huh-forms-usage branch February 7, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Code Quality] Expand Huh forms usage for interactive CLI operations

2 participants