diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a73bdf13..6f70c6e1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - **OAuth Login Status Indicator**: The `/login` provider selector now shows "✓ logged in" next to providers where you're already authenticated. This makes it clear at a glance whether you're using your Claude Pro/Max subscription. ([#88](https://github.com/badlogic/pi-mono/pull/88) by [@steipete](https://github.com/steipete)) +- **Subscription Cost Indicator**: The footer now shows "(sub)" next to the cost when using an OAuth subscription (e.g., `$0.123 (sub)`). This makes it visible without needing `/login` that you're using your Claude Pro/Max subscription. ## [0.11.5] - 2025-12-01 diff --git a/packages/coding-agent/src/model-config.ts b/packages/coding-agent/src/model-config.ts index 36a5e355..62248dbe 100644 --- a/packages/coding-agent/src/model-config.ts +++ b/packages/coding-agent/src/model-config.ts @@ -4,7 +4,8 @@ import AjvModule from "ajv"; import { existsSync, readFileSync } from "fs"; import { homedir } from "os"; import { join } from "path"; -import { getOAuthToken } from "./oauth/index.js"; +import { getOAuthToken, type SupportedOAuthProvider } from "./oauth/index.js"; +import { loadOAuthCredentials } from "./oauth/storage.js"; // Handle both default and named exports const Ajv = (AjvModule as any).default || AjvModule; @@ -292,3 +293,56 @@ export function findModel(provider: string, modelId: string): { model: Model m.provider === provider && m.id === modelId) || null; return { model, error: null }; } + +/** + * Mapping from model provider to OAuth provider ID. + * Only providers that support OAuth are listed here. + */ +const providerToOAuthProvider: Record = { + anthropic: "anthropic", + // Add more mappings as OAuth support is added for other providers +}; + +// Cache for OAuth status per provider (avoids file reads on every render) +const oauthStatusCache: Map = new Map(); + +/** + * Invalidate the OAuth status cache. + * Call this after login/logout operations. + */ +export function invalidateOAuthCache(): void { + oauthStatusCache.clear(); +} + +/** + * Check if a model is using OAuth credentials (subscription). + * This checks if OAuth credentials exist and would be used for the model, + * without actually fetching or refreshing the token. + * Results are cached until invalidateOAuthCache() is called. + */ +export function isModelUsingOAuth(model: Model): boolean { + const oauthProvider = providerToOAuthProvider[model.provider]; + if (!oauthProvider) { + return false; + } + + // Check cache first + if (oauthStatusCache.has(oauthProvider)) { + return oauthStatusCache.get(oauthProvider)!; + } + + // Check if OAuth credentials exist for this provider + let usingOAuth = false; + const credentials = loadOAuthCredentials(oauthProvider); + if (credentials) { + usingOAuth = true; + } + + // Also check for manual OAuth token env var (for Anthropic) + if (!usingOAuth && model.provider === "anthropic" && process.env.ANTHROPIC_OAUTH_TOKEN) { + usingOAuth = true; + } + + oauthStatusCache.set(oauthProvider, usingOAuth); + return usingOAuth; +} diff --git a/packages/coding-agent/src/tui/footer.ts b/packages/coding-agent/src/tui/footer.ts index 05e7bf31..cb2bb497 100644 --- a/packages/coding-agent/src/tui/footer.ts +++ b/packages/coding-agent/src/tui/footer.ts @@ -3,6 +3,7 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { type Component, visibleWidth } from "@mariozechner/pi-tui"; import { existsSync, type FSWatcher, readFileSync, watch } from "fs"; import { join } from "path"; +import { isModelUsingOAuth } from "../model-config.js"; import { theme } from "../theme/theme.js"; /** @@ -169,7 +170,13 @@ export class FooterComponent implements Component { if (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`); if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`); if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`); - if (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`); + + // Show cost with "(sub)" indicator if using OAuth subscription + const usingSubscription = this.state.model ? isModelUsingOAuth(this.state.model) : false; + if (totalCost || usingSubscription) { + const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`; + statsParts.push(costStr); + } // Colorize context percentage based on usage let contextPercentStr: string; diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index c0bdb16d..98475b94 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -17,7 +17,7 @@ import { import { exec } from "child_process"; import { getChangelogPath, parseChangelog } from "../changelog.js"; import { exportSessionToHtml } from "../export-html.js"; -import { getApiKeyForModel, getAvailableModels } from "../model-config.js"; +import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js"; import { listOAuthProviders, login, logout } from "../oauth/index.js"; import type { SessionManager } from "../session-manager.js"; import type { SettingsManager } from "../settings-manager.js"; @@ -1318,7 +1318,8 @@ export class TuiRenderer { }, ); - // Success + // Success - invalidate OAuth cache so footer updates + invalidateOAuthCache(); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild( new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0), @@ -1335,6 +1336,8 @@ export class TuiRenderer { try { await logout(providerId); + // Invalidate OAuth cache so footer updates + invalidateOAuthCache(); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild( new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0),