From 4f312e16fbd1770d4efa0a072a16e49bea9fef74 Mon Sep 17 00:00:00 2001 From: "Matthias G. Eckermann" Date: Thu, 29 Oct 2026 23:51:05 +0100 Subject: [PATCH 1/2] fix(ui): show all lines of a prompt View() measures the layout only when layoutDirty is set or a fill widget is active. Opening a prompt did not set layoutDirty, so the transcript kept the height that it had for the input area. The prompt replaces the input area and is taller. The frame was then taller than the terminal. The terminal did not show the options of the prompt, its key help and the status bar. Set layoutDirty when a prompt of an extension or the password prompt opens, and in resolvePrompt when a prompt closes. Without the second change, the transcript stays too short after the prompt. TestPromptFitsTerminal checks that the frame has the height of the terminal before, during and after a select prompt and a password prompt. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Matthias G. Eckermann --- internal/ui/model.go | 8 +++ internal/ui/prompt_layout_test.go | 84 +++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 internal/ui/prompt_layout_test.go diff --git a/internal/ui/model.go b/internal/ui/model.go index a45414f4..6adb5d0a 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -6381,6 +6381,8 @@ func (m *AppModel) resolvePrompt(resp app.PromptResponse) { } m.prompt = nil m.state = m.prePromptState + // The input area comes back. Measure the layout again. + m.layoutDirty = true } // -------------------------------------------------------------------------- @@ -7781,6 +7783,9 @@ func (m *AppModel) handleAppMessage(msg tea.Msg, cmds []tea.Cmd) (tea.Model, tea if m.prompt != nil { cmds = append(cmds, m.prompt.Init()) } + // The prompt replaces the input area and is taller. Measure the + // layout again, or the terminal does not show its last lines. + m.layoutDirty = true case app.PromptRequestEvent: // Extension wants to show an interactive prompt. Enter prompt state. @@ -7818,6 +7823,9 @@ func (m *AppModel) handleAppMessage(msg tea.Msg, cmds []tea.Cmd) (tea.Model, tea if m.prompt != nil { cmds = append(cmds, m.prompt.Init()) } + // The prompt replaces the input area and is taller. Measure the + // layout again, or the terminal does not show its last lines. + m.layoutDirty = true case app.OverlayRequestEvent: // Extension wants to show a modal overlay dialog. Enter overlay state. diff --git a/internal/ui/prompt_layout_test.go b/internal/ui/prompt_layout_test.go new file mode 100644 index 00000000..85fb17fe --- /dev/null +++ b/internal/ui/prompt_layout_test.go @@ -0,0 +1,84 @@ +package ui + +import ( + "strings" + "testing" + "time" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + + "github.com/mark3labs/kit/internal/app" +) + +// TestPromptFitsTerminal verifies that the layout is measured again when a +// prompt opens and when it closes. The prompt replaces the input area and is +// taller than it. If the transcript keeps the height that it had for the +// input area, the view is taller than the terminal. Then the terminal does +// not show the options of the prompt and the status bar. +func TestPromptFitsTerminal(t *testing.T) { + newModel := func(t *testing.T) *AppModel { + t.Helper() + m, _, _ := newTestAppModel(&stubAppController{}) + m.input = NewInputComponent(80, nil) + m = sendMsg(m, tea.WindowSizeMsg{Width: 80, Height: 24}) + checkViewHeight(t, m, "before the prompt") + return m + } + + t.Run("select prompt of an extension", func(t *testing.T) { + m := newModel(t) + ch := make(chan app.PromptResponse, 1) + m = sendMsg(m, app.PromptRequestEvent{ + PromptType: "select", + Message: "Choose a target", + Options: []string{"local", "staging", "production"}, + ResponseCh: ch, + }) + if view := checkViewHeight(t, m, "with the prompt"); !strings.Contains(view, "production") { + t.Fatalf("the view does not show the last option\n%s", view) + } + + m = sendMsg(m, tea.KeyPressMsg{Code: tea.KeyEscape}) + select { + case resp := <-ch: + if !resp.Cancelled { + t.Fatalf("Esc gave %+v, want a cancelled prompt", resp) + } + case <-time.After(5 * time.Second): + t.Fatal("no response after Esc") + } + checkViewHeight(t, m, "after the prompt") + }) + + t.Run("password prompt", func(t *testing.T) { + m := newModel(t) + ch := make(chan app.PasswordPromptResponse, 1) + m = sendMsg(m, app.PasswordPromptEvent{Prompt: "Password for sudo", ResponseCh: ch}) + if view := checkViewHeight(t, m, "with the prompt"); !strings.Contains(view, "Password for sudo") { + t.Fatalf("the view does not show the prompt\n%s", view) + } + + m = sendMsg(m, tea.KeyPressMsg{Code: tea.KeyEscape}) + select { + case resp := <-ch: + if !resp.Cancelled { + t.Fatalf("Esc gave %+v, want a cancelled prompt", resp) + } + case <-time.After(5 * time.Second): + t.Fatal("no response after Esc") + } + checkViewHeight(t, m, "after the prompt") + }) +} + +// checkViewHeight renders a frame and stops the test if the frame does not +// have the height of the terminal. It returns the frame without ANSI codes. +func checkViewHeight(t *testing.T, m *AppModel, when string) string { + t.Helper() + view := m.View().Content + if got := lipgloss.Height(view); got != m.height { + t.Fatalf("view height %s = %d, want %d\n%s", when, got, m.height, stripAnsi(view)) + } + return stripAnsi(view) +} From b558d773f5a42c6f0984b5d1c0eb16456e7871a2 Mon Sep 17 00:00:00 2001 From: "Matthias G. Eckermann" Date: Fri, 30 Oct 2026 00:22:00 +0100 Subject: [PATCH 2/2] test(ui): check the status bar in the prompt test checkViewHeight checked only the height of the frame. A change can remove the status bar while a prompt is open and give its line to the transcript. Then the frame keeps the height of the terminal, and the test passed. The helper now also checks that the last line of the frame is the status bar with the model name. This applies before, during and after both prompts. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Matthias G. Eckermann --- internal/ui/prompt_layout_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/ui/prompt_layout_test.go b/internal/ui/prompt_layout_test.go index 85fb17fe..93c02655 100644 --- a/internal/ui/prompt_layout_test.go +++ b/internal/ui/prompt_layout_test.go @@ -72,13 +72,18 @@ func TestPromptFitsTerminal(t *testing.T) { }) } -// checkViewHeight renders a frame and stops the test if the frame does not -// have the height of the terminal. It returns the frame without ANSI codes. +// checkViewHeight renders a frame and stops the test unless the frame has +// the height of the terminal and ends with the status bar. The status bar +// shows the model name. It returns the frame without ANSI codes. func checkViewHeight(t *testing.T, m *AppModel, when string) string { t.Helper() view := m.View().Content if got := lipgloss.Height(view); got != m.height { t.Fatalf("view height %s = %d, want %d\n%s", when, got, m.height, stripAnsi(view)) } - return stripAnsi(view) + plain := stripAnsi(view) + if last := plain[strings.LastIndex(plain, "\n")+1:]; !strings.Contains(last, m.modelName) { + t.Fatalf("last line %s = %q, want the status bar with %q\n%s", when, last, m.modelName, plain) + } + return plain }