cmd/tui: simplify integration selection and update menu description (#17159)

This commit is contained in:
Parth Sareen
2026-07-13 12:52:40 -07:00
committed by GitHub
parent 59bd0b49bb
commit cd600e19a3
2 changed files with 41 additions and 160 deletions

View File

@@ -42,68 +42,41 @@ type menuItem struct {
description string
integration string
isRunModel bool
isOthers bool
}
const pinnedIntegrationCount = 4
var runModelMenuItem = menuItem{
title: "Chat, Code, & Work",
description: "Chat with models, code, search the web, and do work",
description: "Chat with models, code, search the web, and delegate real work",
isRunModel: true,
}
var othersMenuItem = menuItem{
title: "More...",
description: "Show additional integrations",
isOthers: true,
}
// launcherMenuIntegrations is intentionally short: the root ollama command is
// a quick path to the most common launch targets. Other registered
// integrations remain available through `ollama launch <integration>`.
var launcherMenuIntegrations = []string{"claude", "opencode", "hermes", "openclaw"}
type model struct {
state *launch.LauncherState
items []menuItem
cursor int
showOthers bool
width int
quitting bool
selected bool
action TUIAction
state *launch.LauncherState
items []menuItem
cursor int
width int
quitting bool
selected bool
action TUIAction
}
func newModel(state *launch.LauncherState) model {
m := model{
state: state,
}
m.showOthers = shouldExpandOthers(state)
m.items = buildMenuItems(state, m.showOthers)
m.items = buildMenuItems(state)
m.cursor = initialCursor(state, m.items)
return m
}
func shouldExpandOthers(state *launch.LauncherState) bool {
if state == nil {
return false
}
for _, item := range otherIntegrationItems(state) {
if item.integration == state.LastSelection {
return true
}
}
return false
}
func buildMenuItems(state *launch.LauncherState, showOthers bool) []menuItem {
func buildMenuItems(state *launch.LauncherState) []menuItem {
items := []menuItem{runModelMenuItem}
items = append(items, pinnedIntegrationItems(state)...)
otherItems := otherIntegrationItems(state)
switch {
case showOthers:
items = append(items, otherItems...)
case len(otherItems) > 0:
items = append(items, othersMenuItem)
}
items = append(items, launcherIntegrationItems(state)...)
return items
}
@@ -119,30 +92,14 @@ func integrationMenuItem(state launch.LauncherIntegrationState) menuItem {
}
}
func otherIntegrationItems(state *launch.LauncherState) []menuItem {
ordered := orderedIntegrationItems(state)
if len(ordered) <= pinnedIntegrationCount {
return nil
}
return ordered[pinnedIntegrationCount:]
}
func pinnedIntegrationItems(state *launch.LauncherState) []menuItem {
ordered := orderedIntegrationItems(state)
if len(ordered) <= pinnedIntegrationCount {
return ordered
}
return ordered[:pinnedIntegrationCount]
}
func orderedIntegrationItems(state *launch.LauncherState) []menuItem {
func launcherIntegrationItems(state *launch.LauncherState) []menuItem {
if state == nil {
return nil
}
items := make([]menuItem, 0, len(state.Integrations))
for _, info := range launch.ListIntegrationInfos() {
integrationState, ok := state.Integrations[info.Name]
items := make([]menuItem, 0, len(launcherMenuIntegrations))
for _, name := range launcherMenuIntegrations {
integrationState, ok := state.Integrations[name]
if !ok {
continue
}
@@ -151,10 +108,6 @@ func orderedIntegrationItems(state *launch.LauncherState) []menuItem {
return items
}
func primaryMenuItemCount(state *launch.LauncherState) int {
return 1 + len(pinnedIntegrationItems(state))
}
func initialCursor(state *launch.LauncherState, items []menuItem) int {
if state == nil || state.LastSelection == "" {
return 0
@@ -190,21 +143,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.cursor > 0 {
m.cursor--
}
if m.showOthers && m.cursor < primaryMenuItemCount(m.state) {
m.showOthers = false
m.items = buildMenuItems(m.state, false)
m.cursor = min(m.cursor, len(m.items)-1)
}
return m, nil
case "down", "j":
if m.cursor < len(m.items)-1 {
m.cursor++
}
if m.cursor < len(m.items) && m.items[m.cursor].isOthers && !m.showOthers {
m.showOthers = true
m.items = buildMenuItems(m.state, true)
}
return m, nil
case "enter", " ":
@@ -235,7 +179,7 @@ func (m model) selectableItem(item menuItem) bool {
if item.isRunModel {
return true
}
if item.integration == "" || item.isOthers {
if item.integration == "" {
return false
}
state, ok := m.state.Integrations[item.integration]
@@ -243,7 +187,7 @@ func (m model) selectableItem(item menuItem) bool {
}
func (m model) changeableItem(item menuItem) bool {
if item.integration == "" || item.isOthers {
if item.integration == "" {
return false
}
state, ok := m.state.Integrations[item.integration]
@@ -287,10 +231,6 @@ func (m model) renderMenuItem(index int, item menuItem) string {
if m.cursor == index {
style = menuSelectedItemStyle
}
} else if item.isOthers {
if m.cursor == index {
style = menuSelectedItemStyle
}
} else {
integrationState := m.state.Integrations[item.integration]
if !integrationState.Selectable {

View File

@@ -91,8 +91,6 @@ func integrationSequence(items []menuItem) []string {
switch {
case item.isRunModel:
sequence = append(sequence, "run")
case item.isOthers:
sequence = append(sequence, "more")
case item.integration != "":
sequence = append(sequence, item.integration)
}
@@ -104,88 +102,31 @@ func compareStrings(got, want []string) string {
return cmp.Diff(want, got)
}
func expectedCollapsedSequence(state *launch.LauncherState) []string {
sequence := []string{"run"}
for _, item := range pinnedIntegrationItems(state) {
sequence = append(sequence, item.integration)
}
if len(otherIntegrationItems(state)) > 0 {
sequence = append(sequence, "more")
}
return sequence
}
func expectedExpandedSequence(state *launch.LauncherState) []string {
sequence := []string{"run"}
for _, item := range pinnedIntegrationItems(state) {
sequence = append(sequence, item.integration)
}
for _, item := range otherIntegrationItems(state) {
sequence = append(sequence, item.integration)
}
return sequence
}
func TestMenuRendersPinnedItemsAndMore(t *testing.T) {
func TestMenuRendersRootLaunchChoices(t *testing.T) {
state := launcherTestState()
menu := newModel(state)
wantPrefix := []string{"run", "claude", "codex-app", "hermes", "openclaw"}
if findMenuCursorByIntegration(menu.items, "codex-app") == -1 {
wantPrefix = []string{"run", "claude", "hermes", "openclaw", "opencode"}
}
if got := integrationSequence(menu.items); len(got) < len(wantPrefix) {
t.Fatalf("expected at least %d menu items, got %v", len(wantPrefix), got)
} else if diff := compareStrings(got[:len(wantPrefix)], wantPrefix); diff != "" {
t.Fatalf("unexpected primary TUI order: %s", diff)
want := []string{"run", "claude", "opencode", "hermes", "openclaw"}
if diff := compareStrings(integrationSequence(menu.items), want); diff != "" {
t.Fatalf("unexpected root launch choices: %s", diff)
}
view := menu.View()
for _, want := range []string{
"Chat, Code, & Work",
"Chat with models, code, search the web, and do work",
"Chat with models, code, search the web, and delegate real work",
"Launch Claude Code",
"Launch OpenCode",
"Launch Hermes Agent",
"Launch OpenClaw",
"More...",
} {
if !strings.Contains(view, want) {
t.Fatalf("expected menu view to contain %q\n%s", want, view)
}
}
if findMenuCursorByIntegration(menu.items, "codex-app") != -1 && !strings.Contains(view, "Launch Codex App") {
t.Fatalf("expected menu view to contain Codex App\n%s", view)
}
if strings.Contains(view, "Launch Claude Desktop") {
t.Fatalf("expected hidden Claude Desktop to be absent\n%s", view)
}
wantOrder := expectedCollapsedSequence(state)
if diff := compareStrings(integrationSequence(menu.items), wantOrder); diff != "" {
t.Fatalf("unexpected pinned order: %s", diff)
}
}
func TestMenuExpandsOthersFromLastSelection(t *testing.T) {
state := launcherTestState()
overflow := otherIntegrationItems(state)
if len(overflow) == 0 {
t.Fatal("expected at least one overflow integration")
}
state.LastSelection = overflow[0].integration
menu := newModel(state)
if !menu.showOthers {
t.Fatal("expected others section to expand when last selection is in the overflow list")
}
view := menu.View()
if !strings.Contains(view, overflow[0].title) {
t.Fatalf("expected expanded view to contain overflow integration\n%s", view)
}
if strings.Contains(view, "More...") {
t.Fatalf("expected expanded view to replace More... item\n%s", view)
}
wantOrder := expectedExpandedSequence(state)
if diff := compareStrings(integrationSequence(menu.items), wantOrder); diff != "" {
t.Fatalf("unexpected expanded order: %s", diff)
for _, hidden := range []string{"Launch Codex App", "Launch Codex", "Launch Droid", "Launch Pi", "More..."} {
if strings.Contains(view, hidden) {
t.Fatalf("expected root menu to omit %q\n%s", hidden, view)
}
}
}
@@ -280,24 +221,24 @@ func TestMenuShowsCurrentModelSuffixes(t *testing.T) {
func TestMenuShowsInstallStatusAndHint(t *testing.T) {
state := launcherTestState()
codex := state.Integrations["codex"]
codex.Installed = false
codex.Selectable = false
codex.Changeable = false
codex.InstallHint = "Install from https://example.com/codex"
state.Integrations["codex"] = codex
opencode := state.Integrations["opencode"]
opencode.Installed = false
opencode.Selectable = false
opencode.Changeable = false
opencode.InstallHint = "Install from https://example.com/opencode"
state.Integrations["opencode"] = opencode
state.LastSelection = "codex"
state.LastSelection = "opencode"
menu := newModel(state)
menu.cursor = findMenuCursorByIntegration(menu.items, "codex")
menu.cursor = findMenuCursorByIntegration(menu.items, "opencode")
if menu.cursor == -1 {
t.Fatal("expected codex menu item in overflow section")
t.Fatal("expected opencode menu item")
}
view := menu.View()
if !strings.Contains(view, "(not installed)") {
t.Fatalf("expected not-installed marker\n%s", view)
}
if !strings.Contains(view, codex.InstallHint) {
if !strings.Contains(view, opencode.InstallHint) {
t.Fatalf("expected install hint in description\n%s", view)
}
}