From 962ccad15d2399d8c89e63e6f975c292f2afc20d Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 23 Feb 2026 23:33:46 -0800 Subject: [PATCH 1/6] revert: login specific prompt logic --- cmd/auth/login.go | 16 ++++------------ internal/pkg/auth/login.go | 28 +++++----------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 65dd779b..9159e256 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -18,8 +18,6 @@ import ( "context" "fmt" - "github.com/slackapi/slack-cli/internal/config" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/iostreams" authpkg "github.com/slackapi/slack-cli/internal/pkg/auth" "github.com/slackapi/slack-cli/internal/shared" @@ -111,7 +109,7 @@ func RunLoginCommand(clients *shared.ClientFactory, cmd *cobra.Command) (types.S return types.SlackAuth{}, err } if selectedAuth.Token != "" { - printAuthSuccess(cmd, clients.Config, clients.IO, credentialsPath, selectedAuth.Token) + printAuthSuccess(cmd, clients.IO, credentialsPath, selectedAuth.Token) printAuthNextSteps(ctx, clients) } return selectedAuth, err @@ -121,14 +119,14 @@ func RunLoginCommand(clients *shared.ClientFactory, cmd *cobra.Command) (types.S if err != nil { return types.SlackAuth{}, err } else { - printAuthSuccess(cmd, clients.Config, clients.IO, credentialsPath, selectedAuth.Token) + printAuthSuccess(cmd, clients.IO, credentialsPath, selectedAuth.Token) printAuthNextSteps(ctx, clients) } return selectedAuth, nil } -func printAuthSuccess(cmd *cobra.Command, config *config.Config, IO iostreams.IOStreamer, credentialsPath string, token string) { +func printAuthSuccess(cmd *cobra.Command, IO iostreams.IOStreamer, credentialsPath string, token string) { ctx := cmd.Context() var secondaryLog string @@ -138,13 +136,7 @@ func printAuthSuccess(cmd *cobra.Command, config *config.Config, IO iostreams.IO secondaryLog = fmt.Sprintf("Service token:\n\n %s\n\nMake sure to copy the token now and save it safely.", token) } - // The legacy prompt leaves no blank line before the success message, so - // print one here. The Charm-based prompt already handles spacing. - if !config.WithExperimentOn(experiment.Charm) { - IO.PrintInfo(ctx, false, "") - } - - IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ Emoji: "key", Text: "You've successfully authenticated!", Secondary: []string{secondaryLog}, diff --git a/internal/pkg/auth/login.go b/internal/pkg/auth/login.go index ef78ff3a..f5cfa5d6 100644 --- a/internal/pkg/auth/login.go +++ b/internal/pkg/auth/login.go @@ -20,12 +20,10 @@ import ( "strings" "time" - "github.com/charmbracelet/huh" "github.com/opentracing/opentracing-go" "github.com/slackapi/slack-cli/internal/api" "github.com/slackapi/slack-cli/internal/auth" "github.com/slackapi/slack-cli/internal/config" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/pkg/version" "github.com/slackapi/slack-cli/internal/shared" @@ -142,27 +140,11 @@ func createNewAuth(ctx context.Context, apiClient api.APIInterface, authClient a return types.SlackAuth{}, "", err } - challengeCode := "" - if !config.WithExperimentOn(experiment.Charm) { - challengeCode, err = io.InputPrompt(ctx, "Enter challenge code", iostreams.InputPromptConfig{ - Required: true, - }) - if err != nil { - return types.SlackAuth{}, "", err - } - } else { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Enter challenge code"). - Validate(huh.ValidateMinLength(1)). - Value(&challengeCode), - ), - ) - err := form.Run() - if err != nil { - return types.SlackAuth{}, "", err - } + challengeCode, err := io.InputPrompt(ctx, "Enter challenge code", iostreams.InputPromptConfig{ + Required: true, + }) + if err != nil { + return types.SlackAuth{}, "", err } authExchangeRes, err := apiClient.ExchangeAuthTicket(ctx, authTicket, challengeCode, version.Get()) From 952c73117d99c5a9e05552e8651e3e509c8a4439 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 23 Feb 2026 23:51:18 -0800 Subject: [PATCH 2/6] feat(experiment): add charm prompts to iostreams --- internal/iostreams/charm.go | 123 +++++++++++++++++++++++++++++++++++ internal/iostreams/survey.go | 20 ++++++ 2 files changed, 143 insertions(+) create mode 100644 internal/iostreams/charm.go diff --git a/internal/iostreams/charm.go b/internal/iostreams/charm.go new file mode 100644 index 00000000..d01b255b --- /dev/null +++ b/internal/iostreams/charm.go @@ -0,0 +1,123 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iostreams + +// Charm-based prompt implementations using the huh library +// These are used when the "charm" experiment is enabled + +import ( + "context" + "slices" + + "github.com/charmbracelet/huh" +) + +// charmInputPrompt prompts for text input using a charm huh form +func charmInputPrompt(_ *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) { + var input string + field := huh.NewInput(). + Title(message). + Value(&input) + if cfg.Required { + field.Validate(huh.ValidateMinLength(1)) + } + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return "", err + } + return input, nil +} + +// charmConfirmPrompt prompts for a yes/no confirmation using a charm huh form +func charmConfirmPrompt(_ *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) { + var choice = defaultValue + field := huh.NewConfirm(). + Title(message). + Value(&choice) + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return false, err + } + return choice, nil +} + +// charmSelectPrompt prompts the user to select one option using a charm huh form +func charmSelectPrompt(_ *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) { + var selected string + var opts []huh.Option[string] + for _, opt := range options { + key := opt + if cfg.Description != nil { + if desc := cfg.Description(opt, len(opts)); desc != "" { + key = opt + "\n " + desc + } + } + opts = append(opts, huh.NewOption(key, opt)) + } + + field := huh.NewSelect[string](). + Title(msg). + Options(opts...). + Value(&selected) + + if cfg.PageSize > 0 { + field.Height(cfg.PageSize + 2) + } + + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return SelectPromptResponse{}, err + } + + index := slices.Index(options, selected) + return SelectPromptResponse{Prompt: true, Index: index, Option: selected}, nil +} + +// charmPasswordPrompt prompts for a password (hidden input) using a charm huh form +func charmPasswordPrompt(_ *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) { + var input string + field := huh.NewInput(). + Title(message). + EchoMode(huh.EchoModePassword). + Value(&input) + if cfg.Required { + field.Validate(huh.ValidateMinLength(1)) + } + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return PasswordPromptResponse{}, err + } + return PasswordPromptResponse{Prompt: true, Value: input}, nil +} + +// charmMultiSelectPrompt prompts the user to select multiple options using a charm huh form +func charmMultiSelectPrompt(_ *IOStreams, _ context.Context, message string, options []string) ([]string, error) { + var selected []string + var opts []huh.Option[string] + for _, opt := range options { + opts = append(opts, huh.NewOption(opt, opt)) + } + + field := huh.NewMultiSelect[string](). + Title(message). + Options(opts...). + Value(&selected) + + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return []string{}, err + } + return selected, nil +} diff --git a/internal/iostreams/survey.go b/internal/iostreams/survey.go index 1f26f79e..8e2d95a1 100644 --- a/internal/iostreams/survey.go +++ b/internal/iostreams/survey.go @@ -27,6 +27,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/style" "github.com/spf13/pflag" @@ -120,6 +121,9 @@ func (cfg ConfirmPromptConfig) IsRequired() bool { // ConfirmPrompt prompts the user for a "yes" or "no" (true or false) value for // the message func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultValue bool) (bool, error) { + if io.config.WithExperimentOn(experiment.Charm) { + return charmConfirmPrompt(io, ctx, message, defaultValue) + } // Temporarily swap default template for custom one defaultConfirmTemplate := survey.ConfirmQuestionTemplate @@ -191,6 +195,10 @@ func (cfg InputPromptConfig) IsRequired() bool { // InputPrompt prompts the user for a string value for the message, which can // optionally be made required func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputPromptConfig) (string, error) { + if io.config.WithExperimentOn(experiment.Charm) { + return charmInputPrompt(io, ctx, message, cfg) + } + defaultInputTemplate := survey.InputQuestionTemplate survey.InputQuestionTemplate = InputQuestionTemplate defer func() { @@ -263,6 +271,10 @@ func (cfg MultiSelectPromptConfig) IsRequired() bool { // MultiSelectPrompt prompts the user to select multiple values in a list and // returns the selected values func (io *IOStreams) MultiSelectPrompt(ctx context.Context, message string, options []string) ([]string, error) { + if io.config.WithExperimentOn(experiment.Charm) { + return charmMultiSelectPrompt(io, ctx, message, options) + } + defaultMultiSelectTemplate := survey.MultiSelectQuestionTemplate survey.MultiSelectQuestionTemplate = MultiSelectQuestionTemplate defer func() { @@ -340,6 +352,10 @@ func (io *IOStreams) PasswordPrompt(ctx context.Context, message string, cfg Pas return PasswordPromptResponse{}, errInteractivityFlags(cfg) } + if io.config.WithExperimentOn(experiment.Charm) { + return charmPasswordPrompt(io, ctx, message, cfg) + } + defaultPasswordTemplate := survey.PasswordQuestionTemplate if cfg.Template != "" { survey.PasswordQuestionTemplate = cfg.Template @@ -454,6 +470,10 @@ func (io *IOStreams) SelectPrompt(ctx context.Context, msg string, options []str } } + if io.config.WithExperimentOn(experiment.Charm) { + return charmSelectPrompt(io, ctx, msg, options, cfg) + } + defaultSelectTemplate := survey.SelectQuestionTemplate if cfg.Template != "" { survey.SelectQuestionTemplate = cfg.Template From 822b714d70815e1d74f4d8587fa90ae80cb6152c Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 24 Feb 2026 13:22:42 -0500 Subject: [PATCH 3/6] feat(charm): add Slack brand theme for huh prompts --- internal/iostreams/charm_theme.go | 120 ++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 internal/iostreams/charm_theme.go diff --git a/internal/iostreams/charm_theme.go b/internal/iostreams/charm_theme.go new file mode 100644 index 00000000..0f736c25 --- /dev/null +++ b/internal/iostreams/charm_theme.go @@ -0,0 +1,120 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iostreams + +// Slack brand theme for charmbracelet/huh prompts. +// Uses official Slack brand colors to give the CLI a fun, playful feel. + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +// Slack brand colors +var ( + slackAubergine = lipgloss.Color("#4a154b") + slackBrightAuberg = lipgloss.Color("#611f69") + slackBlue = lipgloss.Color("#36c5f0") + slackGreen = lipgloss.Color("#2eb67d") + slackYellow = lipgloss.Color("#ecb22e") + slackRed = lipgloss.Color("#e01e5a") + slackPool = lipgloss.Color("#78d7dd") + slackLegalGray = lipgloss.Color("#5e5d60") + slackOptionText = lipgloss.AdaptiveColor{Light: "#1d1c1d", Dark: "#f4ede4"} + slackDescriptionText = lipgloss.AdaptiveColor{Light: "#454447", Dark: "#b9b5b0"} + slackPlaceholderText = lipgloss.AdaptiveColor{Light: "#5e5d60", Dark: "#868380"} +) + +// ThemeSlack returns a huh theme styled with Slack brand colors. +func ThemeSlack() *huh.Theme { + t := huh.ThemeBase() + + // Focused field styles + t.Focused.Base = t.Focused.Base. + BorderForeground(slackBrightAuberg) + t.Focused.Title = lipgloss.NewStyle(). + Foreground(slackAubergine). + Bold(true) + t.Focused.Description = lipgloss.NewStyle(). + Foreground(slackDescriptionText) + t.Focused.ErrorIndicator = lipgloss.NewStyle(). + Foreground(slackRed). + SetString(" *") + t.Focused.ErrorMessage = lipgloss.NewStyle(). + Foreground(slackRed) + + // Select styles + t.Focused.SelectSelector = lipgloss.NewStyle(). + Foreground(slackBlue). + SetString("> ") + t.Focused.Option = lipgloss.NewStyle(). + Foreground(slackOptionText) + t.Focused.NextIndicator = lipgloss.NewStyle(). + Foreground(slackPool). + MarginLeft(1). + SetString("↓") + t.Focused.PrevIndicator = lipgloss.NewStyle(). + Foreground(slackPool). + MarginRight(1). + SetString("↑") + + // Multi-select styles + t.Focused.MultiSelectSelector = lipgloss.NewStyle(). + Foreground(slackYellow). + SetString("> ") + t.Focused.SelectedOption = lipgloss.NewStyle(). + Foreground(slackGreen) + t.Focused.SelectedPrefix = lipgloss.NewStyle(). + Foreground(slackGreen). + SetString("[✓] ") + t.Focused.UnselectedOption = lipgloss.NewStyle(). + Foreground(slackOptionText) + t.Focused.UnselectedPrefix = lipgloss.NewStyle(). + Foreground(slackLegalGray). + SetString("[ ] ") + + // Text input styles + t.Focused.TextInput.Cursor = lipgloss.NewStyle(). + Foreground(slackYellow) + t.Focused.TextInput.Prompt = lipgloss.NewStyle(). + Foreground(slackBlue) + t.Focused.TextInput.Placeholder = lipgloss.NewStyle(). + Foreground(slackPlaceholderText) + t.Focused.TextInput.Text = lipgloss.NewStyle(). + Foreground(slackOptionText) + + // Button styles + button := lipgloss.NewStyle(). + Padding(0, 2). + MarginRight(1) + t.Focused.FocusedButton = button. + Foreground(lipgloss.Color("#fff")). + Background(slackAubergine). + Bold(true) + t.Focused.BlurredButton = button. + Foreground(slackLegalGray). + Background(lipgloss.Color("#000")) + + // Blurred field styles — subdued version of focused + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base. + BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.SelectSelector = lipgloss.NewStyle().SetString(" ") + t.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(" ") + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + return t +} From 335baf2e9af13a362a2ab55bbce2bf4e09eb12e9 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 26 Feb 2026 10:52:07 -0500 Subject: [PATCH 4/6] adds slack theme to cli prompts --- internal/iostreams/charm.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/iostreams/charm.go b/internal/iostreams/charm.go index d01b255b..4e9e1dfb 100644 --- a/internal/iostreams/charm.go +++ b/internal/iostreams/charm.go @@ -33,7 +33,7 @@ func charmInputPrompt(_ *IOStreams, _ context.Context, message string, cfg Input if cfg.Required { field.Validate(huh.ValidateMinLength(1)) } - err := huh.NewForm(huh.NewGroup(field)).Run() + err := huh.NewForm(huh.NewGroup(field)).WithTheme(ThemeSlack()).Run() if err != nil { return "", err } @@ -46,7 +46,7 @@ func charmConfirmPrompt(_ *IOStreams, _ context.Context, message string, default field := huh.NewConfirm(). Title(message). Value(&choice) - err := huh.NewForm(huh.NewGroup(field)).Run() + err := huh.NewForm(huh.NewGroup(field)).WithTheme(ThemeSlack()).Run() if err != nil { return false, err } @@ -76,7 +76,7 @@ func charmSelectPrompt(_ *IOStreams, _ context.Context, msg string, options []st field.Height(cfg.PageSize + 2) } - err := huh.NewForm(huh.NewGroup(field)).Run() + err := huh.NewForm(huh.NewGroup(field)).WithTheme(ThemeSlack()).Run() if err != nil { return SelectPromptResponse{}, err } @@ -95,7 +95,7 @@ func charmPasswordPrompt(_ *IOStreams, _ context.Context, message string, cfg Pa if cfg.Required { field.Validate(huh.ValidateMinLength(1)) } - err := huh.NewForm(huh.NewGroup(field)).Run() + err := huh.NewForm(huh.NewGroup(field)).WithTheme(ThemeSlack()).Run() if err != nil { return PasswordPromptResponse{}, err } @@ -115,7 +115,7 @@ func charmMultiSelectPrompt(_ *IOStreams, _ context.Context, message string, opt Options(opts...). Value(&selected) - err := huh.NewForm(huh.NewGroup(field)).Run() + err := huh.NewForm(huh.NewGroup(field)).WithTheme(ThemeSlack()).Run() if err != nil { return []string{}, err } From e12c259abb59eaf2ea593ffa3510b5f303bf1106 Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:26:29 -0500 Subject: [PATCH 5/6] Update internal/iostreams/charm_theme.go Co-authored-by: Eden Zimbelman --- internal/iostreams/charm_theme.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/iostreams/charm_theme.go b/internal/iostreams/charm_theme.go index 0f736c25..ce79b508 100644 --- a/internal/iostreams/charm_theme.go +++ b/internal/iostreams/charm_theme.go @@ -58,7 +58,7 @@ func ThemeSlack() *huh.Theme { // Select styles t.Focused.SelectSelector = lipgloss.NewStyle(). Foreground(slackBlue). - SetString("> ") + SetString("❱ ") t.Focused.Option = lipgloss.NewStyle(). Foreground(slackOptionText) t.Focused.NextIndicator = lipgloss.NewStyle(). From d692f111a64fc976a76e6be51a693920036aa17e Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:26:53 -0500 Subject: [PATCH 6/6] Update internal/iostreams/charm_theme.go Co-authored-by: Eden Zimbelman --- internal/iostreams/charm_theme.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/iostreams/charm_theme.go b/internal/iostreams/charm_theme.go index ce79b508..1c8739f1 100644 --- a/internal/iostreams/charm_theme.go +++ b/internal/iostreams/charm_theme.go @@ -73,7 +73,7 @@ func ThemeSlack() *huh.Theme { // Multi-select styles t.Focused.MultiSelectSelector = lipgloss.NewStyle(). Foreground(slackYellow). - SetString("> ") + SetString("❱ ") t.Focused.SelectedOption = lipgloss.NewStyle(). Foreground(slackGreen) t.Focused.SelectedPrefix = lipgloss.NewStyle().