From 11d0cc9f25201132ffd1580189f0b5792db61b0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:41:33 +0000 Subject: [PATCH 1/2] Initial plan From e6c105fe853f2619c5bf30e06629c07510fc820b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:12:14 +0000 Subject: [PATCH 2/2] feat: implement custom Huh theme using pkg/styles color palette Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/add_interactive_auth.go | 3 +- pkg/cli/add_interactive_engine.go | 3 +- pkg/cli/add_interactive_git.go | 5 +- pkg/cli/add_interactive_orchestrator.go | 3 +- pkg/cli/add_interactive_schedule.go | 3 +- pkg/cli/add_interactive_workflow.go | 3 +- pkg/cli/engine_secrets.go | 7 ++- pkg/cli/interactive.go | 7 ++- pkg/cli/run_interactive.go | 7 ++- pkg/console/confirm.go | 3 +- pkg/console/input.go | 3 +- pkg/styles/huh_theme.go | 74 +++++++++++++++++++++++++ 12 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 pkg/styles/huh_theme.go diff --git a/pkg/cli/add_interactive_auth.go b/pkg/cli/add_interactive_auth.go index 4d1aea4ac99..f00c4bb8bea 100644 --- a/pkg/cli/add_interactive_auth.go +++ b/pkg/cli/add_interactive_auth.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/styles" ) // checkGHAuthStatus verifies the user is logged in to GitHub CLI @@ -55,7 +56,7 @@ func (c *AddInteractiveConfig) checkGitRepository() error { return nil }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get repository info: %w", err) diff --git a/pkg/cli/add_interactive_engine.go b/pkg/cli/add_interactive_engine.go index fcd2e82ea33..7dbe6349aa9 100644 --- a/pkg/cli/add_interactive_engine.go +++ b/pkg/cli/add_interactive_engine.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -123,7 +124,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error { Options(engineOptions...). Value(&selectedEngine), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to select coding agent: %w", err) diff --git a/pkg/cli/add_interactive_git.go b/pkg/cli/add_interactive_git.go index 9cb366cb7c2..fcbf99c998a 100644 --- a/pkg/cli/add_interactive_git.go +++ b/pkg/cli/add_interactive_git.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -96,7 +97,7 @@ func (c *AddInteractiveConfig) createWorkflowPRAndConfigureSecret(ctx context.Co Options(options...). Value(&chosen), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if selectErr := selectForm.Run(); selectErr != nil { return fmt.Errorf("failed to get user input: %w", selectErr) @@ -129,7 +130,7 @@ func (c *AddInteractiveConfig) createWorkflowPRAndConfigureSecret(ctx context.Co Description("Add a prefix if required, for example: feat: or fix:"). Value(&newTitle), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if titleErr := titleForm.Run(); titleErr != nil { return fmt.Errorf("failed to get user input: %w", titleErr) } diff --git a/pkg/cli/add_interactive_orchestrator.go b/pkg/cli/add_interactive_orchestrator.go index a38b6f631f8..2e00285b1e5 100644 --- a/pkg/cli/add_interactive_orchestrator.go +++ b/pkg/cli/add_interactive_orchestrator.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/styles" ) var addInteractiveLog = logger.New("cli:add_interactive") @@ -228,7 +229,7 @@ func (c *AddInteractiveConfig) confirmChanges(workflowFiles, initFiles []string, Negative("No, cancel"). Value(&confirmed), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("confirmation failed: %w", err) diff --git a/pkg/cli/add_interactive_schedule.go b/pkg/cli/add_interactive_schedule.go index 9edb9fde84a..07cdafc4327 100644 --- a/pkg/cli/add_interactive_schedule.go +++ b/pkg/cli/add_interactive_schedule.go @@ -10,6 +10,7 @@ import ( "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" "github.com/github/gh-aw/pkg/sliceutil" + "github.com/github/gh-aw/pkg/styles" ) var scheduleWizardLog = logger.New("cli:add_interactive_schedule") @@ -227,7 +228,7 @@ func (c *AddInteractiveConfig) selectScheduleFrequency() error { Options(options...). Value(&selected), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to select schedule frequency: %w", err) diff --git a/pkg/cli/add_interactive_workflow.go b/pkg/cli/add_interactive_workflow.go index de2bfc732fc..a2322601b49 100644 --- a/pkg/cli/add_interactive_workflow.go +++ b/pkg/cli/add_interactive_workflow.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/huh" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -109,7 +110,7 @@ func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error Negative("No, I'll run later"). Value(&runNow), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return nil // Not critical, just skip diff --git a/pkg/cli/engine_secrets.go b/pkg/cli/engine_secrets.go index 0230f08d19d..f2989b331b2 100644 --- a/pkg/cli/engine_secrets.go +++ b/pkg/cli/engine_secrets.go @@ -13,6 +13,7 @@ import ( "github.com/github/gh-aw/pkg/repoutil" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -307,7 +308,7 @@ func promptForCopilotPATUnified(req SecretRequirement, config EngineSecretConfig return stringutil.ValidateCopilotPAT(s) }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get Copilot token: %w", err) @@ -355,7 +356,7 @@ func promptForSystemTokenUnified(req SecretRequirement, config EngineSecretConfi return nil }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get %s token: %w", req.Name, err) @@ -408,7 +409,7 @@ func promptForGenericAPIKeyUnified(req SecretRequirement, config EngineSecretCon return nil }), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return fmt.Errorf("failed to get %s API key: %w", label, err) diff --git a/pkg/cli/interactive.go b/pkg/cli/interactive.go index a0f2f00b2cc..a2403101b70 100644 --- a/pkg/cli/interactive.go +++ b/pkg/cli/interactive.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/workflow" ) @@ -98,7 +99,7 @@ func (b *InteractiveWorkflowBuilder) promptForWorkflowName() error { Value(&b.WorkflowName). Validate(ValidateWorkflowName), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) return form.Run() } @@ -222,7 +223,7 @@ func (b *InteractiveWorkflowBuilder) promptForConfiguration() error { ). Title("Instructions"). Description("Describe what you want this workflow to accomplish"), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return err @@ -267,7 +268,7 @@ func (b *InteractiveWorkflowBuilder) generateWorkflow(force bool) error { Negative("No, cancel"). Value(&overwrite), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := confirmForm.Run(); err != nil { return fmt.Errorf("confirmation failed: %w", err) diff --git a/pkg/cli/run_interactive.go b/pkg/cli/run_interactive.go index 132b1ef4f91..59193cb87f8 100644 --- a/pkg/cli/run_interactive.go +++ b/pkg/cli/run_interactive.go @@ -13,6 +13,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/sliceutil" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/tty" "github.com/github/gh-aw/pkg/workflow" ) @@ -189,7 +190,7 @@ func selectWorkflow(workflows []WorkflowOption) (*WorkflowOption, error) { Height(15). Value(&selected), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return nil, fmt.Errorf("workflow selection cancelled or failed: %w", err) @@ -313,7 +314,7 @@ func collectInputsWithMap(inputs map[string]*workflow.InputDefinition) ([]string } // Show the form - form := huh.NewForm(formGroups...).WithAccessible(console.IsAccessibleMode()) + form := huh.NewForm(formGroups...).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { return nil, fmt.Errorf("input collection cancelled: %w", err) } @@ -350,7 +351,7 @@ func confirmExecution(wf *WorkflowOption, inputs []string) bool { Negative("No, cancel"). Value(&confirm), ), - ).WithAccessible(console.IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(console.IsAccessibleMode()) if err := form.Run(); err != nil { runInteractiveLog.Printf("Confirmation failed: %v", err) diff --git a/pkg/console/confirm.go b/pkg/console/confirm.go index 98737b59a83..6edfbd2dcee 100644 --- a/pkg/console/confirm.go +++ b/pkg/console/confirm.go @@ -4,6 +4,7 @@ package console import ( "github.com/charmbracelet/huh" + "github.com/github/gh-aw/pkg/styles" ) // ConfirmAction shows an interactive confirmation dialog using Bubble Tea (huh) @@ -19,7 +20,7 @@ func ConfirmAction(title, affirmative, negative string) (bool, error) { Negative(negative). Value(&confirmed), ), - ).WithAccessible(IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(IsAccessibleMode()) if err := confirmForm.Run(); err != nil { return false, err diff --git a/pkg/console/input.go b/pkg/console/input.go index 9697c524f33..f203b8586e1 100644 --- a/pkg/console/input.go +++ b/pkg/console/input.go @@ -6,6 +6,7 @@ import ( "errors" "github.com/charmbracelet/huh" + "github.com/github/gh-aw/pkg/styles" "github.com/github/gh-aw/pkg/tty" ) @@ -34,7 +35,7 @@ func PromptSecretInput(title, description string) (string, error) { }). Value(&value), ), - ).WithAccessible(IsAccessibleMode()) + ).WithTheme(styles.HuhTheme()).WithAccessible(IsAccessibleMode()) if err := form.Run(); err != nil { return "", err diff --git a/pkg/styles/huh_theme.go b/pkg/styles/huh_theme.go new file mode 100644 index 00000000000..c3110368c5d --- /dev/null +++ b/pkg/styles/huh_theme.go @@ -0,0 +1,74 @@ +//go:build !js && !wasm + +package styles + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +// HuhTheme returns a custom huh.Theme that maps the pkg/styles Dracula-inspired +// color palette to huh form fields, giving interactive forms the same visual +// identity as the rest of the CLI output. +func HuhTheme() *huh.Theme { + t := huh.ThemeBase() + + // Map the pkg/styles palette to lipgloss.AdaptiveColor for huh compatibility. + // huh uses github.com/charmbracelet/lipgloss, so we use that type here. + var ( + primary = lipgloss.AdaptiveColor{Light: hexColorPurpleLight, Dark: hexColorPurpleDark} + success = lipgloss.AdaptiveColor{Light: hexColorSuccessLight, Dark: hexColorSuccessDark} + errorColor = lipgloss.AdaptiveColor{Light: hexColorErrorLight, Dark: hexColorErrorDark} + warning = lipgloss.AdaptiveColor{Light: hexColorWarningLight, Dark: hexColorWarningDark} + comment = lipgloss.AdaptiveColor{Light: hexColorCommentLight, Dark: hexColorCommentDark} + fg = lipgloss.AdaptiveColor{Light: hexColorForegroundLight, Dark: hexColorForegroundDark} + bg = lipgloss.AdaptiveColor{Light: hexColorBackgroundLight, Dark: hexColorBackgroundDark} + border = lipgloss.AdaptiveColor{Light: hexColorBorderLight, Dark: hexColorBorderDark} + ) + + // Focused field styles + t.Focused.Base = t.Focused.Base.BorderForeground(border) + t.Focused.Card = t.Focused.Base + t.Focused.Title = t.Focused.Title.Foreground(primary).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(primary).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(primary) + t.Focused.Description = t.Focused.Description.Foreground(comment) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor) + + // Select / navigation indicators + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(warning) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(warning) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(warning) + + // List option styles + t.Focused.Option = t.Focused.Option.Foreground(fg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(warning) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(success) + t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(success) + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(fg) + t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(comment) + + // Button styles + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(bg).Background(primary).Bold(true) + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(fg).Background(bg) + t.Focused.Next = t.Focused.FocusedButton + + // Text input styles + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(warning) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(comment) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primary) + + // Blurred styles mirror focused but hide the border + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.Card = t.Blurred.Base + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + // Group header styles + t.Group.Title = t.Focused.Title + t.Group.Description = t.Focused.Description + + return t +}