From eacaa7970646f0fa861419174bc8cb008331a987 Mon Sep 17 00:00:00 2001 From: ParthSareen Date: Wed, 24 Jun 2026 15:34:04 -0700 Subject: [PATCH] cmd/tui/chat: show availability badge beside model name instead of plan in meta --- cmd/agent_tui.go | 47 ++++++++++++++++++++++++++++++++++++++++++ cmd/tui/chat/chat.go | 11 +++++----- cmd/tui/chat/modals.go | 14 ++++++++++--- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/cmd/agent_tui.go b/cmd/agent_tui.go index 1a2abd4c3..8b577ccb5 100644 --- a/cmd/agent_tui.go +++ b/cmd/agent_tui.go @@ -577,9 +577,56 @@ func agentModelOptions(ctx context.Context, client *api.Client) ([]agentchat.Mod add(name, agentLocalModelDescription(model), false, "", false) } + // Compute availability badges for cloud models based on account state. + badges := cloudAvailabilityBadges(ctx, client, options) + for i := range options { + options[i].AvailabilityBadge = badges[options[i].Name] + } + return options, nil } +// cloudAvailabilityBadges returns a map of model name → availability badge +// for cloud models that require sign-in or a plan upgrade. +func cloudAvailabilityBadges(ctx context.Context, client *api.Client, options []agentchat.ModelOption) map[string]string { + badges := make(map[string]string) + hasCloud := false + for _, opt := range options { + if opt.Cloud { + hasCloud = true + break + } + } + if !hasCloud { + return badges + } + + if disabled, known := agentCloudStatusDisabled(ctx, client); known && disabled { + return badges + } + + whoamiCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + user, err := client.Whoami(whoamiCtx) + if err != nil { + // Can't determine auth state — don't show badges. + return badges + } + + signedIn := user != nil && user.Name != "" + for _, opt := range options { + if !opt.Cloud { + continue + } + if !signedIn { + badges[opt.Name] = "Sign in required" + } else if opt.RequiredPlan != "" && !launch.PlanSatisfies(user.Plan, opt.RequiredPlan) { + badges[opt.Name] = "Upgrade required" + } + } + return badges +} + func agentRecommendationDescription(rec api.ModelRecommendation) string { var parts []string if description := strings.TrimSpace(rec.Description); description != "" { diff --git a/cmd/tui/chat/chat.go b/cmd/tui/chat/chat.go index b530c4b14..764ab21bf 100644 --- a/cmd/tui/chat/chat.go +++ b/cmd/tui/chat/chat.go @@ -36,11 +36,12 @@ var chatEmptyPrompts = []string{ } type ModelOption struct { - Name string - Description string - Recommended bool - RequiredPlan string - Cloud bool + Name string + Description string + Recommended bool + RequiredPlan string + Cloud bool + AvailabilityBadge string } type Options struct { diff --git a/cmd/tui/chat/modals.go b/cmd/tui/chat/modals.go index 028f5bca1..6da539baf 100644 --- a/cmd/tui/chat/modals.go +++ b/cmd/tui/chat/modals.go @@ -46,6 +46,7 @@ type chatPicker[T any] struct { fullFooter string itemTitle func(T) string itemMeta func(T) string + itemBadge func(T) string } type chatHistoryPopup struct { @@ -318,6 +319,7 @@ func newChatModelPicker(models []ModelOption, current, filter string) *chatModel picker.fullFooter = "↑/↓ move • enter switch • type search • esc cancel" picker.itemTitle = func(model ModelOption) string { return model.Name } picker.itemMeta = func(model ModelOption) string { return modelOptionMeta(model, current) } + picker.itemBadge = func(model ModelOption) string { return model.AvailabilityBadge } return picker } @@ -542,6 +544,7 @@ func newChatResumePicker(chats []appstore.ChatSummary, currentChatID string) *ch picker.fullFooter = "↑/↓ move • enter resume • type search • esc cancel" picker.itemTitle = resumeChatTitle picker.itemMeta = resumeChatMeta + picker.itemBadge = func(appstore.ChatSummary) string { return "" } return picker } @@ -915,11 +918,15 @@ func (p *chatPicker[T]) render(width int) string { for i := start; i < end; i++ { item := filtered[i] selected := i == p.cursor + badge := p.itemBadge(item) for j, line := range wrapChatText(p.itemTitle(item), max(10, width-2)) { marker := " " if selected && j == 0 { marker = "› " } + if j == 0 && badge != "" { + line += " " + chatResumeMetaStyle.Render("("+badge+")") + } if selected { b.WriteString(chatResumeSelectedStyle.Render(marker + line)) } else { @@ -974,11 +981,15 @@ func (p *chatPicker[T]) renderInline(width int) []string { for i := start; i < end; i++ { item := filtered[i] selected := i == p.cursor + badge := p.itemBadge(item) for j, line := range wrapChatText(p.itemTitle(item), max(10, width-2)) { marker := " " if selected && j == 0 { marker = "› " } + if j == 0 && badge != "" { + line += " " + chatResumeMetaStyle.Render("("+badge+")") + } if selected { lines = append(lines, truncateRenderedLine(chatResumeSelectedStyle.Render(marker+line), width)) } else { @@ -1006,9 +1017,6 @@ func modelOptionMeta(model ModelOption, current string) string { if model.Description != "" { parts = append(parts, model.Description) } - if model.RequiredPlan != "" { - parts = append(parts, model.RequiredPlan+" plan") - } return strings.Join(parts, " · ") }