Keys section (auth/provider-keys wave): BYOK keys in the settings dialog

Owner-approved design (2026-09-20): a Keys section right after General,
two groups over the merged auth API (GET /api/keys masked statuses,
PUT/DELETE /api/keys/:id, immediate effect, shape-gated 400s):

- Model providers: OpenRouter pinned as an always-visible row (the
  app's lifeline — synthesized when absent so first boot shows it),
  stored provider keys render as they appear, and an Add-provider
  dropdown lists every pi-ai provider that takes an API key. The
  dropdown reads GET /api/keys' optional providers array — the
  Platform interlude (master-routed) derives it from pi-ai's own
  env-key table; until it lands the group shows a pending note
  (rolePins pattern — absent field, no drift, no guessed routes).
- Tools & services: the curated rows — Deepgram (dictation) and the
  Google pair (search key + CSE ID) — with description hints and docs
  links (external links route through the Electron shell to the real
  browser).
- Rows: always-visible paste inputs (owner choice; no reveal —
  plaintext never returns from the store), masked status line
  (…tail · chars · environment|saved), the shadowedByEnv explanation
  when a real env var owns the name, per-row inline 400 messages, Save
  on Enter/button, Delete when present (env-owned keys honestly keep
  their env status after a file delete).
- Unknown auth.json vars surface as read-only dimmed rows — preserved
  by the store, not editable through the curated surface.

classifyKeys/keyStatusLine pure exports drive the two-group
projection; tests: SSR loading state, first-boot synthesis, provider
row classification, addable filtering, unknown-var read-only status
line formatting. Nav grows to six sections (Keys second).

Live-verified: nav order, structure, bad-paste 400s inline, save
round-trip with env-shadow explanation, delete round-trip, env-owned
OpenRouter status stability. Gates: typecheck, lint, 748/748, build.
This commit is contained in:
amosblomqvist
2026-09-19 22:53:15 +03:00
parent ec38f91a2b
commit bd66b31853
4 changed files with 471 additions and 3 deletions
+7
View File
@@ -39,6 +39,7 @@ import {
import { useAppearance } from "./appearance.tsx";
import { SettingsSection, SettingsRow, Toggle } from "./controls.tsx";
import { AgentSection } from "./agent-section.tsx";
import { KeysSection } from "./keys-section.tsx";
import { AppearanceSection, GraphSection } from "./appearance-section.tsx";
import { KeyboardSection } from "./keyboard-section.tsx";
@@ -47,6 +48,7 @@ import { KeyboardSection } from "./keyboard-section.tsx";
* real question users ask). */
const SECTIONS = [
{ id: "general", label: "General" },
{ id: "keys", label: "Keys" },
{ id: "agent", label: "Agent" },
{ id: "appearance", label: "Appearance" },
{ id: "graph", label: "Graph" },
@@ -257,6 +259,11 @@ export function SettingsDialogBody({ onClose }: { onClose: () => void }) {
</SettingsRow>
</SettingsSection>
)}
{active === "keys" && (
<SettingsSection title="Keys" testid="section-keys">
<KeysSection />
</SettingsSection>
)}
{active === "agent" && (
<SettingsSection
title="Agent"