From 6519ac64f163808841562461bcb243f640695005 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Fri, 6 Mar 2026 16:50:42 -0800 Subject: [PATCH 1/2] feat: add placeholder to app name prompt in create command Show the randomly generated app name as placeholder text in the input field instead of printing a separate hint line. This provides a cleaner UX where users see the default name inline and can press Enter to accept. --- cmd/project/create.go | 5 +++-- cmd/project/create_test.go | 32 ++++++++++++++++++++++++++++++++ internal/iostreams/charm.go | 1 + internal/iostreams/charm_test.go | 9 +++++++++ internal/iostreams/survey.go | 4 +++- 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/cmd/project/create.go b/cmd/project/create.go index f73dec98..f9c1cd97 100644 --- a/cmd/project/create.go +++ b/cmd/project/create.go @@ -151,8 +151,9 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] if appNameArg == "" { if clients.IO.IsTTY() { defaultName := generateRandomAppName() - cmd.Print(style.Secondary(fmt.Sprintf(" Press Enter to use the generated name: %s", defaultName)), "\n") - name, err := clients.IO.InputPrompt(ctx, "Name your app:", iostreams.InputPromptConfig{}) + name, err := clients.IO.InputPrompt(ctx, "Name your app:", iostreams.InputPromptConfig{ + Placeholder: defaultName, + }) if err != nil { return err } diff --git a/cmd/project/create_test.go b/cmd/project/create_test.go index 37d83102..f8ad6405 100644 --- a/cmd/project/create_test.go +++ b/cmd/project/create_test.go @@ -345,6 +345,38 @@ func TestCreateCommand(t *testing.T) { cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) }, }, + "name prompt includes placeholder with generated name": { + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.IO.On("IsTTY").Return(true) + cm.IO.On("SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything). + Return( + iostreams.SelectPromptResponse{ + Prompt: true, + Index: 0, + }, + nil, + ) + cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything). + Return( + iostreams.SelectPromptResponse{ + Prompt: true, + Index: 0, + }, + nil, + ) + cm.IO.On("InputPrompt", mock.Anything, "Name your app:", mock.Anything). + Return("my-app", nil) + createClientMock = new(CreateClientMock) + createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", nil) + CreateFunc = createClientMock.Create + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + // Verify that InputPrompt was called with a config that has a non-empty Placeholder + cm.IO.AssertCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.MatchedBy(func(cfg iostreams.InputPromptConfig) bool { + return cfg.Placeholder != "" + })) + }, + }, "user accepts default name from prompt": { Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { cm.IO.On("IsTTY").Return(true) diff --git a/internal/iostreams/charm.go b/internal/iostreams/charm.go index d55b336e..0e0f4350 100644 --- a/internal/iostreams/charm.go +++ b/internal/iostreams/charm.go @@ -29,6 +29,7 @@ import ( func buildInputForm(message string, cfg InputPromptConfig, input *string) *huh.Form { field := huh.NewInput(). Title(message). + Placeholder(cfg.Placeholder). Value(input) if cfg.Required { field.Validate(huh.ValidateMinLength(1)) diff --git a/internal/iostreams/charm_test.go b/internal/iostreams/charm_test.go index 1075c8b1..1aef7176 100644 --- a/internal/iostreams/charm_test.go +++ b/internal/iostreams/charm_test.go @@ -53,6 +53,15 @@ func TestCharmInput(t *testing.T) { assert.Contains(t, view, "Huh") }) + t.Run("renders placeholder text", func(t *testing.T) { + var input string + f := buildInputForm("Name?", InputPromptConfig{Placeholder: "my-cool-app"}, &input) + f.Update(f.Init()) + + view := ansi.Strip(f.View()) + assert.Contains(t, view, "my-cool-app") + }) + t.Run("stores typed value", func(t *testing.T) { var input string f := buildInputForm("Name?", InputPromptConfig{}, &input) diff --git a/internal/iostreams/survey.go b/internal/iostreams/survey.go index 8e2d95a1..7873681d 100644 --- a/internal/iostreams/survey.go +++ b/internal/iostreams/survey.go @@ -179,7 +179,8 @@ var InputQuestionTemplate = fmt.Sprintf(` // InputPromptConfig holds additional config for an Input prompt type InputPromptConfig struct { - Required bool // Whether the input must be non-empty + Required bool // Whether the input must be non-empty + Placeholder string // Placeholder text shown when input is empty } // GetFlags returns all flags for the Input prompt @@ -208,6 +209,7 @@ func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputP var input string err := survey.AskOne(&survey.Input{ Message: message, + Default: cfg.Placeholder, }, &input, SurveyOptions(cfg)...) if err != nil { From c362c5a2f4344fc1431a6133c1f1b03d86adcf44 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Fri, 6 Mar 2026 17:02:35 -0800 Subject: [PATCH 2/2] style: display survey input placeholder text in grey --- internal/iostreams/survey.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/iostreams/survey.go b/internal/iostreams/survey.go index 7873681d..aebd9774 100644 --- a/internal/iostreams/survey.go +++ b/internal/iostreams/survey.go @@ -174,8 +174,8 @@ var InputQuestionTemplate = fmt.Sprintf(` {{- if and .Help (not .ShowHelp)}}{{ print .Config.HelpInput }} for help {{- if and .Suggest}}, {{end}}{{end -}} {{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}} ]{{color "reset"}} {{end}} - {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} -{{- end}}`, blue()) + {{- if .Default}}{{color "%s"}}({{.Default}}) {{color "reset"}}{{end}} +{{- end}}`, blue(), gray()) // InputPromptConfig holds additional config for an Input prompt type InputPromptConfig struct {