Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions internal/tui/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2442,3 +2442,34 @@ func TestAFailedToolDoesNotStrandTheSetup(t *testing.T) {
t.Error("the screen does not say what can be done about it")
}
}

// Step 3 on a short terminal. The banner went on top and View cut what did not
// fit from the bottom, so the part that went was the field for the key.
// Reported as: "no hay espacio para ponerla en el setup".
func TestTheKeyFieldFitsOnAShortTerminal(t *testing.T) {
for _, size := range [][2]int{{80, 14}, {80, 18}, {120, 22}, {120, 30}} {
m := setupModel(t, &session.Session{Token: "t"})
m.lay = newLayout(size[0], size[1])
m.wizard = wizardLink
m.startKeyEdit()
if !strings.Contains(m.View(), "paste your NaN API key here") {
t.Errorf("at %dx%d step 3 has no field to paste the key into", size[0], size[1])
}
}
}

// And a key the cluster refuses leaves the field open for another one, instead
// of the error and nothing under it.
func TestARefusedKeyOpensTheFieldAgain(t *testing.T) {
m := setupModel(t, &session.Session{Token: "t", APIKey: testKey})
m.wizard = wizardKey

updated, _ := m.Update(keyCheckedMsg{err: errors.New("401")})
after := updated.(model)
if !after.editingKey {
t.Error("after a refused key step 3 has nowhere to paste another")
}
if !strings.Contains(after.keyCheck, "refused") {
t.Errorf("the refusal is gone: %q", after.keyCheck)
}
}
42 changes: 35 additions & 7 deletions internal/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// than five times later as a 401 inside five different tools.
if msg.err != nil {
m.keyCheck = "error: the cluster refused this key — " + msg.err.Error()
// During the guided setup the field closed on enter and stayed
// closed, so a refused key left step 3 with the error and nothing to
// paste a better one into. It opens again, empty, under the error.
if m.wizard == wizardKey && !m.editingKey {
return m, m.startKeyEdit()
}
} else {
m.keyCheck = fmt.Sprintf("key accepted by the cluster · %d models", msg.models)
// The last link in the chain. Signing in leads to the key, and the
Expand Down Expand Up @@ -500,7 +506,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.setupMsg = "error saving: " + err.Error()
} else {
m.setupMsg = "API key saved"
m.keyCheck = "checking it against the cluster…"
m.keyCheck = keyChecking
check = checkKey(val)
}
}
Expand Down Expand Up @@ -546,6 +552,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
case "enter":
// Step 3 with its field closed - the key is still being checked, or
// something closed it - says "enter to continue", so enter opens it.
if m.wizard == wizardKey && m.keyCheck != keyChecking {
return m, m.startKeyEdit()
}
if m.wizard == wizardDone {
m.wizard = wizardOff
m.active = tabIndex(tabHome)
Expand Down Expand Up @@ -752,6 +763,9 @@ func (m *model) startLogin() tea.Cmd {
return m.loginInput.Focus()
}

// What step 3 says while the cluster is looking at a key just saved.
const keyChecking = "checking it against the cluster…"

// startKeyEdit opens the API key field, moving to the tab that shows it.
func (m *model) startKeyEdit() tea.Cmd {
if m.wizard != wizardOff {
Expand Down Expand Up @@ -3231,11 +3245,6 @@ func (m model) renderWizard(l layout) string {
now := lipgloss.NewStyle().Foreground(cCyan).Bold(true)

var b strings.Builder
if l.w >= BannerWidthPlain+4 {
b.WriteString(Banner(l.indent, m.wizard.mood(),
l.w >= BannerWidth+4 && l.h >= BannerRoom) + "\n")
}

b.WriteString(l.indent + title.Render("Setting up") + "\n\n")

at := m.wizard.index()
Expand Down Expand Up @@ -3283,7 +3292,26 @@ func (m model) renderWizard(l layout) string {
}
b.WriteString(m.renderFailures(l))
}
return b.String()

// The banner goes on top only if it leaves room for the rest. It used to go
// first unconditionally, and View cuts what does not fit from the bottom -
// so on a short terminal the part that went was the field for the key, and
// step 3 asked for one with nowhere to type it. The mascot goes first, then
// the wordmark; the step you are on never does.
body := b.String()
room := l.h - 4 - strings.Count(body, "\n")
if l.w >= BannerWidthPlain+4 {
withMascot := l.w >= BannerWidth+4 && l.h >= BannerRoom
banner := Banner(l.indent, m.wizard.mood(), withMascot) + "\n"
if withMascot && strings.Count(banner, "\n") > room {
withMascot = false
banner = Banner(l.indent, m.wizard.mood(), false) + "\n"
}
if strings.Count(banner, "\n") <= room {
body = banner + body
}
}
return body
}

// What would not configure, and what to do about it.
Expand Down
Loading