From d17e489f4b7808fd69287fc46e772c48b74ca569 Mon Sep 17 00:00:00 2001 From: Artur Do Lago Date: Sun, 11 Jan 2026 01:54:24 +0100 Subject: [PATCH] feat(tui): add provider auth status indicators to model dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows ✗ for expired and △ for expiring tokens next to provider names in favorites, recent, and all providers sections. Co-Authored-By: Claude Opus 4.5 --- docs/ROADMAP.md | 21 ++++++------- .../cli/cmd/tui/component/dialog-model.tsx | 30 ++++++++++++++----- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f53e06ae8b1..f4b6d5be0a5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -126,8 +126,9 @@ Agent-core is a fork of OpenCode with three personas (Zee, Stanley, Johny) shari - [x] MemoryStore high-level API for save/search/list - [x] Collection management with auto-creation - [x] Embedding generation (`src/memory/embedding.ts`) - - [x] Local BGE-M3 via vLLM (1024d) - - [x] OpenAI fallback + - [x] **Qwen3-Embedding-8B via Nebius API** (4096d, #1 on MTEB) + - [x] Voyage AI provider support + - [x] OpenAI/local fallback options - [x] Conversation continuity across compactions (`src/personas/continuity.ts`) - [x] ContinuityManager with session lifecycle - [x] Key fact extraction (heuristic + LLM ready) @@ -137,7 +138,7 @@ Agent-core is a fork of OpenCode with three personas (Zee, Stanley, Johny) shari - [x] Namespace-based isolation - [x] Shared Qdrant collections - [x] Memory bridge for persona state (`src/personas/memory-bridge.ts`) -- [ ] Memory search MCP tool (not yet exposed via MCP) +- [x] Memory search MCP tool (`personas-memory` server) ### 3.2 Orchestration (Tiara) - COMPLETE - [x] Drone spawning via Task tool (`src/personas/tiara.ts`) @@ -158,21 +159,21 @@ Agent-core is a fork of OpenCode with three personas (Zee, Stanley, Johny) shari - [x] Session lifecycle hook for auto-extraction - [x] Heuristic and LLM-based extraction -### 3.3 MCP Servers +### 3.3 MCP Servers - COMPLETE - [x] Context7 integration -- [ ] Custom MCP for personas (not yet implemented) - - [ ] Memory MCP (expose memory search to external tools) - - [ ] Calendar MCP (expose calendar to external tools) - - [ ] Portfolio MCP (expose portfolio to external tools) +- [x] Custom MCP for personas (`src/mcp/servers/`) + - [x] Memory MCP (`personas-memory`) - store, search, list, delete, stats + - [x] Calendar MCP (`personas-calendar`) - events, create, update, delete, free-time + - [x] Portfolio MCP (`personas-portfolio`) - status, positions, market-data, SEC, backtest --- ## Phase 4: TUI Improvements -### 4.1 Model Selection - MOSTLY COMPLETE +### 4.1 Model Selection - COMPLETE - [x] Favorites system (`dialog-model.tsx` favorites support) - [x] Recently used models (`local.model.recent()`) -- [ ] Provider status indicators (auth valid/expired) +- [x] Provider status indicators (auth valid/expired) - shows ✗/△ in model dialog - [x] Cost display for non-free models (shows "Free" badge) ### 4.2 Agent Experience diff --git a/packages/agent-core/src/cli/cmd/tui/component/dialog-model.tsx b/packages/agent-core/src/cli/cmd/tui/component/dialog-model.tsx index 6283e6340ec..d2f0b612dc3 100644 --- a/packages/agent-core/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/agent-core/src/cli/cmd/tui/component/dialog-model.tsx @@ -8,6 +8,18 @@ import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { Keybind } from "@/util/keybind" import * as fuzzysort from "fuzzysort" +/** Get auth status indicator for a provider */ +function getAuthIndicator( + providerID: string, + authStatus: Record, +): string { + const status = authStatus[providerID] + if (!status) return "" + if (!status.valid) return "✗ " + if (status.expiringSoon) return "△ " + return "" +} + export function useConnected() { const sync = useSync() return createMemo(() => @@ -37,6 +49,7 @@ export function DialogModel(props: { providerID?: string }) { const showSections = showExtra() && needle.length === 0 const favorites = connected() ? local.model.favorite() : [] const recents = local.model.recent() + const authStatus = sync.data.provider_auth_status const recentList = showSections ? recents.filter( @@ -50,6 +63,7 @@ export function DialogModel(props: { providerID?: string }) { if (!provider) return [] const model = provider.models[item.modelID] if (!model) return [] + const authIndicator = getAuthIndicator(provider.id, authStatus) return [ { key: item, @@ -58,7 +72,7 @@ export function DialogModel(props: { providerID?: string }) { modelID: model.id, }, title: model.name ?? item.modelID, - description: provider.name, + description: authIndicator + provider.name, category: "Favorites", disabled: provider.id === "opencode" && model.id.includes("-nano"), footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, @@ -83,6 +97,7 @@ export function DialogModel(props: { providerID?: string }) { if (!provider) return [] const model = provider.models[item.modelID] if (!model) return [] + const authIndicator = getAuthIndicator(provider.id, authStatus) return [ { key: item, @@ -91,7 +106,7 @@ export function DialogModel(props: { providerID?: string }) { modelID: model.id, }, title: model.name ?? item.modelID, - description: provider.name, + description: authIndicator + provider.name, category: "Recent", disabled: provider.id === "opencode" && model.id.includes("-nano"), footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, @@ -116,8 +131,9 @@ export function DialogModel(props: { providerID?: string }) { (provider) => provider.id !== "opencode", (provider) => provider.name, ), - flatMap((provider) => - pipe( + flatMap((provider) => { + const authIndicator = getAuthIndicator(provider.id, authStatus) + return pipe( provider.models, entries(), filter(([_, info]) => info.status !== "deprecated"), @@ -130,7 +146,7 @@ export function DialogModel(props: { providerID?: string }) { return { value, title: info.name ?? model, - category: connected() ? provider.name : undefined, + category: connected() ? authIndicator + provider.name : undefined, disabled: provider.id === "opencode" && model.includes("-nano"), footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, onSelect() { @@ -159,8 +175,8 @@ export function DialogModel(props: { providerID?: string }) { (x) => x.footer !== "Free", (x) => x.title, ), - ), - ), + ) + }), ) const popularProviders = !connected()