From 91818021a5b09bd502533741eab8c28322c4c0cf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 12:55:17 +0000 Subject: [PATCH 1/3] Add API key URL links to settings UI When no API key is configured, display the provider's console URL (console.anthropic.com or platform.openai.com) next to the provider name. Pressing Tab on a provider without a key opens the URL in the browser. --- src/app.ts | 10 +++++++++ src/components/SettingsPanel.ts | 39 ++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/app.ts b/src/app.ts index 330c05b..3cf7858 100644 --- a/src/app.ts +++ b/src/app.ts @@ -68,6 +68,7 @@ import { navigateSettings, selectSettingsItem, goBackInSettings, + getSelectedProviderUrl, type SettingsState, } from "./components/SettingsPanel"; @@ -363,6 +364,15 @@ export class HackerNewsApp { } else if (key.name === "k" || key.name === "up") { navigateSettings(this.settingsState, -1, getConfiguredProvider() || "anthropic"); this.rerenderSettings(); + } else if (key.name === "tab") { + // Open API key URL if a provider without a key is selected + const url = getSelectedProviderUrl( + this.settingsState, + getConfiguredProvider() || "anthropic", + ); + if (url) { + this.callbacks.onOpenUrl?.(url); + } } else if (key.name === "return" || key.name === "enter") { const action = selectSettingsItem( this.settingsState, diff --git a/src/components/SettingsPanel.ts b/src/components/SettingsPanel.ts index 57155ca..07a740c 100644 --- a/src/components/SettingsPanel.ts +++ b/src/components/SettingsPanel.ts @@ -138,9 +138,12 @@ export function renderSettings( itemBox.add(label); if (!hasKey) { + const apiKeyUrl = listItem.item.provider === "anthropic" + ? "console.anthropic.com" + : "platform.openai.com"; const hint = new TextRenderable(ctx, { - content: "Add API Key", - fg: COLORS.textSecondary, + content: apiKeyUrl, + fg: COLORS.textTertiary, }); itemBox.add(hint); } @@ -233,8 +236,17 @@ export function renderSettings( container.add(new BoxRenderable(ctx, { height: 2 })); + // Check if any provider is missing a key (to show Tab hint) + const hasAnthropic = !!getApiKey("anthropic"); + const hasOpenAI = !!getApiKey("openai"); + const showTabHint = !hasAnthropic || !hasOpenAI; + + const hintText = showTabHint + ? "↑/↓ navigate Enter select Tab open URL Esc back" + : "↑/↓ navigate Enter select Esc back"; + const hint = new TextRenderable(ctx, { - content: "↑/↓ navigate Enter select Esc back", + content: hintText, fg: COLORS.textSecondary, }); container.add(hint); @@ -305,3 +317,24 @@ export function selectSettingsItem( export function goBackInSettings(_state: SettingsState): boolean { return false; // Always exit settings on Esc } + +/** + * Returns the API key URL for the currently selected provider if it doesn't have a key. + * Returns null if the selection is not a provider or if the provider already has a key. + */ +export function getSelectedProviderUrl( + state: SettingsState, + chatProvider: Provider, +): string | null { + const items = getSettingsList(chatProvider); + const selected = items[state.selectedIndex]; + if (!selected) return null; + + if (selected.item.type === "provider" && !selected.item.hasKey) { + return selected.item.provider === "anthropic" + ? "https://console.anthropic.com" + : "https://platform.openai.com"; + } + + return null; +} From bb177d1451e11f88fff3bacae6f30f70b2be5e17 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 13:14:47 +0000 Subject: [PATCH 2/3] Simplify API key URL hint to inline (tab) indicator --- src/components/SettingsPanel.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/components/SettingsPanel.ts b/src/components/SettingsPanel.ts index 07a740c..ccf1a0e 100644 --- a/src/components/SettingsPanel.ts +++ b/src/components/SettingsPanel.ts @@ -142,7 +142,7 @@ export function renderSettings( ? "console.anthropic.com" : "platform.openai.com"; const hint = new TextRenderable(ctx, { - content: apiKeyUrl, + content: `${apiKeyUrl} (tab)`, fg: COLORS.textTertiary, }); itemBox.add(hint); @@ -236,17 +236,8 @@ export function renderSettings( container.add(new BoxRenderable(ctx, { height: 2 })); - // Check if any provider is missing a key (to show Tab hint) - const hasAnthropic = !!getApiKey("anthropic"); - const hasOpenAI = !!getApiKey("openai"); - const showTabHint = !hasAnthropic || !hasOpenAI; - - const hintText = showTabHint - ? "↑/↓ navigate Enter select Tab open URL Esc back" - : "↑/↓ navigate Enter select Esc back"; - const hint = new TextRenderable(ctx, { - content: hintText, + content: "↑/↓ navigate Enter select Esc back", fg: COLORS.textSecondary, }); container.add(hint); From dd922070ba3de03c951e3e2f2b81f8e2585a4abe Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Mon, 26 Jan 2026 07:29:47 -0800 Subject: [PATCH 3/3] Fix API key URLs and consolidate URL constants - Update Anthropic URL to platform.claude.com/settings/keys - Update OpenAI URL to platform.openai.com/api-keys - Extract provider URLs to shared PROVIDER_API_KEY_URLS constant - Add tests for getSelectedProviderUrl function Co-Authored-By: Claude Opus 4.5 --- src/components/SettingsPanel.ts | 20 +++++++---- src/test/settings.test.ts | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/components/SettingsPanel.ts b/src/components/SettingsPanel.ts index ccf1a0e..e5cb12e 100644 --- a/src/components/SettingsPanel.ts +++ b/src/components/SettingsPanel.ts @@ -8,6 +8,17 @@ import { OPENAI_MODELS, } from "../config"; +const PROVIDER_API_KEY_URLS: Record = { + anthropic: { + display: "platform.claude.com", + full: "https://platform.claude.com/settings/keys", + }, + openai: { + display: "platform.openai.com", + full: "https://platform.openai.com/api-keys", + }, +}; + type SettingsItemType = | { type: "provider"; provider: Provider; hasKey: boolean } | { type: "model"; modelId: string; modelName: string } @@ -138,11 +149,8 @@ export function renderSettings( itemBox.add(label); if (!hasKey) { - const apiKeyUrl = listItem.item.provider === "anthropic" - ? "console.anthropic.com" - : "platform.openai.com"; const hint = new TextRenderable(ctx, { - content: `${apiKeyUrl} (tab)`, + content: `${PROVIDER_API_KEY_URLS[listItem.item.provider].display} (tab)`, fg: COLORS.textTertiary, }); itemBox.add(hint); @@ -322,9 +330,7 @@ export function getSelectedProviderUrl( if (!selected) return null; if (selected.item.type === "provider" && !selected.item.hasKey) { - return selected.item.provider === "anthropic" - ? "https://console.anthropic.com" - : "https://platform.openai.com"; + return PROVIDER_API_KEY_URLS[selected.item.provider].full; } return null; diff --git a/src/test/settings.test.ts b/src/test/settings.test.ts index 0196c41..7b7e116 100644 --- a/src/test/settings.test.ts +++ b/src/test/settings.test.ts @@ -1,4 +1,6 @@ import { describe, it, expect } from "bun:test"; +import { getSelectedProviderUrl, initSettingsState, type SettingsState } from "../components/SettingsPanel"; +import { getApiKey } from "../config"; // NOTE: Settings Mode rendering tests are skipped due to a pre-existing Yoga layout engine crash // in the OpenTUI test framework. The settings action logic is tested by directly calling @@ -289,3 +291,61 @@ describe("Auth Setup State Management", () => { expect(mockState.keyInput).toBeNull(); }); }); + +describe("Provider API Key URLs", () => { + it("should return correct Anthropic URL when Anthropic has no key and is selected", () => { + // Only test if Anthropic doesn't have a key configured + if (getApiKey("anthropic")) return; + + const state = initSettingsState(); + state.selectedIndex = 0; // Anthropic is first in the list + + const url = getSelectedProviderUrl(state, "anthropic"); + expect(url).toBe("https://platform.claude.com/settings/keys"); + }); + + it("should return correct OpenAI URL when OpenAI has no key and is selected", () => { + // Only test if OpenAI doesn't have a key configured + if (getApiKey("openai")) return; + + const state = initSettingsState(); + state.selectedIndex = 1; // OpenAI is second in the list + + const url = getSelectedProviderUrl(state, "anthropic"); + expect(url).toBe("https://platform.openai.com/api-keys"); + }); + + it("should return null when selection is not a provider (action item)", () => { + const state = initSettingsState(); + // Done action is at index 2 when no keys are configured + state.selectedIndex = 2; + + const url = getSelectedProviderUrl(state, "anthropic"); + expect(url).toBeNull(); + }); + + it("should return null when selected index is out of bounds", () => { + const state = initSettingsState(); + state.selectedIndex = 999; + + const url = getSelectedProviderUrl(state, "anthropic"); + expect(url).toBeNull(); + }); + + it("should return null when provider has a key configured", () => { + // This test verifies the behavior when a provider already has a key + // We can't easily mock this, but we can verify the function exists + // and handles the case based on real config state + const state = initSettingsState(); + state.selectedIndex = 0; + + const url = getSelectedProviderUrl(state, "anthropic"); + + // If Anthropic has a key, URL should be null; otherwise it should be the Anthropic URL + if (getApiKey("anthropic")) { + expect(url).toBeNull(); + } else { + expect(url).toBe("https://platform.claude.com/settings/keys"); + } + }); +});