From b331f8a6498288f37b9dd8f83977e5bfa3ae60c8 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 16:40:18 +0000 Subject: [PATCH 1/7] Show a blank prompt glyph until invalid input is edited The prompt glyph ran Validate on every render, so a prompt that rejects empty input, such as the login host prompt, opened with a red cross before anything was typed. The glyph is now blank until the first edit or Enter, then shows a cross while the input is invalid and a checkmark once it is valid. Co-authored-by: Isaac --- .../prompt_validate_baseline_test.go | 44 +++++++++++++++++++ .../01-empty.golden | 2 +- .../01-enter.golden | 2 + .../01-initial.golden | 1 + libs/cmdio/prompt.go | 21 ++++++--- 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go index 5f703a5865d..68d507a57fb 100644 --- a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -45,3 +45,47 @@ func TestPromptBaseline_Validate(t *testing.T) { require.NoError(t, err, "raw output: %q", tm.Raw()) assert.Equal(t, "https://example.com", v, "snapshot:\n%s", tm.Snapshot()) } + +// TestPromptBaseline_ValidateEnterBeforeEdit pins that Enter on an untouched +// prompt flips the glyph to "✗" along with the error line. +func TestPromptBaseline_ValidateEnterBeforeEdit(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if s == "" { + return errors.New("host is required") + } + return nil + }, + }) + tm.WaitFor("Workspace host") + tm.Type(termtest.KeyEnter) + tm.Golden("01-enter") + + tm.Type(termtest.KeyCtrlC) + _, err := tm.Result() + assert.Error(t, err) +} + +// TestPromptBaseline_ValidateValidBeforeEdit pins that an untouched prompt +// whose input already passes Validate shows "✔". +func TestPromptBaseline_ValidateValidBeforeEdit(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if strings.Contains(s, " ") { + return errors.New("must not contain spaces") + } + return nil + }, + }) + tm.WaitFor("Workspace host") + tm.Golden("01-initial") + + tm.Type(termtest.KeyEnter) + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Empty(t, v) +} diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden index aef99ba9f27..6da352062e8 100644 --- a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden @@ -1 +1 @@ -✗ Workspace host: █ + Workspace host: █ diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden new file mode 100644 index 00000000000..a001d0c4bfa --- /dev/null +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden @@ -0,0 +1,2 @@ +✗ Workspace host: █ +>> host is required diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden new file mode 100644 index 00000000000..c2487520307 --- /dev/null +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden @@ -0,0 +1 @@ +✔ Workspace host: █ diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 8c8ca1a3a39..52870183e09 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -33,7 +33,9 @@ type PromptOptions struct { // Validate, when set, is called on every keystroke. While it returns a // non-nil error the leading glyph flips from "✔" to "✗" and Enter is // inert; pressing Enter while invalid surfaces the returned error - // below the prompt until the next edit. + // below the prompt until the next edit. Until the first edit or Enter, + // an invalid input shows a blank glyph instead, so an empty prompt does + // not open as an error. Validate func(input string) error } @@ -73,6 +75,9 @@ type promptModel struct { deleted bool submitted bool + // edited is set by the first change to runes. + edited bool + // submitErr is the error from the last failed Enter attempt. Rendered // below the prompt and cleared on the next edit. submitErr error @@ -92,12 +97,16 @@ func (m *promptModel) value() string { } // glyph returns the leading status indicator: bold-green ✔ when valid, -// bold-red ✗ when validate rejects the buffer. +// bold-red ✗ when validate rejects the buffer after an edit or Enter, and a +// blank before that. func (m *promptModel) glyph() string { - if m.validate != nil && m.validate(m.value()) != nil { - return ansiBold + ansiRed + glyphInvalid + ansiReset + if m.validate == nil || m.validate(m.value()) == nil { + return ansiBold + ansiGreen + glyphValid + ansiReset + } + if !m.edited && m.submitErr == nil { + return " " } - return ansiBold + ansiGreen + glyphValid + ansiReset + return ansiBold + ansiRed + glyphInvalid + ansiReset } func (m *promptModel) Init() tea.Cmd { return nil } @@ -154,6 +163,7 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.runes = append(m.runes[:m.cursor-1], m.runes[m.cursor:]...) m.cursor-- + m.edited = true m.submitErr = nil case tea.KeyRunes, tea.KeySpace: @@ -172,6 +182,7 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.runes = append(m.runes[:m.cursor], typed...) m.runes = append(m.runes, tail...) m.cursor += len(typed) + m.edited = true m.submitErr = nil default: From ac96528a4eda22ecc8e42c1b7ba181c778acdf90 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 13:31:50 +0000 Subject: [PATCH 2/7] Add a changelog fragment for the blank prompt glyph Co-authored-by: Isaac --- .nextchanges/cli/prompt-blank-glyph.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nextchanges/cli/prompt-blank-glyph.md diff --git a/.nextchanges/cli/prompt-blank-glyph.md b/.nextchanges/cli/prompt-blank-glyph.md new file mode 100644 index 00000000000..412d3abb250 --- /dev/null +++ b/.nextchanges/cli/prompt-blank-glyph.md @@ -0,0 +1 @@ +* Prompts that validate their input, such as the `databricks auth login` host prompt, no longer open with a red cross before anything is typed. ([#7314](https://github.com/databricks/cli/pull/7314)) From 0208a18f6fc51d9d44c7400fcedf11b18b0a3e25 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 13:44:02 +0000 Subject: [PATCH 3/7] Tighten the blank prompt glyph test and changelog example Assert the exact Ctrl+C error, drop the redundant edited flag in the backspace branch, and cite the configure host prompt in the changelog, since the auth login host prompt has no Validate on main. Co-authored-by: Isaac --- .nextchanges/cli/prompt-blank-glyph.md | 2 +- libs/cmdio/cmdiotest/prompt_validate_baseline_test.go | 3 ++- libs/cmdio/prompt.go | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nextchanges/cli/prompt-blank-glyph.md b/.nextchanges/cli/prompt-blank-glyph.md index 412d3abb250..8771493937a 100644 --- a/.nextchanges/cli/prompt-blank-glyph.md +++ b/.nextchanges/cli/prompt-blank-glyph.md @@ -1 +1 @@ -* Prompts that validate their input, such as the `databricks auth login` host prompt, no longer open with a red cross before anything is typed. ([#7314](https://github.com/databricks/cli/pull/7314)) +* Prompts that validate their input, such as the `databricks configure` host prompt, no longer open with a red cross before anything is typed. ([#7314](https://github.com/databricks/cli/pull/7314)) diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go index 68d507a57fb..181186448e6 100644 --- a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -65,7 +65,8 @@ func TestPromptBaseline_ValidateEnterBeforeEdit(t *testing.T) { tm.Type(termtest.KeyCtrlC) _, err := tm.Result() - assert.Error(t, err) + require.Error(t, err) + assert.EqualError(t, err, "^C") } // TestPromptBaseline_ValidateValidBeforeEdit pins that an untouched prompt diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 52870183e09..01600186f1b 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -163,7 +163,6 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.runes = append(m.runes[:m.cursor-1], m.runes[m.cursor:]...) m.cursor-- - m.edited = true m.submitErr = nil case tea.KeyRunes, tea.KeySpace: From 6e52fb26394c0a342f03e69022b3d3bfd8234cd8 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 16:15:21 +0000 Subject: [PATCH 4/7] Track a single touched flag for the blank prompt glyph Co-authored-by: Isaac --- libs/cmdio/prompt.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 01600186f1b..3705e2810f0 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -75,8 +75,9 @@ type promptModel struct { deleted bool submitted bool - // edited is set by the first change to runes. - edited bool + // touched is set by the first edit or Enter. Until then an invalid input + // shows a blank glyph, so a required field does not open as an error. + touched bool // submitErr is the error from the last failed Enter attempt. Rendered // below the prompt and cleared on the next edit. @@ -103,7 +104,7 @@ func (m *promptModel) glyph() string { if m.validate == nil || m.validate(m.value()) == nil { return ansiBold + ansiGreen + glyphValid + ansiReset } - if !m.edited && m.submitErr == nil { + if !m.touched { return " " } return ansiBold + ansiRed + glyphInvalid + ansiReset @@ -124,6 +125,7 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyEnter, tea.KeyCtrlJ: // Enter sends CR, Ctrl+J sends LF; treat both as submit. + m.touched = true if m.validate != nil { if err := m.validate(m.value()); err != nil { m.submitErr = err @@ -181,7 +183,7 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.runes = append(m.runes[:m.cursor], typed...) m.runes = append(m.runes, tail...) m.cursor += len(typed) - m.edited = true + m.touched = true m.submitErr = nil default: From 9bfe9b1f90c7a6d9cf57cb0d4f71848436bc00a3 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 16:15:47 +0000 Subject: [PATCH 5/7] Use a glyphBlank constant for the blank prompt glyph Co-authored-by: Isaac --- libs/cmdio/prompt.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 3705e2810f0..6d7450571eb 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -10,11 +10,13 @@ import ( ) // Glyphs drawn into the prompt's rendered output. cursorBlock stands in for -// the (hidden) OS cursor at the current edit position. +// the (hidden) OS cursor at the current edit position. The glyphs must all be +// one column wide so the label does not shift when the glyph changes. const ( cursorBlock = "█" glyphValid = "✔" glyphInvalid = "✗" + glyphBlank = " " ) // PromptOptions configures a single-line text prompt shown by [RunPrompt]. @@ -105,7 +107,7 @@ func (m *promptModel) glyph() string { return ansiBold + ansiGreen + glyphValid + ansiReset } if !m.touched { - return " " + return glyphBlank } return ansiBold + ansiRed + glyphInvalid + ansiReset } From ba3a77bdf162b51f2d64f3bccde6c70769a12b09 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 16:15:50 +0000 Subject: [PATCH 6/7] Ignore empty pastes in the prompt Co-authored-by: Isaac --- libs/cmdio/prompt.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 6d7450571eb..4469465e972 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -181,6 +181,12 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if key.Type == tea.KeySpace { typed = []rune{' '} } + // Some terminals send an empty bracketed paste when the clipboard is + // empty. Nothing changes, so it must not count as an edit or clear the + // error line. + if len(typed) == 0 { + return m, nil + } tail := append([]rune{}, m.runes[m.cursor:]...) m.runes = append(m.runes[:m.cursor], typed...) m.runes = append(m.runes, tail...) From 6175522a1e2b07d3e75d032ba3786777598c2bf9 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 16:16:42 +0000 Subject: [PATCH 7/7] Test that navigation keys and empty pastes keep the blank prompt glyph Co-authored-by: Isaac --- .../prompt_validate_baseline_test.go | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go index 181186448e6..1cca78dbf6d 100644 --- a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -90,3 +90,45 @@ func TestPromptBaseline_ValidateValidBeforeEdit(t *testing.T) { require.NoError(t, err, "raw output: %q", tm.Raw()) assert.Empty(t, v) } + +// TestPromptBaseline_ValidateNonEditKeysKeepBlankGlyph pins that keys which +// leave the buffer unchanged do not flip the blank glyph to "✗". +func TestPromptBaseline_ValidateNonEditKeysKeepBlankGlyph(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if s == "" { + return errors.New("host is required") + } + return nil + }, + }) + tm.WaitFor("Workspace host") + tm.Type(termtest.KeyLeft + termtest.KeyRight + termtest.KeyBackspace + termtest.KeyCtrlC) + _, err := tm.Result() + assert.EqualError(t, err, "^C") + // The prompt line is erased on exit, so check every rendered frame. + assert.NotContains(t, tm.Raw(), "✗") +} + +// TestPromptBaseline_ValidateEmptyPasteKeepsBlankGlyph pins that an empty +// bracketed paste, which some terminals send for an empty clipboard, is not +// treated as an edit. +func TestPromptBaseline_ValidateEmptyPasteKeepsBlankGlyph(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if s == "" { + return errors.New("host is required") + } + return nil + }, + }) + tm.WaitFor("Workspace host") + tm.Type("\x1b[200~\x1b[201~" + termtest.KeyCtrlC) + _, err := tm.Result() + assert.EqualError(t, err, "^C") + assert.NotContains(t, tm.Raw(), "✗") +}