feat: improve /connect command and /model selector UX (#665)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-24 12:06:16 -08:00
committed by GitHub
parent 9659fa32e8
commit 23c528a25b
7 changed files with 1014 additions and 37 deletions

View File

@@ -117,6 +117,7 @@ import { ModelSelector } from "./components/ModelSelector";
import { NewAgentDialog } from "./components/NewAgentDialog";
import { PendingApprovalStub } from "./components/PendingApprovalStub";
import { PinDialog, validateAgentName } from "./components/PinDialog";
import { ProviderSelector } from "./components/ProviderSelector";
// QuestionDialog removed - now using InlineQuestionApproval
import { ReasoningMessage } from "./components/ReasoningMessageRich";
@@ -978,6 +979,7 @@ export default function App({
| "mcp-connect"
| "help"
| "hooks"
| "connect"
| null;
const [activeOverlay, setActiveOverlay] = useState<ActiveOverlay>(null);
const [feedbackPrefill, setFeedbackPrefill] = useState("");
@@ -1053,6 +1055,40 @@ export default function App({
: null;
const currentModelProvider = llmConfig?.provider_name ?? null;
// Billing tier for conditional UI (fetched once on mount)
const [billingTier, setBillingTier] = useState<string | null>(null);
// Fetch billing tier once on mount
useEffect(() => {
(async () => {
try {
const settings = settingsManager.getSettings();
const baseURL =
process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL ||
"https://api.letta.com";
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
const response = await fetch(`${baseURL}/v1/metadata/balance`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-Letta-Source": "letta-code",
},
});
if (response.ok) {
const data = (await response.json()) as { billing_tier?: string };
if (data.billing_tier) {
setBillingTier(data.billing_tier);
}
}
} catch {
// Silently ignore - billing tier is optional context
}
})();
}, []);
// Token streaming preference (can be toggled at runtime)
const [tokenStreamingEnabled, setTokenStreamingEnabled] =
useState(tokenStreaming);
@@ -4707,11 +4743,14 @@ export default function App({
return { submitted: true };
}
// Special handling for /connect command - OAuth connection
if (msg.trim().startsWith("/connect")) {
// Handle all /connect commands through the unified handler
// For codex: uses local ChatGPT OAuth server (no dialog needed)
// For zai: requires API key as argument
// Special handling for /connect command - opens provider selector
if (msg.trim() === "/connect") {
setActiveOverlay("connect");
return { submitted: true };
}
// /connect codex - direct OAuth flow (kept for backwards compatibility)
if (msg.trim().startsWith("/connect codex")) {
const { handleConnect } = await import("./commands/connect");
await handleConnect(
{
@@ -9094,6 +9133,34 @@ Plan file path: ${planFilePath}`;
onCancel={closeOverlay}
filterProvider={modelSelectorOptions.filterProvider}
forceRefresh={modelSelectorOptions.forceRefresh}
billingTier={billingTier ?? undefined}
/>
)}
{/* Provider Selector - for connecting BYOK providers */}
{activeOverlay === "connect" && (
<ProviderSelector
onCancel={closeOverlay}
onStartOAuth={async () => {
// Close selector and start OAuth flow
closeOverlay();
const { handleConnect } = await import("./commands/connect");
await handleConnect(
{
buffersRef,
refreshDerived,
setCommandRunning,
onCodexConnected: () => {
setModelSelectorOptions({
filterProvider: "chatgpt-plus-pro",
forceRefresh: true,
});
setActiveOverlay("model");
},
},
"/connect codex",
);
}}
/>
)}