@mariozechner/pi-coding-agent 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.6] - 2025-12-02
6
+
7
+ ### Added
8
+
9
+ - **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))
10
+ - **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.
11
+
5
12
  ## [0.11.5] - 2025-12-01
6
13
 
7
14
  ### Added
@@ -33,4 +33,16 @@ export declare function findModel(provider: string, modelId: string): {
33
33
  model: Model<Api> | null;
34
34
  error: string | null;
35
35
  };
36
+ /**
37
+ * Invalidate the OAuth status cache.
38
+ * Call this after login/logout operations.
39
+ */
40
+ export declare function invalidateOAuthCache(): void;
41
+ /**
42
+ * Check if a model is using OAuth credentials (subscription).
43
+ * This checks if OAuth credentials exist and would be used for the model,
44
+ * without actually fetching or refreshing the token.
45
+ * Results are cached until invalidateOAuthCache() is called.
46
+ */
47
+ export declare function isModelUsingOAuth(model: Model<Api>): boolean;
36
48
  //# sourceMappingURL=model-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-config.d.ts","sourceRoot":"","sources":["../src/model-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAA0D,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AA8DnH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOnE;AAgID;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI;IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAmBnF;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0BtF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAgBlG;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAS/G","sourcesContent":["import { type Api, getApiKey, getModels, getProviders, type KnownProvider, type Model } from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getOAuthToken } from \"./oauth/index.js\";\n\n// Handle both default and named exports\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\ntype ProviderConfig = Static<typeof ProviderConfigSchema>;\ntype ModelDefinition = Static<typeof ModelDefinitionSchema>;\n\n// Custom provider API key mappings (provider name -> apiKey config)\nconst customProviderApiKeys: Map<string, string> = new Map();\n\n/**\n * Resolve an API key config value to an actual key.\n * First checks if it's an environment variable, then treats as literal.\n */\nexport function resolveApiKey(keyConfig: string): string | undefined {\n\t// First check if it's an env var name\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\n\t// Otherwise treat as literal API key\n\treturn keyConfig;\n}\n\n/**\n * Load custom models from ~/.pi/agent/models.json\n * Returns { models, error } - either models array or error message\n */\nfunction loadCustomModels(): { models: Model<Api>[]; error: string | null } {\n\tconst configPath = join(homedir(), \".pi\", \"agent\", \"models.json\");\n\tif (!existsSync(configPath)) {\n\t\treturn { models: [], error: null };\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(configPath, \"utf-8\");\n\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t// Validate schema\n\t\tconst ajv = new Ajv();\n\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\tif (!validate(config)) {\n\t\t\tconst errors =\n\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\"Unknown schema error\";\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Additional validation\n\t\ttry {\n\t\t\tvalidateConfig(config);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Parse models\n\t\treturn { models: parseModels(config), error: null };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tmodels: [],\n\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t};\n\t}\n}\n\n/**\n * Validate config structure and requirements\n */\nfunction validateConfig(config: ModelsConfig): void {\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. ` +\n\t\t\t\t\t\t`Set at provider or model level.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate required fields\n\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t}\n\t}\n}\n\n/**\n * Parse config into Model objects\n */\nfunction parseModels(config: ModelsConfig): Model<Api>[] {\n\tconst models: Model<Api>[] = [];\n\n\t// Clear and rebuild custom provider API key mappings\n\tcustomProviderApiKeys.clear();\n\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t// Store API key config for this provider\n\t\tcustomProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t// Model-level api overrides provider-level api\n\t\t\tconst api = modelDef.api || providerConfig.api;\n\n\t\t\tif (!api) {\n\t\t\t\t// This should have been caught by validateConfig, but be safe\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\tconst headers =\n\t\t\t\tproviderConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;\n\n\t\t\tmodels.push({\n\t\t\t\tid: modelDef.id,\n\t\t\t\tname: modelDef.name,\n\t\t\t\tapi: api as Api,\n\t\t\t\tprovider: providerName,\n\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\tcost: modelDef.cost,\n\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\theaders,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn models;\n}\n\n/**\n * Get all models (built-in + custom), freshly loaded\n * Returns { models, error } - either models array or error message\n */\nexport function loadAndMergeModels(): { models: Model<Api>[]; error: string | null } {\n\tconst builtInModels: Model<Api>[] = [];\n\tconst providers = getProviders();\n\n\t// Load all built-in models\n\tfor (const provider of providers) {\n\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t}\n\n\t// Load custom models\n\tconst { models: customModels, error } = loadCustomModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\t// Merge: custom models come after built-in\n\treturn { models: [...builtInModels, ...customModels], error: null };\n}\n\n/**\n * Get API key for a model (checks custom providers first, then built-in)\n * Now async to support OAuth token refresh\n */\nexport async function getApiKeyForModel(model: Model<Api>): Promise<string | undefined> {\n\t// For custom providers, check their apiKey config\n\tconst customKeyConfig = customProviderApiKeys.get(model.provider);\n\tif (customKeyConfig) {\n\t\treturn resolveApiKey(customKeyConfig);\n\t}\n\n\t// For Anthropic, check OAuth first\n\tif (model.provider === \"anthropic\") {\n\t\t// 1. Check OAuth storage (auto-refresh if needed)\n\t\tconst oauthToken = await getOAuthToken(\"anthropic\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)\n\t\tconst oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;\n\t\tif (oauthEnv) {\n\t\t\treturn oauthEnv;\n\t\t}\n\n\t\t// 3. Fall back to ANTHROPIC_API_KEY env var\n\t}\n\n\t// For built-in providers, use getApiKey from @mariozechner/pi-ai\n\treturn getApiKey(model.provider as KnownProvider);\n}\n\n/**\n * Get only models that have valid API keys available\n * Returns { models, error } - either models array or error message\n */\nexport async function getAvailableModels(): Promise<{ models: Model<Api>[]; error: string | null }> {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst availableModels: Model<Api>[] = [];\n\tfor (const model of allModels) {\n\t\tconst apiKey = await getApiKeyForModel(model);\n\t\tif (apiKey) {\n\t\t\tavailableModels.push(model);\n\t\t}\n\t}\n\n\treturn { models: availableModels, error: null };\n}\n\n/**\n * Find a specific model by provider and ID\n * Returns { model, error } - either model or error message\n */\nexport function findModel(provider: string, modelId: string): { model: Model<Api> | null; error: string | null } {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { model: null, error };\n\t}\n\n\tconst model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;\n\treturn { model, error: null };\n}\n"]}
1
+ {"version":3,"file":"model-config.d.ts","sourceRoot":"","sources":["../src/model-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAA0D,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AA+DnH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOnE;AAgID;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI;IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAmBnF;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0BtF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAgBlG;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAS/G;AAcD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAyB5D","sourcesContent":["import { type Api, getApiKey, getModels, getProviders, type KnownProvider, type Model } from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getOAuthToken, type SupportedOAuthProvider } from \"./oauth/index.js\";\nimport { loadOAuthCredentials } from \"./oauth/storage.js\";\n\n// Handle both default and named exports\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\ntype ProviderConfig = Static<typeof ProviderConfigSchema>;\ntype ModelDefinition = Static<typeof ModelDefinitionSchema>;\n\n// Custom provider API key mappings (provider name -> apiKey config)\nconst customProviderApiKeys: Map<string, string> = new Map();\n\n/**\n * Resolve an API key config value to an actual key.\n * First checks if it's an environment variable, then treats as literal.\n */\nexport function resolveApiKey(keyConfig: string): string | undefined {\n\t// First check if it's an env var name\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\n\t// Otherwise treat as literal API key\n\treturn keyConfig;\n}\n\n/**\n * Load custom models from ~/.pi/agent/models.json\n * Returns { models, error } - either models array or error message\n */\nfunction loadCustomModels(): { models: Model<Api>[]; error: string | null } {\n\tconst configPath = join(homedir(), \".pi\", \"agent\", \"models.json\");\n\tif (!existsSync(configPath)) {\n\t\treturn { models: [], error: null };\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(configPath, \"utf-8\");\n\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t// Validate schema\n\t\tconst ajv = new Ajv();\n\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\tif (!validate(config)) {\n\t\t\tconst errors =\n\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\"Unknown schema error\";\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Additional validation\n\t\ttry {\n\t\t\tvalidateConfig(config);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Parse models\n\t\treturn { models: parseModels(config), error: null };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tmodels: [],\n\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t};\n\t}\n}\n\n/**\n * Validate config structure and requirements\n */\nfunction validateConfig(config: ModelsConfig): void {\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. ` +\n\t\t\t\t\t\t`Set at provider or model level.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate required fields\n\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t}\n\t}\n}\n\n/**\n * Parse config into Model objects\n */\nfunction parseModels(config: ModelsConfig): Model<Api>[] {\n\tconst models: Model<Api>[] = [];\n\n\t// Clear and rebuild custom provider API key mappings\n\tcustomProviderApiKeys.clear();\n\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t// Store API key config for this provider\n\t\tcustomProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t// Model-level api overrides provider-level api\n\t\t\tconst api = modelDef.api || providerConfig.api;\n\n\t\t\tif (!api) {\n\t\t\t\t// This should have been caught by validateConfig, but be safe\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\tconst headers =\n\t\t\t\tproviderConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;\n\n\t\t\tmodels.push({\n\t\t\t\tid: modelDef.id,\n\t\t\t\tname: modelDef.name,\n\t\t\t\tapi: api as Api,\n\t\t\t\tprovider: providerName,\n\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\tcost: modelDef.cost,\n\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\theaders,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn models;\n}\n\n/**\n * Get all models (built-in + custom), freshly loaded\n * Returns { models, error } - either models array or error message\n */\nexport function loadAndMergeModels(): { models: Model<Api>[]; error: string | null } {\n\tconst builtInModels: Model<Api>[] = [];\n\tconst providers = getProviders();\n\n\t// Load all built-in models\n\tfor (const provider of providers) {\n\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t}\n\n\t// Load custom models\n\tconst { models: customModels, error } = loadCustomModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\t// Merge: custom models come after built-in\n\treturn { models: [...builtInModels, ...customModels], error: null };\n}\n\n/**\n * Get API key for a model (checks custom providers first, then built-in)\n * Now async to support OAuth token refresh\n */\nexport async function getApiKeyForModel(model: Model<Api>): Promise<string | undefined> {\n\t// For custom providers, check their apiKey config\n\tconst customKeyConfig = customProviderApiKeys.get(model.provider);\n\tif (customKeyConfig) {\n\t\treturn resolveApiKey(customKeyConfig);\n\t}\n\n\t// For Anthropic, check OAuth first\n\tif (model.provider === \"anthropic\") {\n\t\t// 1. Check OAuth storage (auto-refresh if needed)\n\t\tconst oauthToken = await getOAuthToken(\"anthropic\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)\n\t\tconst oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;\n\t\tif (oauthEnv) {\n\t\t\treturn oauthEnv;\n\t\t}\n\n\t\t// 3. Fall back to ANTHROPIC_API_KEY env var\n\t}\n\n\t// For built-in providers, use getApiKey from @mariozechner/pi-ai\n\treturn getApiKey(model.provider as KnownProvider);\n}\n\n/**\n * Get only models that have valid API keys available\n * Returns { models, error } - either models array or error message\n */\nexport async function getAvailableModels(): Promise<{ models: Model<Api>[]; error: string | null }> {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst availableModels: Model<Api>[] = [];\n\tfor (const model of allModels) {\n\t\tconst apiKey = await getApiKeyForModel(model);\n\t\tif (apiKey) {\n\t\t\tavailableModels.push(model);\n\t\t}\n\t}\n\n\treturn { models: availableModels, error: null };\n}\n\n/**\n * Find a specific model by provider and ID\n * Returns { model, error } - either model or error message\n */\nexport function findModel(provider: string, modelId: string): { model: Model<Api> | null; error: string | null } {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { model: null, error };\n\t}\n\n\tconst model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;\n\treturn { model, error: null };\n}\n\n/**\n * Mapping from model provider to OAuth provider ID.\n * Only providers that support OAuth are listed here.\n */\nconst providerToOAuthProvider: Record<string, SupportedOAuthProvider> = {\n\tanthropic: \"anthropic\",\n\t// Add more mappings as OAuth support is added for other providers\n};\n\n// Cache for OAuth status per provider (avoids file reads on every render)\nconst oauthStatusCache: Map<string, boolean> = new Map();\n\n/**\n * Invalidate the OAuth status cache.\n * Call this after login/logout operations.\n */\nexport function invalidateOAuthCache(): void {\n\toauthStatusCache.clear();\n}\n\n/**\n * Check if a model is using OAuth credentials (subscription).\n * This checks if OAuth credentials exist and would be used for the model,\n * without actually fetching or refreshing the token.\n * Results are cached until invalidateOAuthCache() is called.\n */\nexport function isModelUsingOAuth(model: Model<Api>): boolean {\n\tconst oauthProvider = providerToOAuthProvider[model.provider];\n\tif (!oauthProvider) {\n\t\treturn false;\n\t}\n\n\t// Check cache first\n\tif (oauthStatusCache.has(oauthProvider)) {\n\t\treturn oauthStatusCache.get(oauthProvider)!;\n\t}\n\n\t// Check if OAuth credentials exist for this provider\n\tlet usingOAuth = false;\n\tconst credentials = loadOAuthCredentials(oauthProvider);\n\tif (credentials) {\n\t\tusingOAuth = true;\n\t}\n\n\t// Also check for manual OAuth token env var (for Anthropic)\n\tif (!usingOAuth && model.provider === \"anthropic\" && process.env.ANTHROPIC_OAUTH_TOKEN) {\n\t\tusingOAuth = true;\n\t}\n\n\toauthStatusCache.set(oauthProvider, usingOAuth);\n\treturn usingOAuth;\n}\n"]}
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
7
  import { getOAuthToken } from "./oauth/index.js";
8
+ import { loadOAuthCredentials } from "./oauth/storage.js";
8
9
  // Handle both default and named exports
9
10
  const Ajv = AjvModule.default || AjvModule;
10
11
  // Schema for custom model definition
@@ -244,4 +245,49 @@ export function findModel(provider, modelId) {
244
245
  const model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;
245
246
  return { model, error: null };
246
247
  }
248
+ /**
249
+ * Mapping from model provider to OAuth provider ID.
250
+ * Only providers that support OAuth are listed here.
251
+ */
252
+ const providerToOAuthProvider = {
253
+ anthropic: "anthropic",
254
+ // Add more mappings as OAuth support is added for other providers
255
+ };
256
+ // Cache for OAuth status per provider (avoids file reads on every render)
257
+ const oauthStatusCache = new Map();
258
+ /**
259
+ * Invalidate the OAuth status cache.
260
+ * Call this after login/logout operations.
261
+ */
262
+ export function invalidateOAuthCache() {
263
+ oauthStatusCache.clear();
264
+ }
265
+ /**
266
+ * Check if a model is using OAuth credentials (subscription).
267
+ * This checks if OAuth credentials exist and would be used for the model,
268
+ * without actually fetching or refreshing the token.
269
+ * Results are cached until invalidateOAuthCache() is called.
270
+ */
271
+ export function isModelUsingOAuth(model) {
272
+ const oauthProvider = providerToOAuthProvider[model.provider];
273
+ if (!oauthProvider) {
274
+ return false;
275
+ }
276
+ // Check cache first
277
+ if (oauthStatusCache.has(oauthProvider)) {
278
+ return oauthStatusCache.get(oauthProvider);
279
+ }
280
+ // Check if OAuth credentials exist for this provider
281
+ let usingOAuth = false;
282
+ const credentials = loadOAuthCredentials(oauthProvider);
283
+ if (credentials) {
284
+ usingOAuth = true;
285
+ }
286
+ // Also check for manual OAuth token env var (for Anthropic)
287
+ if (!usingOAuth && model.provider === "anthropic" && process.env.ANTHROPIC_OAUTH_TOKEN) {
288
+ usingOAuth = true;
289
+ }
290
+ oauthStatusCache.set(oauthProvider, usingOAuth);
291
+ return usingOAuth;
292
+ }
247
293
  //# sourceMappingURL=model-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-config.js","sourceRoot":"","sources":["../src/model-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Api, getApiKey, getModels, getProviders, type KnownProvider, type Model } from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getOAuthToken } from \"./oauth/index.js\";\n\n// Handle both default and named exports\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\ntype ProviderConfig = Static<typeof ProviderConfigSchema>;\ntype ModelDefinition = Static<typeof ModelDefinitionSchema>;\n\n// Custom provider API key mappings (provider name -> apiKey config)\nconst customProviderApiKeys: Map<string, string> = new Map();\n\n/**\n * Resolve an API key config value to an actual key.\n * First checks if it's an environment variable, then treats as literal.\n */\nexport function resolveApiKey(keyConfig: string): string | undefined {\n\t// First check if it's an env var name\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\n\t// Otherwise treat as literal API key\n\treturn keyConfig;\n}\n\n/**\n * Load custom models from ~/.pi/agent/models.json\n * Returns { models, error } - either models array or error message\n */\nfunction loadCustomModels(): { models: Model<Api>[]; error: string | null } {\n\tconst configPath = join(homedir(), \".pi\", \"agent\", \"models.json\");\n\tif (!existsSync(configPath)) {\n\t\treturn { models: [], error: null };\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(configPath, \"utf-8\");\n\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t// Validate schema\n\t\tconst ajv = new Ajv();\n\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\tif (!validate(config)) {\n\t\t\tconst errors =\n\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\"Unknown schema error\";\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Additional validation\n\t\ttry {\n\t\t\tvalidateConfig(config);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Parse models\n\t\treturn { models: parseModels(config), error: null };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tmodels: [],\n\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t};\n\t}\n}\n\n/**\n * Validate config structure and requirements\n */\nfunction validateConfig(config: ModelsConfig): void {\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. ` +\n\t\t\t\t\t\t`Set at provider or model level.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate required fields\n\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t}\n\t}\n}\n\n/**\n * Parse config into Model objects\n */\nfunction parseModels(config: ModelsConfig): Model<Api>[] {\n\tconst models: Model<Api>[] = [];\n\n\t// Clear and rebuild custom provider API key mappings\n\tcustomProviderApiKeys.clear();\n\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t// Store API key config for this provider\n\t\tcustomProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t// Model-level api overrides provider-level api\n\t\t\tconst api = modelDef.api || providerConfig.api;\n\n\t\t\tif (!api) {\n\t\t\t\t// This should have been caught by validateConfig, but be safe\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\tconst headers =\n\t\t\t\tproviderConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;\n\n\t\t\tmodels.push({\n\t\t\t\tid: modelDef.id,\n\t\t\t\tname: modelDef.name,\n\t\t\t\tapi: api as Api,\n\t\t\t\tprovider: providerName,\n\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\tcost: modelDef.cost,\n\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\theaders,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn models;\n}\n\n/**\n * Get all models (built-in + custom), freshly loaded\n * Returns { models, error } - either models array or error message\n */\nexport function loadAndMergeModels(): { models: Model<Api>[]; error: string | null } {\n\tconst builtInModels: Model<Api>[] = [];\n\tconst providers = getProviders();\n\n\t// Load all built-in models\n\tfor (const provider of providers) {\n\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t}\n\n\t// Load custom models\n\tconst { models: customModels, error } = loadCustomModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\t// Merge: custom models come after built-in\n\treturn { models: [...builtInModels, ...customModels], error: null };\n}\n\n/**\n * Get API key for a model (checks custom providers first, then built-in)\n * Now async to support OAuth token refresh\n */\nexport async function getApiKeyForModel(model: Model<Api>): Promise<string | undefined> {\n\t// For custom providers, check their apiKey config\n\tconst customKeyConfig = customProviderApiKeys.get(model.provider);\n\tif (customKeyConfig) {\n\t\treturn resolveApiKey(customKeyConfig);\n\t}\n\n\t// For Anthropic, check OAuth first\n\tif (model.provider === \"anthropic\") {\n\t\t// 1. Check OAuth storage (auto-refresh if needed)\n\t\tconst oauthToken = await getOAuthToken(\"anthropic\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)\n\t\tconst oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;\n\t\tif (oauthEnv) {\n\t\t\treturn oauthEnv;\n\t\t}\n\n\t\t// 3. Fall back to ANTHROPIC_API_KEY env var\n\t}\n\n\t// For built-in providers, use getApiKey from @mariozechner/pi-ai\n\treturn getApiKey(model.provider as KnownProvider);\n}\n\n/**\n * Get only models that have valid API keys available\n * Returns { models, error } - either models array or error message\n */\nexport async function getAvailableModels(): Promise<{ models: Model<Api>[]; error: string | null }> {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst availableModels: Model<Api>[] = [];\n\tfor (const model of allModels) {\n\t\tconst apiKey = await getApiKeyForModel(model);\n\t\tif (apiKey) {\n\t\t\tavailableModels.push(model);\n\t\t}\n\t}\n\n\treturn { models: availableModels, error: null };\n}\n\n/**\n * Find a specific model by provider and ID\n * Returns { model, error } - either model or error message\n */\nexport function findModel(provider: string, modelId: string): { model: Model<Api> | null; error: string | null } {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { model: null, error };\n\t}\n\n\tconst model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;\n\treturn { model, error: null };\n}\n"]}
1
+ {"version":3,"file":"model-config.js","sourceRoot":"","sources":["../src/model-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Api, getApiKey, getModels, getProviders, type KnownProvider, type Model } from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getOAuthToken, type SupportedOAuthProvider } from \"./oauth/index.js\";\nimport { loadOAuthCredentials } from \"./oauth/storage.js\";\n\n// Handle both default and named exports\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\ntype ProviderConfig = Static<typeof ProviderConfigSchema>;\ntype ModelDefinition = Static<typeof ModelDefinitionSchema>;\n\n// Custom provider API key mappings (provider name -> apiKey config)\nconst customProviderApiKeys: Map<string, string> = new Map();\n\n/**\n * Resolve an API key config value to an actual key.\n * First checks if it's an environment variable, then treats as literal.\n */\nexport function resolveApiKey(keyConfig: string): string | undefined {\n\t// First check if it's an env var name\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\n\t// Otherwise treat as literal API key\n\treturn keyConfig;\n}\n\n/**\n * Load custom models from ~/.pi/agent/models.json\n * Returns { models, error } - either models array or error message\n */\nfunction loadCustomModels(): { models: Model<Api>[]; error: string | null } {\n\tconst configPath = join(homedir(), \".pi\", \"agent\", \"models.json\");\n\tif (!existsSync(configPath)) {\n\t\treturn { models: [], error: null };\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(configPath, \"utf-8\");\n\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t// Validate schema\n\t\tconst ajv = new Ajv();\n\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\tif (!validate(config)) {\n\t\t\tconst errors =\n\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\"Unknown schema error\";\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Additional validation\n\t\ttry {\n\t\t\tvalidateConfig(config);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Parse models\n\t\treturn { models: parseModels(config), error: null };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tmodels: [],\n\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t};\n\t}\n}\n\n/**\n * Validate config structure and requirements\n */\nfunction validateConfig(config: ModelsConfig): void {\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. ` +\n\t\t\t\t\t\t`Set at provider or model level.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate required fields\n\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t}\n\t}\n}\n\n/**\n * Parse config into Model objects\n */\nfunction parseModels(config: ModelsConfig): Model<Api>[] {\n\tconst models: Model<Api>[] = [];\n\n\t// Clear and rebuild custom provider API key mappings\n\tcustomProviderApiKeys.clear();\n\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t// Store API key config for this provider\n\t\tcustomProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t// Model-level api overrides provider-level api\n\t\t\tconst api = modelDef.api || providerConfig.api;\n\n\t\t\tif (!api) {\n\t\t\t\t// This should have been caught by validateConfig, but be safe\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\tconst headers =\n\t\t\t\tproviderConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;\n\n\t\t\tmodels.push({\n\t\t\t\tid: modelDef.id,\n\t\t\t\tname: modelDef.name,\n\t\t\t\tapi: api as Api,\n\t\t\t\tprovider: providerName,\n\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\tcost: modelDef.cost,\n\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\theaders,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn models;\n}\n\n/**\n * Get all models (built-in + custom), freshly loaded\n * Returns { models, error } - either models array or error message\n */\nexport function loadAndMergeModels(): { models: Model<Api>[]; error: string | null } {\n\tconst builtInModels: Model<Api>[] = [];\n\tconst providers = getProviders();\n\n\t// Load all built-in models\n\tfor (const provider of providers) {\n\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t}\n\n\t// Load custom models\n\tconst { models: customModels, error } = loadCustomModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\t// Merge: custom models come after built-in\n\treturn { models: [...builtInModels, ...customModels], error: null };\n}\n\n/**\n * Get API key for a model (checks custom providers first, then built-in)\n * Now async to support OAuth token refresh\n */\nexport async function getApiKeyForModel(model: Model<Api>): Promise<string | undefined> {\n\t// For custom providers, check their apiKey config\n\tconst customKeyConfig = customProviderApiKeys.get(model.provider);\n\tif (customKeyConfig) {\n\t\treturn resolveApiKey(customKeyConfig);\n\t}\n\n\t// For Anthropic, check OAuth first\n\tif (model.provider === \"anthropic\") {\n\t\t// 1. Check OAuth storage (auto-refresh if needed)\n\t\tconst oauthToken = await getOAuthToken(\"anthropic\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)\n\t\tconst oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;\n\t\tif (oauthEnv) {\n\t\t\treturn oauthEnv;\n\t\t}\n\n\t\t// 3. Fall back to ANTHROPIC_API_KEY env var\n\t}\n\n\t// For built-in providers, use getApiKey from @mariozechner/pi-ai\n\treturn getApiKey(model.provider as KnownProvider);\n}\n\n/**\n * Get only models that have valid API keys available\n * Returns { models, error } - either models array or error message\n */\nexport async function getAvailableModels(): Promise<{ models: Model<Api>[]; error: string | null }> {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst availableModels: Model<Api>[] = [];\n\tfor (const model of allModels) {\n\t\tconst apiKey = await getApiKeyForModel(model);\n\t\tif (apiKey) {\n\t\t\tavailableModels.push(model);\n\t\t}\n\t}\n\n\treturn { models: availableModels, error: null };\n}\n\n/**\n * Find a specific model by provider and ID\n * Returns { model, error } - either model or error message\n */\nexport function findModel(provider: string, modelId: string): { model: Model<Api> | null; error: string | null } {\n\tconst { models: allModels, error } = loadAndMergeModels();\n\n\tif (error) {\n\t\treturn { model: null, error };\n\t}\n\n\tconst model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;\n\treturn { model, error: null };\n}\n\n/**\n * Mapping from model provider to OAuth provider ID.\n * Only providers that support OAuth are listed here.\n */\nconst providerToOAuthProvider: Record<string, SupportedOAuthProvider> = {\n\tanthropic: \"anthropic\",\n\t// Add more mappings as OAuth support is added for other providers\n};\n\n// Cache for OAuth status per provider (avoids file reads on every render)\nconst oauthStatusCache: Map<string, boolean> = new Map();\n\n/**\n * Invalidate the OAuth status cache.\n * Call this after login/logout operations.\n */\nexport function invalidateOAuthCache(): void {\n\toauthStatusCache.clear();\n}\n\n/**\n * Check if a model is using OAuth credentials (subscription).\n * This checks if OAuth credentials exist and would be used for the model,\n * without actually fetching or refreshing the token.\n * Results are cached until invalidateOAuthCache() is called.\n */\nexport function isModelUsingOAuth(model: Model<Api>): boolean {\n\tconst oauthProvider = providerToOAuthProvider[model.provider];\n\tif (!oauthProvider) {\n\t\treturn false;\n\t}\n\n\t// Check cache first\n\tif (oauthStatusCache.has(oauthProvider)) {\n\t\treturn oauthStatusCache.get(oauthProvider)!;\n\t}\n\n\t// Check if OAuth credentials exist for this provider\n\tlet usingOAuth = false;\n\tconst credentials = loadOAuthCredentials(oauthProvider);\n\tif (credentials) {\n\t\tusingOAuth = true;\n\t}\n\n\t// Also check for manual OAuth token env var (for Anthropic)\n\tif (!usingOAuth && model.provider === \"anthropic\" && process.env.ANTHROPIC_OAUTH_TOKEN) {\n\t\tusingOAuth = true;\n\t}\n\n\toauthStatusCache.set(oauthProvider, usingOAuth);\n\treturn usingOAuth;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../src/tui/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAKpE;;GAEG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,KAAK,EAAE,UAAU,EAE5B;IAED;;;OAGG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,IAAI,GAAG,IAAI,CAG5C;IAED,OAAO,CAAC,eAAe;IAwBvB;;OAEG;IACH,OAAO,IAAI,IAAI,CAKd;IAED,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAEnC;IAED,UAAU,IAAI,IAAI,CAGjB;IAED;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAyBxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+H9B;CACD","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { join } from \"path\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\n\tconstructor(state: AgentState) {\n\t\tthis.state = state;\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\tif (!existsSync(gitHeadPath)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return (count / 1000).toFixed(1) + \"k\";\n\t\t\treturn Math.round(count / 1000) + \"k\";\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = \"~\" + pwd.slice(home.length);\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tconst maxPathLength = Math.max(20, width - 10); // Leave some margin\n\t\tif (pwd.length > maxPathLength) {\n\t\t\tconst start = pwd.slice(0, Math.floor(maxPathLength / 2) - 2);\n\t\t\tconst end = pwd.slice(-(Math.floor(maxPathLength / 2) - 1));\n\t\t\tpwd = `${start}...${end}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\t\tif (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`);\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", `${contextPercent}%`);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", `${contextPercent}%`);\n\t\t} else {\n\t\t\tcontextPercentStr = `${contextPercent}%`;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tconst statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tconst statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"dim\", pwd), theme.fg(\"dim\", statsLine)];\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../src/tui/footer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAMpE;;GAEG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,KAAK,EAAE,UAAU,EAE5B;IAED;;;OAGG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,IAAI,GAAG,IAAI,CAG5C;IAED,OAAO,CAAC,eAAe;IAwBvB;;OAEG;IACH,OAAO,IAAI,IAAI,CAKd;IAED,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAEnC;IAED,UAAU,IAAI,IAAI,CAGjB;IAED;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAyBxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqI9B;CACD","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { join } from \"path\";\nimport { isModelUsingOAuth } from \"../model-config.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\n\tconstructor(state: AgentState) {\n\t\tthis.state = state;\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\tif (!existsSync(gitHeadPath)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return (count / 1000).toFixed(1) + \"k\";\n\t\t\treturn Math.round(count / 1000) + \"k\";\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = \"~\" + pwd.slice(home.length);\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tconst maxPathLength = Math.max(20, width - 10); // Leave some margin\n\t\tif (pwd.length > maxPathLength) {\n\t\t\tconst start = pwd.slice(0, Math.floor(maxPathLength / 2) - 2);\n\t\t\tconst end = pwd.slice(-(Math.floor(maxPathLength / 2) - 1));\n\t\t\tpwd = `${start}...${end}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = this.state.model ? isModelUsingOAuth(this.state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", `${contextPercent}%`);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", `${contextPercent}%`);\n\t\t} else {\n\t\t\tcontextPercentStr = `${contextPercent}%`;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tconst statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tconst statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"dim\", pwd), theme.fg(\"dim\", statsLine)];\n\t}\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { visibleWidth } from "@mariozechner/pi-tui";
2
2
  import { existsSync, readFileSync, watch } from "fs";
3
3
  import { join } from "path";
4
+ import { isModelUsingOAuth } from "../model-config.js";
4
5
  import { theme } from "../theme/theme.js";
5
6
  /**
6
7
  * Footer component that shows pwd, token stats, and context usage
@@ -154,8 +155,12 @@ export class FooterComponent {
154
155
  statsParts.push(`R${formatTokens(totalCacheRead)}`);
155
156
  if (totalCacheWrite)
156
157
  statsParts.push(`W${formatTokens(totalCacheWrite)}`);
157
- if (totalCost)
158
- statsParts.push(`$${totalCost.toFixed(3)}`);
158
+ // Show cost with "(sub)" indicator if using OAuth subscription
159
+ const usingSubscription = this.state.model ? isModelUsingOAuth(this.state.model) : false;
160
+ if (totalCost || usingSubscription) {
161
+ const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
162
+ statsParts.push(costStr);
163
+ }
159
164
  // Colorize context percentage based on usage
160
165
  let contextPercentStr;
161
166
  if (contextPercentValue > 90) {
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../src/tui/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { join } from \"path\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\n\tconstructor(state: AgentState) {\n\t\tthis.state = state;\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\tif (!existsSync(gitHeadPath)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return (count / 1000).toFixed(1) + \"k\";\n\t\t\treturn Math.round(count / 1000) + \"k\";\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = \"~\" + pwd.slice(home.length);\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tconst maxPathLength = Math.max(20, width - 10); // Leave some margin\n\t\tif (pwd.length > maxPathLength) {\n\t\t\tconst start = pwd.slice(0, Math.floor(maxPathLength / 2) - 2);\n\t\t\tconst end = pwd.slice(-(Math.floor(maxPathLength / 2) - 1));\n\t\t\tpwd = `${start}...${end}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\t\tif (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`);\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", `${contextPercent}%`);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", `${contextPercent}%`);\n\t\t} else {\n\t\t\tcontextPercentStr = `${contextPercent}%`;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tconst statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tconst statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"dim\", pwd), theme.fg(\"dim\", statsLine)];\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../src/tui/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport { type Component, visibleWidth } from \"@mariozechner/pi-tui\";\nimport { existsSync, type FSWatcher, readFileSync, watch } from \"fs\";\nimport { join } from \"path\";\nimport { isModelUsingOAuth } from \"../model-config.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Footer component that shows pwd, token stats, and context usage\n */\nexport class FooterComponent implements Component {\n\tprivate state: AgentState;\n\tprivate cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name\n\tprivate gitWatcher: FSWatcher | null = null;\n\tprivate onBranchChange: (() => void) | null = null;\n\n\tconstructor(state: AgentState) {\n\t\tthis.state = state;\n\t}\n\n\t/**\n\t * Set up a file watcher on .git/HEAD to detect branch changes.\n\t * Call the provided callback when branch changes.\n\t */\n\twatchBranch(onBranchChange: () => void): void {\n\t\tthis.onBranchChange = onBranchChange;\n\t\tthis.setupGitWatcher();\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\t// Clean up existing watcher\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\n\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\tif (!existsSync(gitHeadPath)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.gitWatcher = watch(gitHeadPath, () => {\n\t\t\t\tthis.cachedBranch = undefined; // Invalidate cache\n\t\t\t\tif (this.onBranchChange) {\n\t\t\t\t\tthis.onBranchChange();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\t}\n\n\t/**\n\t * Clean up the file watcher\n\t */\n\tdispose(): void {\n\t\tif (this.gitWatcher) {\n\t\t\tthis.gitWatcher.close();\n\t\t\tthis.gitWatcher = null;\n\t\t}\n\t}\n\n\tupdateState(state: AgentState): void {\n\t\tthis.state = state;\n\t}\n\n\tinvalidate(): void {\n\t\t// Invalidate cached branch so it gets re-read on next render\n\t\tthis.cachedBranch = undefined;\n\t}\n\n\t/**\n\t * Get current git branch by reading .git/HEAD directly.\n\t * Returns null if not in a git repo, branch name otherwise.\n\t */\n\tprivate getCurrentBranch(): string | null {\n\t\t// Return cached value if available\n\t\tif (this.cachedBranch !== undefined) {\n\t\t\treturn this.cachedBranch;\n\t\t}\n\n\t\ttry {\n\t\t\tconst gitHeadPath = join(process.cwd(), \".git\", \"HEAD\");\n\t\t\tconst content = readFileSync(gitHeadPath, \"utf8\").trim();\n\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\t// Normal branch: extract branch name\n\t\t\t\tthis.cachedBranch = content.slice(16);\n\t\t\t} else {\n\t\t\t\t// Detached HEAD state\n\t\t\t\tthis.cachedBranch = \"detached\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not in a git repo or error reading file\n\t\t\tthis.cachedBranch = null;\n\t\t}\n\n\t\treturn this.cachedBranch;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Calculate cumulative usage from all assistant messages\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of this.state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Get last assistant message for context percentage calculation (skip aborted messages)\n\t\tconst lastAssistantMessage = this.state.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as AssistantMessage | undefined;\n\n\t\t// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)\n\t\tconst contextTokens = lastAssistantMessage\n\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t: 0;\n\t\tconst contextWindow = this.state.model?.contextWindow || 0;\n\t\tconst contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;\n\t\tconst contextPercent = contextPercentValue.toFixed(1);\n\n\t\t// Format token counts (similar to web-ui)\n\t\tconst formatTokens = (count: number): string => {\n\t\t\tif (count < 1000) return count.toString();\n\t\t\tif (count < 10000) return (count / 1000).toFixed(1) + \"k\";\n\t\t\treturn Math.round(count / 1000) + \"k\";\n\t\t};\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = \"~\" + pwd.slice(home.length);\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.getCurrentBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Truncate path if too long to fit width\n\t\tconst maxPathLength = Math.max(20, width - 10); // Leave some margin\n\t\tif (pwd.length > maxPathLength) {\n\t\t\tconst start = pwd.slice(0, Math.floor(maxPathLength / 2) - 2);\n\t\t\tconst end = pwd.slice(-(Math.floor(maxPathLength / 2) - 1));\n\t\t\tpwd = `${start}...${end}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = this.state.model ? isModelUsingOAuth(this.state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", `${contextPercent}%`);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", `${contextPercent}%`);\n\t\t} else {\n\t\t\tcontextPercentStr = `${contextPercent}%`;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tconst statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = this.state.model?.id || \"no-model\";\n\n\t\t// Add thinking level hint if model supports reasoning and thinking is enabled\n\t\tlet rightSide = modelName;\n\t\tif (this.state.model?.reasoning) {\n\t\t\tconst thinkingLevel = this.state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel !== \"off\") {\n\t\t\t\trightSide = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\tconst statsLeftWidth = visibleWidth(statsLeft);\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 3) {\n\t\t\t\t// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)\n\t\t\t\tconst plainRightSide = rightSide.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\t\t\t\tconst truncatedPlain = plainRightSide.substring(0, availableForRight);\n\t\t\t\t// For simplicity, just use plain truncated version (loses color, but fits)\n\t\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - truncatedPlain.length);\n\t\t\t\tstatsLine = statsLeft + padding + truncatedPlain;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Return two lines: pwd and stats\n\t\treturn [theme.fg(\"dim\", pwd), theme.fg(\"dim\", statsLine)];\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-selector.d.ts","sourceRoot":"","sources":["../../src/tui/oauth-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAK/D;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YAAY,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA8BjG;IAED,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,UAAU;IA+BlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAsBjC;CACD","sourcesContent":["import { Container, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../oauth/index.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new Text(theme.bold(title), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t\tthis.allProviders = this.allProviders.filter((p) => p.available);\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"oauth-selector.d.ts","sourceRoot":"","sources":["../../src/tui/oauth-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyB,MAAM,sBAAsB,CAAC;AAMxE;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IAErC,YAAY,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,EA8BjG;IAED,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,UAAU;IAoClB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAsBjC;CACD","sourcesContent":["import { Container, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../oauth/index.js\";\nimport { loadOAuthCredentials } from \"../oauth/storage.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new TruncatedText(theme.bold(title)));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t\tthis.allProviders = this.allProviders.filter((p) => p.available);\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\t// Check if user is logged in for this provider\n\t\t\tconst credentials = loadOAuthCredentials(provider.id);\n\t\t\tconst isLoggedIn = credentials !== null;\n\t\t\tconst statusIndicator = isLoggedIn ? theme.fg(\"success\", \" ✓ logged in\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text + statusIndicator;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text + statusIndicator;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new TruncatedText(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new TruncatedText(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
@@ -1,5 +1,6 @@
1
- import { Container, Spacer, Text } from "@mariozechner/pi-tui";
1
+ import { Container, Spacer, TruncatedText } from "@mariozechner/pi-tui";
2
2
  import { getOAuthProviders } from "../oauth/index.js";
3
+ import { loadOAuthCredentials } from "../oauth/storage.js";
3
4
  import { theme } from "../theme/theme.js";
4
5
  import { DynamicBorder } from "./dynamic-border.js";
5
6
  /**
@@ -24,7 +25,7 @@ export class OAuthSelectorComponent extends Container {
24
25
  this.addChild(new Spacer(1));
25
26
  // Add title
26
27
  const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
27
- this.addChild(new Text(theme.bold(title), 0, 0));
28
+ this.addChild(new TruncatedText(theme.bold(title)));
28
29
  this.addChild(new Spacer(1));
29
30
  // Create list container
30
31
  this.listContainer = new Container();
@@ -47,22 +48,26 @@ export class OAuthSelectorComponent extends Container {
47
48
  continue;
48
49
  const isSelected = i === this.selectedIndex;
49
50
  const isAvailable = provider.available;
51
+ // Check if user is logged in for this provider
52
+ const credentials = loadOAuthCredentials(provider.id);
53
+ const isLoggedIn = credentials !== null;
54
+ const statusIndicator = isLoggedIn ? theme.fg("success", " ✓ logged in") : "";
50
55
  let line = "";
51
56
  if (isSelected) {
52
57
  const prefix = theme.fg("accent", "→ ");
53
58
  const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
54
- line = prefix + text;
59
+ line = prefix + text + statusIndicator;
55
60
  }
56
61
  else {
57
62
  const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
58
- line = text;
63
+ line = text + statusIndicator;
59
64
  }
60
- this.listContainer.addChild(new Text(line, 0, 0));
65
+ this.listContainer.addChild(new TruncatedText(line, 0, 0));
61
66
  }
62
67
  // Show "no providers" if empty
63
68
  if (this.allProviders.length === 0) {
64
69
  const message = this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
65
- this.listContainer.addChild(new Text(theme.fg("muted", ` ${message}`), 0, 0));
70
+ this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
66
71
  }
67
72
  }
68
73
  handleInput(keyData) {
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-selector.js","sourceRoot":"","sources":["../../src/tui/oauth-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, Spacer, Text } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../oauth/index.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new Text(theme.bold(title), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t\tthis.allProviders = this.allProviders.filter((p) => p.available);\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"oauth-selector.js","sourceRoot":"","sources":["../../src/tui/oauth-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, Spacer, TruncatedText } from \"@mariozechner/pi-tui\";\nimport { getOAuthProviders, type OAuthProviderInfo } from \"../oauth/index.js\";\nimport { loadOAuthCredentials } from \"../oauth/storage.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n/**\n * Component that renders an OAuth provider selector\n */\nexport class OAuthSelectorComponent extends Container {\n\tprivate listContainer: Container;\n\tprivate allProviders: OAuthProviderInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate mode: \"login\" | \"logout\";\n\tprivate onSelectCallback: (providerId: string) => void;\n\tprivate onCancelCallback: () => void;\n\n\tconstructor(mode: \"login\" | \"logout\", onSelect: (providerId: string) => void, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.mode = mode;\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Load all OAuth providers\n\t\tthis.loadProviders();\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add title\n\t\tconst title = mode === \"login\" ? \"Select provider to login:\" : \"Select provider to logout:\";\n\t\tthis.addChild(new TruncatedText(theme.bold(title)));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Initial render\n\t\tthis.updateList();\n\t}\n\n\tprivate loadProviders(): void {\n\t\tthis.allProviders = getOAuthProviders();\n\t\tthis.allProviders = this.allProviders.filter((p) => p.available);\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tfor (let i = 0; i < this.allProviders.length; i++) {\n\t\t\tconst provider = this.allProviders[i];\n\t\t\tif (!provider) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isAvailable = provider.available;\n\n\t\t\t// Check if user is logged in for this provider\n\t\t\tconst credentials = loadOAuthCredentials(provider.id);\n\t\t\tconst isLoggedIn = credentials !== null;\n\t\t\tconst statusIndicator = isLoggedIn ? theme.fg(\"success\", \" ✓ logged in\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst text = isAvailable ? theme.fg(\"accent\", provider.name) : theme.fg(\"dim\", provider.name);\n\t\t\t\tline = prefix + text + statusIndicator;\n\t\t\t} else {\n\t\t\t\tconst text = isAvailable ? ` ${provider.name}` : theme.fg(\"dim\", ` ${provider.name}`);\n\t\t\t\tline = text + statusIndicator;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new TruncatedText(line, 0, 0));\n\t\t}\n\n\t\t// Show \"no providers\" if empty\n\t\tif (this.allProviders.length === 0) {\n\t\t\tconst message =\n\t\t\t\tthis.mode === \"login\" ? \"No OAuth providers available\" : \"No OAuth providers logged in. Use /login first.\";\n\t\t\tthis.listContainer.addChild(new TruncatedText(theme.fg(\"muted\", ` ${message}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\t// Up arrow\n\t\tif (keyData === \"\\x1b[A\") {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow\n\t\telse if (keyData === \"\\x1b[B\") {\n\t\t\tthis.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (keyData === \"\\r\") {\n\t\t\tconst selectedProvider = this.allProviders[this.selectedIndex];\n\t\t\tif (selectedProvider?.available) {\n\t\t\t\tthis.onSelectCallback(selectedProvider.id);\n\t\t\t}\n\t\t}\n\t\t// Escape\n\t\telse if (keyData === \"\\x1b\") {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t}\n}\n"]}