# Provider Preset Redesign Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace manual provider configuration with preset-based system — users pick a provider, enter API key, done. **Architecture:** Frontend preset definitions hardcoded in a shared config file. Backend stores saved providers in `~/.claude/cc-haha/providers.json` (lightweight index). On activation, backend writes 6 env keys to `~/.claude/settings.json`. Official preset clears those keys. **Tech Stack:** TypeScript, Zod, Zustand, React, Tailwind CSS **Spec:** `docs/superpowers/specs/2026-04-07-provider-preset-redesign.md` --- ## File Structure | Action | File | Responsibility | |--------|------|---------------| | Create | `src/server/config/providerPresets.ts` | Preset definitions (shared backend) | | Create | `desktop/src/config/providerPresets.ts` | Preset definitions (frontend copy) | | Rewrite | `src/server/types/provider.ts` | Simplified schemas — models as record not array | | Rewrite | `src/server/services/providerService.ts` | New storage path, full env write, official clear | | Modify | `src/server/api/providers.ts` | Simplified activate (no modelId), add presets endpoint | | Rewrite | `desktop/src/types/provider.ts` | Simplified frontend types | | Rewrite | `desktop/src/api/providers.ts` | Updated API calls | | Rewrite | `desktop/src/stores/providerStore.ts` | Aligned with new API shape | | Rewrite | `desktop/src/pages/Settings.tsx` (ProviderSettings section) | Preset chips + simplified form | --- ### Task 1: Create Preset Definitions **Files:** - Create: `src/server/config/providerPresets.ts` - Create: `desktop/src/config/providerPresets.ts` - [ ] **Step 1: Create backend preset file** ```typescript // src/server/config/providerPresets.ts // Provider presets inspired by cc-switch (https://github.com/farion1231/cc-switch) // Original work by Jason Young, MIT License export type ModelMapping = { main: string haiku: string sonnet: string opus: string } export type ProviderPreset = { id: string name: string baseUrl: string defaultModels: ModelMapping needsApiKey: boolean websiteUrl: string } export const PROVIDER_PRESETS: ProviderPreset[] = [ { id: 'official', name: 'Claude Official', baseUrl: '', defaultModels: { main: '', haiku: '', sonnet: '', opus: '' }, needsApiKey: false, websiteUrl: 'https://www.anthropic.com/claude-code', }, { id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/anthropic', defaultModels: { main: 'DeepSeek-V3.2', haiku: 'DeepSeek-V3.2', sonnet: 'DeepSeek-V3.2', opus: 'DeepSeek-V3.2' }, needsApiKey: true, websiteUrl: 'https://platform.deepseek.com', }, { id: 'zhipuglm', name: 'Zhipu GLM', baseUrl: 'https://open.bigmodel.cn/api/anthropic', defaultModels: { main: 'glm-5', haiku: 'glm-5', sonnet: 'glm-5', opus: 'glm-5' }, needsApiKey: true, websiteUrl: 'https://open.bigmodel.cn', }, { id: 'kimi', name: 'Kimi', baseUrl: 'https://api.moonshot.cn/anthropic', defaultModels: { main: 'kimi-k2.5', haiku: 'kimi-k2.5', sonnet: 'kimi-k2.5', opus: 'kimi-k2.5' }, needsApiKey: true, websiteUrl: 'https://platform.moonshot.cn', }, { id: 'minimax', name: 'MiniMax', baseUrl: 'https://api.minimaxi.com/anthropic', defaultModels: { main: 'MiniMax-M2.7', haiku: 'MiniMax-M2.7', sonnet: 'MiniMax-M2.7', opus: 'MiniMax-M2.7' }, needsApiKey: true, websiteUrl: 'https://platform.minimaxi.com', }, { id: 'custom', name: 'Custom', baseUrl: '', defaultModels: { main: '', haiku: '', sonnet: '', opus: '' }, needsApiKey: true, websiteUrl: '', }, ] ``` - [ ] **Step 2: Create frontend preset file (identical content)** Copy `src/server/config/providerPresets.ts` to `desktop/src/config/providerPresets.ts`. Same content — the type + array are plain data, no server dependencies. - [ ] **Step 3: Commit** ```bash git add src/server/config/providerPresets.ts desktop/src/config/providerPresets.ts git commit -m "feat: add provider preset definitions (Official, DeepSeek, ZhipuGLM, Kimi, MiniMax, Custom)" ``` --- ### Task 2: Rewrite Backend Types **Files:** - Rewrite: `src/server/types/provider.ts` - [ ] **Step 1: Rewrite provider types with simplified model structure** Replace `src/server/types/provider.ts` entirely: ```typescript // src/server/types/provider.ts /** * Provider types — preset-based provider configuration. * * Providers are stored in ~/.claude/cc-haha/providers.json as a lightweight index. * The active provider's env vars are written to ~/.claude/settings.json. */ import { z } from 'zod' export const ModelMappingSchema = z.object({ main: z.string(), haiku: z.string(), sonnet: z.string(), opus: z.string(), }) export const SavedProviderSchema = z.object({ id: z.string(), presetId: z.string(), name: z.string().min(1), apiKey: z.string(), baseUrl: z.string(), models: ModelMappingSchema, notes: z.string().optional(), }) export const ProvidersIndexSchema = z.object({ activeId: z.string().nullable(), providers: z.array(SavedProviderSchema), }) export const CreateProviderSchema = z.object({ presetId: z.string().min(1), name: z.string().min(1), apiKey: z.string(), baseUrl: z.string(), models: ModelMappingSchema, notes: z.string().optional(), }) export const UpdateProviderSchema = z.object({ name: z.string().min(1).optional(), apiKey: z.string().optional(), baseUrl: z.string().optional(), models: ModelMappingSchema.optional(), notes: z.string().optional(), }) export const TestProviderSchema = z.object({ baseUrl: z.string().url(), apiKey: z.string().min(1), modelId: z.string().min(1), }) // TypeScript types export type ModelMapping = z.infer export type SavedProvider = z.infer export type ProvidersIndex = z.infer export type CreateProviderInput = z.infer export type UpdateProviderInput = z.infer export type TestProviderInput = z.infer export interface ProviderTestResult { success: boolean latencyMs: number error?: string modelUsed?: string httpStatus?: number } ``` - [ ] **Step 2: Commit** ```bash git add src/server/types/provider.ts git commit -m "refactor: rewrite provider types — simplified model mapping, preset-based structure" ``` --- ### Task 3: Rewrite Backend Provider Service **Files:** - Rewrite: `src/server/services/providerService.ts` - [ ] **Step 1: Rewrite providerService.ts** Replace `src/server/services/providerService.ts` entirely: ```typescript // src/server/services/providerService.ts /** * Provider Service — preset-based provider configuration * * Storage: ~/.claude/cc-haha/providers.json (lightweight index) * Active provider env vars written to ~/.claude/settings.json */ import * as fs from 'fs/promises' import * as path from 'path' import * as os from 'os' import { ApiError } from '../middleware/errorHandler.js' import type { SavedProvider, ProvidersIndex, CreateProviderInput, UpdateProviderInput, TestProviderInput, ProviderTestResult, } from '../types/provider.js' /** The 6 env keys we manage in settings.json */ const MANAGED_ENV_KEYS = [ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL', ] as const const DEFAULT_INDEX: ProvidersIndex = { activeId: null, providers: [] } export class ProviderService { private getConfigDir(): string { return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude') } private getCcHahaDir(): string { return path.join(this.getConfigDir(), 'cc-haha') } private getIndexPath(): string { return path.join(this.getCcHahaDir(), 'providers.json') } private getSettingsPath(): string { return path.join(this.getConfigDir(), 'settings.json') } // --- File I/O (atomic write) --- private async readIndex(): Promise { try { const raw = await fs.readFile(this.getIndexPath(), 'utf-8') return JSON.parse(raw) as ProvidersIndex } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return { ...DEFAULT_INDEX, providers: [] } } throw ApiError.internal(`Failed to read providers index: ${err}`) } } private async writeIndex(index: ProvidersIndex): Promise { const filePath = this.getIndexPath() const dir = path.dirname(filePath) await fs.mkdir(dir, { recursive: true }) const tmpFile = `${filePath}.tmp.${Date.now()}` try { await fs.writeFile(tmpFile, JSON.stringify(index, null, 2) + '\n', 'utf-8') await fs.rename(tmpFile, filePath) } catch (err) { await fs.unlink(tmpFile).catch(() => {}) throw ApiError.internal(`Failed to write providers index: ${err}`) } } private async readSettings(): Promise> { try { const raw = await fs.readFile(this.getSettingsPath(), 'utf-8') return JSON.parse(raw) as Record } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {} throw ApiError.internal(`Failed to read settings.json: ${err}`) } } private async writeSettings(settings: Record): Promise { const filePath = this.getSettingsPath() const dir = path.dirname(filePath) await fs.mkdir(dir, { recursive: true }) const tmpFile = `${filePath}.tmp.${Date.now()}` try { await fs.writeFile(tmpFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8') await fs.rename(tmpFile, filePath) } catch (err) { await fs.unlink(tmpFile).catch(() => {}) throw ApiError.internal(`Failed to write settings.json: ${err}`) } } // --- CRUD --- async listProviders(): Promise<{ providers: SavedProvider[]; activeId: string | null }> { const index = await this.readIndex() return { providers: index.providers, activeId: index.activeId } } async getProvider(id: string): Promise { const index = await this.readIndex() const provider = index.providers.find((p) => p.id === id) if (!provider) throw ApiError.notFound(`Provider not found: ${id}`) return provider } async addProvider(input: CreateProviderInput): Promise { const index = await this.readIndex() const provider: SavedProvider = { id: crypto.randomUUID(), presetId: input.presetId, name: input.name, apiKey: input.apiKey, baseUrl: input.baseUrl, models: input.models, ...(input.notes !== undefined && { notes: input.notes }), } index.providers.push(provider) await this.writeIndex(index) return provider } async updateProvider(id: string, input: UpdateProviderInput): Promise { const index = await this.readIndex() const idx = index.providers.findIndex((p) => p.id === id) if (idx === -1) throw ApiError.notFound(`Provider not found: ${id}`) const existing = index.providers[idx] const updated: SavedProvider = { ...existing, ...(input.name !== undefined && { name: input.name }), ...(input.apiKey !== undefined && { apiKey: input.apiKey }), ...(input.baseUrl !== undefined && { baseUrl: input.baseUrl }), ...(input.models !== undefined && { models: input.models }), ...(input.notes !== undefined && { notes: input.notes }), } index.providers[idx] = updated await this.writeIndex(index) // Re-sync if this is the active provider if (index.activeId === id) { await this.syncToSettings(updated) } return updated } async deleteProvider(id: string): Promise { const index = await this.readIndex() const idx = index.providers.findIndex((p) => p.id === id) if (idx === -1) throw ApiError.notFound(`Provider not found: ${id}`) if (index.activeId === id) { throw ApiError.conflict('Cannot delete the active provider. Switch to another provider first.') } index.providers.splice(idx, 1) await this.writeIndex(index) } // --- Activation --- async activateProvider(id: string): Promise { const index = await this.readIndex() const provider = index.providers.find((p) => p.id === id) if (!provider) throw ApiError.notFound(`Provider not found: ${id}`) index.activeId = id await this.writeIndex(index) if (provider.presetId === 'official') { await this.clearProviderFromSettings() } else { await this.syncToSettings(provider) } } /** Activate official — clear all managed env keys */ async activateOfficial(): Promise { const index = await this.readIndex() index.activeId = null await this.writeIndex(index) await this.clearProviderFromSettings() } // --- Settings sync --- private async syncToSettings(provider: SavedProvider): Promise { const settings = await this.readSettings() const existingEnv = (settings.env as Record) || {} settings.env = { ...existingEnv, ANTHROPIC_BASE_URL: provider.baseUrl, ANTHROPIC_AUTH_TOKEN: provider.apiKey, ANTHROPIC_MODEL: provider.models.main, ANTHROPIC_DEFAULT_HAIKU_MODEL: provider.models.haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: provider.models.sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: provider.models.opus, } await this.writeSettings(settings) } private async clearProviderFromSettings(): Promise { const settings = await this.readSettings() const env = (settings.env as Record) || {} for (const key of MANAGED_ENV_KEYS) { delete env[key] } settings.env = env // Clean up empty env object if (Object.keys(env).length === 0) { delete settings.env } await this.writeSettings(settings) } // --- Test --- async testProvider(id: string): Promise { const provider = await this.getProvider(id) if (!provider.baseUrl || !provider.apiKey) { return { success: false, latencyMs: 0, error: 'Missing baseUrl or apiKey' } } return this.testProviderConfig({ baseUrl: provider.baseUrl, apiKey: provider.apiKey, modelId: provider.models.main, }) } async testProviderConfig(input: TestProviderInput): Promise { const url = `${input.baseUrl.replace(/\/+$/, '')}/v1/messages` const start = Date.now() try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': input.apiKey, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: input.modelId, max_tokens: 1, messages: [{ role: 'user', content: 'Hi' }], }), signal: AbortSignal.timeout(15000), }) const latencyMs = Date.now() - start if (response.ok) { return { success: true, latencyMs, modelUsed: input.modelId, httpStatus: response.status } } let errorMessage = `HTTP ${response.status}` try { const body = (await response.json()) as Record if (body.error && typeof body.error === 'object') { errorMessage = ((body.error as Record).message as string) || errorMessage } else if (typeof body.message === 'string') { errorMessage = body.message } } catch { errorMessage = `HTTP ${response.status} ${response.statusText}` } return { success: false, latencyMs, error: errorMessage, modelUsed: input.modelId, httpStatus: response.status } } catch (err: unknown) { const latencyMs = Date.now() - start if (err instanceof DOMException && err.name === 'TimeoutError') { return { success: false, latencyMs, error: 'Request timed out after 15 seconds', modelUsed: input.modelId } } return { success: false, latencyMs, error: err instanceof Error ? err.message : String(err), modelUsed: input.modelId } } } } ``` - [ ] **Step 2: Commit** ```bash git add src/server/services/providerService.ts git commit -m "refactor: rewrite providerService — cc-haha storage, full env sync, official clear" ``` --- ### Task 4: Update Backend API Routes **Files:** - Modify: `src/server/api/providers.ts` - [ ] **Step 1: Rewrite providers API** Replace `src/server/api/providers.ts` entirely: ```typescript // src/server/api/providers.ts /** * Providers REST API * * GET /api/providers — list all saved providers + activeId * GET /api/providers/presets — list available presets * POST /api/providers — add a provider * PUT /api/providers/:id — update a provider * DELETE /api/providers/:id — delete a provider * POST /api/providers/:id/activate — activate a saved provider * POST /api/providers/official — activate official (clear env) * POST /api/providers/:id/test — test a saved provider * POST /api/providers/test — test unsaved config */ import { z } from 'zod' import { ProviderService } from '../services/providerService.js' import { PROVIDER_PRESETS } from '../config/providerPresets.js' import { CreateProviderSchema, UpdateProviderSchema, TestProviderSchema, } from '../types/provider.js' import { ApiError, errorResponse } from '../middleware/errorHandler.js' const providerService = new ProviderService() function maskApiKey(key: string): string { if (key.length <= 8) return '****' return key.slice(0, 4) + '****' + key.slice(-4) } function sanitizeProvider(provider: Record): Record { if (typeof provider.apiKey === 'string') { return { ...provider, apiKey: maskApiKey(provider.apiKey) } } return provider } export async function handleProvidersApi( req: Request, _url: URL, segments: string[], ): Promise { try { const id = segments[2] const action = segments[3] // POST /api/providers/test if (id === 'test' && req.method === 'POST') { return await handleTestUnsaved(req) } // GET /api/providers/presets if (id === 'presets' && req.method === 'GET') { return Response.json({ presets: PROVIDER_PRESETS }) } // POST /api/providers/official if (id === 'official' && req.method === 'POST') { await providerService.activateOfficial() return Response.json({ ok: true }) } // /api/providers (no ID) if (!id) { if (req.method === 'GET') { const { providers, activeId } = await providerService.listProviders() return Response.json({ providers: providers.map(sanitizeProvider), activeId }) } if (req.method === 'POST') { return await handleCreate(req) } throw methodNotAllowed(req.method) } // /api/providers/:id/activate if (action === 'activate') { if (req.method !== 'POST') throw methodNotAllowed(req.method) await providerService.activateProvider(id) return Response.json({ ok: true }) } // /api/providers/:id/test if (action === 'test') { if (req.method !== 'POST') throw methodNotAllowed(req.method) const result = await providerService.testProvider(id) return Response.json({ result }) } // /api/providers/:id if (req.method === 'GET') { const provider = await providerService.getProvider(id) return Response.json({ provider: sanitizeProvider(provider) }) } if (req.method === 'PUT') { return await handleUpdate(req, id) } if (req.method === 'DELETE') { await providerService.deleteProvider(id) return Response.json({ ok: true }) } throw methodNotAllowed(req.method) } catch (error) { return errorResponse(error) } } async function handleCreate(req: Request): Promise { const body = await parseJsonBody(req) try { const input = CreateProviderSchema.parse(body) const provider = await providerService.addProvider(input) return Response.json({ provider }, { status: 201 }) } catch (err) { if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; ')) throw err } } async function handleUpdate(req: Request, id: string): Promise { const body = await parseJsonBody(req) try { const input = UpdateProviderSchema.parse(body) const provider = await providerService.updateProvider(id, input) return Response.json({ provider }) } catch (err) { if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; ')) throw err } } async function handleTestUnsaved(req: Request): Promise { const body = await parseJsonBody(req) try { const input = TestProviderSchema.parse(body) const result = await providerService.testProviderConfig(input) return Response.json({ result }) } catch (err) { if (err instanceof z.ZodError) throw ApiError.badRequest(err.issues.map((i) => i.message).join('; ')) throw err } } async function parseJsonBody(req: Request): Promise> { try { return (await req.json()) as Record } catch { throw ApiError.badRequest('Invalid JSON body') } } function methodNotAllowed(method: string): ApiError { return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED') } ``` - [ ] **Step 2: Commit** ```bash git add src/server/api/providers.ts git commit -m "refactor: update provider API — add presets endpoint, simplify activate, add official route" ``` --- ### Task 5: Rewrite Frontend Types + API + Store **Files:** - Rewrite: `desktop/src/types/provider.ts` - Rewrite: `desktop/src/api/providers.ts` - Rewrite: `desktop/src/stores/providerStore.ts` - [ ] **Step 1: Rewrite frontend types** Replace `desktop/src/types/provider.ts`: ```typescript // desktop/src/types/provider.ts export type ModelMapping = { main: string haiku: string sonnet: string opus: string } export type SavedProvider = { id: string presetId: string name: string apiKey: string // masked from server baseUrl: string models: ModelMapping notes?: string } export type CreateProviderInput = { presetId: string name: string apiKey: string baseUrl: string models: ModelMapping notes?: string } export type UpdateProviderInput = { name?: string apiKey?: string baseUrl?: string models?: ModelMapping notes?: string } export type TestProviderConfigInput = { baseUrl: string apiKey: string modelId: string } export type ProviderTestResult = { success: boolean latencyMs: number error?: string modelUsed?: string httpStatus?: number } ``` - [ ] **Step 2: Rewrite frontend API client** Replace `desktop/src/api/providers.ts`: ```typescript // desktop/src/api/providers.ts import { api } from './client' import type { SavedProvider, CreateProviderInput, UpdateProviderInput, TestProviderConfigInput, ProviderTestResult, } from '../types/provider' import type { ProviderPreset } from '../config/providerPresets' type ProvidersResponse = { providers: SavedProvider[]; activeId: string | null } type ProviderResponse = { provider: SavedProvider } type PresetsResponse = { presets: ProviderPreset[] } type TestResultResponse = { result: ProviderTestResult } export const providersApi = { list() { return api.get('/api/providers') }, presets() { return api.get('/api/providers/presets') }, create(input: CreateProviderInput) { return api.post('/api/providers', input) }, update(id: string, input: UpdateProviderInput) { return api.put(`/api/providers/${id}`, input) }, delete(id: string) { return api.delete<{ ok: true }>(`/api/providers/${id}`) }, activate(id: string) { return api.post<{ ok: true }>(`/api/providers/${id}/activate`) }, activateOfficial() { return api.post<{ ok: true }>('/api/providers/official') }, test(id: string) { return api.post(`/api/providers/${id}/test`) }, testConfig(input: TestProviderConfigInput) { return api.post('/api/providers/test', input) }, } ``` - [ ] **Step 3: Rewrite frontend store** Replace `desktop/src/stores/providerStore.ts`: ```typescript // desktop/src/stores/providerStore.ts import { create } from 'zustand' import { providersApi } from '../api/providers' import type { SavedProvider, CreateProviderInput, UpdateProviderInput, TestProviderConfigInput, ProviderTestResult, } from '../types/provider' type ProviderStore = { providers: SavedProvider[] activeId: string | null isLoading: boolean fetchProviders: () => Promise createProvider: (input: CreateProviderInput) => Promise updateProvider: (id: string, input: UpdateProviderInput) => Promise deleteProvider: (id: string) => Promise activateProvider: (id: string) => Promise activateOfficial: () => Promise testProvider: (id: string) => Promise testConfig: (input: TestProviderConfigInput) => Promise } export const useProviderStore = create((set, get) => ({ providers: [], activeId: null, isLoading: false, fetchProviders: async () => { set({ isLoading: true }) try { const { providers, activeId } = await providersApi.list() set({ providers, activeId, isLoading: false }) } catch { set({ isLoading: false }) } }, createProvider: async (input) => { const { provider } = await providersApi.create(input) await get().fetchProviders() return provider }, updateProvider: async (id, input) => { const { provider } = await providersApi.update(id, input) await get().fetchProviders() return provider }, deleteProvider: async (id) => { await providersApi.delete(id) await get().fetchProviders() }, activateProvider: async (id) => { await providersApi.activate(id) await get().fetchProviders() }, activateOfficial: async () => { await providersApi.activateOfficial() await get().fetchProviders() }, testProvider: async (id) => { const { result } = await providersApi.test(id) return result }, testConfig: async (input) => { const { result } = await providersApi.testConfig(input) return result }, })) ``` - [ ] **Step 4: Commit** ```bash git add desktop/src/types/provider.ts desktop/src/api/providers.ts desktop/src/stores/providerStore.ts git commit -m "refactor: rewrite frontend provider types, API client, and store for preset system" ``` --- ### Task 6: Rewrite ProviderSettings UI in Settings.tsx **Files:** - Rewrite: `desktop/src/pages/Settings.tsx` (ProviderSettings + ProviderFormModal sections only, lines 65-396) - [ ] **Step 1: Rewrite ProviderSettings and ProviderFormModal** Replace the `ProviderSettings` function and `ProviderFormModal` function in `desktop/src/pages/Settings.tsx`. Keep the imports, `Settings` component, `TabButton`, `PermissionSettings`, and `GeneralSettings` unchanged. Update the imports at the top of the file: ```typescript import { useState, useEffect } from 'react' import { useSettingsStore } from '../stores/settingsStore' import { useProviderStore } from '../stores/providerStore' import { useUIStore } from '../stores/uiStore' import { Modal } from '../components/shared/Modal' import { Input } from '../components/shared/Input' import { Button } from '../components/shared/Button' import { PROVIDER_PRESETS } from '../config/providerPresets' import type { PermissionMode, EffortLevel } from '../types/settings' import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping } from '../types/provider' ``` New `ProviderSettings`: ```typescript function ProviderSettings() { const { providers, activeId, isLoading, fetchProviders, deleteProvider, activateProvider, activateOfficial, testProvider } = useProviderStore() const fetchSettings = useSettingsStore((s) => s.fetchAll) const [editingProvider, setEditingProvider] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [testResults, setTestResults] = useState>({}) useEffect(() => { fetchProviders() }, [fetchProviders]) const handleDelete = async (provider: SavedProvider) => { if (activeId === provider.id) return if (!window.confirm(`Delete provider "${provider.name}"? This cannot be undone.`)) return await deleteProvider(provider.id).catch(console.error) } const handleTest = async (provider: SavedProvider) => { setTestResults((r) => ({ ...r, [provider.id]: { loading: true } })) try { const result = await testProvider(provider.id) setTestResults((r) => ({ ...r, [provider.id]: { loading: false, result } })) } catch { setTestResults((r) => ({ ...r, [provider.id]: { loading: false, result: { success: false, latencyMs: 0, error: 'Request failed' } } })) } } const handleActivate = async (id: string) => { await activateProvider(id) await fetchSettings() } const handleActivateOfficial = async () => { await activateOfficial() await fetchSettings() } const isOfficialActive = activeId === null return (

Providers

Manage API providers for model access.

{/* Official provider card */}
!isOfficialActive && handleActivateOfficial()} >
Claude Official {isOfficialActive && ( ACTIVE )}
Anthropic native — no API key required
{/* Saved providers */} {isLoading && providers.length === 0 ? (
) : (
{providers.map((provider) => { const isActive = activeId === provider.id const test = testResults[provider.id] const preset = PROVIDER_PRESETS.find((p) => p.id === provider.presetId) return (
{provider.name} {preset && preset.id !== 'custom' && ( {preset.name} )} {isActive && ( ACTIVE )}
{provider.baseUrl} · {provider.models.main}
{test && !test.loading && test.result && (
{test.result.success ? `Connected (${test.result.latencyMs}ms)` : `Failed: ${test.result.error}`}
)}
{!isActive && ( )} {!isActive && ( )}
) })}
)} {/* Create Modal */} setShowCreateModal(false)} mode="create" /> {/* Edit Modal */} {editingProvider && ( setEditingProvider(null)} mode="edit" provider={editingProvider} /> )}
) } ``` New `ProviderFormModal`: ```typescript type ProviderFormProps = { open: boolean onClose: () => void mode: 'create' | 'edit' provider?: SavedProvider } function ProviderFormModal({ open, onClose, mode, provider }: ProviderFormProps) { const { createProvider, updateProvider, testConfig } = useProviderStore() const fetchSettings = useSettingsStore((s) => s.fetchAll) // Exclude 'official' from create presets — it has its own card const availablePresets = PROVIDER_PRESETS.filter((p) => p.id !== 'official') const initialPreset = provider ? availablePresets.find((p) => p.id === provider.presetId) || availablePresets.at(-1)! : availablePresets[0] const [selectedPreset, setSelectedPreset] = useState(initialPreset) const [name, setName] = useState(provider?.name ?? initialPreset.name) const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? initialPreset.baseUrl) const [apiKey, setApiKey] = useState('') const [notes, setNotes] = useState(provider?.notes ?? '') const [models, setModels] = useState(provider?.models ?? { ...initialPreset.defaultModels }) const [showAdvanced, setShowAdvanced] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [testResult, setTestResult] = useState(null) const [isTesting, setIsTesting] = useState(false) const handlePresetChange = (preset: typeof initialPreset) => { setSelectedPreset(preset) setName(preset.name) setBaseUrl(preset.baseUrl) setModels({ ...preset.defaultModels }) setTestResult(null) } const isCustom = selectedPreset.id === 'custom' const canSubmit = name.trim() && baseUrl.trim() && (mode === 'edit' || apiKey.trim()) && models.main.trim() const handleSubmit = async () => { if (!canSubmit) return setIsSubmitting(true) try { if (mode === 'create') { await createProvider({ presetId: selectedPreset.id, name: name.trim(), apiKey: apiKey.trim(), baseUrl: baseUrl.trim(), models, notes: notes.trim() || undefined, }) } else if (provider) { const input: UpdateProviderInput = { name: name.trim(), baseUrl: baseUrl.trim(), models, notes: notes.trim() || undefined, } if (apiKey.trim()) input.apiKey = apiKey.trim() await updateProvider(provider.id, input) if (useProviderStore.getState().activeId === provider.id) await fetchSettings() } onClose() } catch (err) { console.error('Failed to save provider:', err) } finally { setIsSubmitting(false) } } const handleTest = async () => { if (!baseUrl.trim() || !models.main.trim()) return setIsTesting(true) setTestResult(null) try { let result: ProviderTestResult if (mode === 'edit' && provider && !apiKey.trim()) { result = await useProviderStore.getState().testProvider(provider.id) } else { if (!apiKey.trim()) return result = await testConfig({ baseUrl: baseUrl.trim(), apiKey: apiKey.trim(), modelId: models.main.trim() }) } setTestResult(result) } catch { setTestResult({ success: false, latencyMs: 0, error: 'Request failed' }) } finally { setIsTesting(false) } } return ( } >
{/* Preset chips (create mode only) */} {mode === 'create' && (
{availablePresets.map((preset) => ( ))}
)} setName(e.target.value)} placeholder="Provider name" /> {/* Base URL — always visible for custom, read-only hint for presets */} {isCustom || mode === 'edit' ? ( setBaseUrl(e.target.value)} placeholder="https://api.example.com/anthropic" /> ) : (
{baseUrl}
)} setApiKey(e.target.value)} placeholder={mode === 'edit' ? '****' : 'sk-...'} /> setNotes(e.target.value)} placeholder="Optional notes..." /> {/* Advanced: Model Mapping */}
{showAdvanced && (
setModels({ ...models, main: e.target.value })} placeholder="Model ID" /> setModels({ ...models, haiku: e.target.value })} placeholder="Same as main" /> setModels({ ...models, sonnet: e.target.value })} placeholder="Same as main" /> setModels({ ...models, opus: e.target.value })} placeholder="Same as main" />
)}
{/* Test connection */}
{testResult && ( {testResult.success ? `Connected (${testResult.latencyMs}ms)` : `Failed: ${testResult.error}`} )}
) } ``` - [ ] **Step 2: Commit** ```bash git add desktop/src/pages/Settings.tsx git commit -m "feat: rewrite ProviderSettings UI — preset chips, simplified form, official card" ``` --- ### Task 7: Clean Up Unused Code **Files:** - Modify: `desktop/src/pages/Settings.tsx` (remove unused imports) - [ ] **Step 1: Verify no unused imports** Check that the imports at the top of Settings.tsx match what's used. The old `Provider`, `ProviderModel` imports should be removed, replaced with `SavedProvider`, `ModelMapping`. `ProviderTestResult` and `UpdateProviderInput` should remain. Remove `ProviderModel` from type imports if still present (it no longer exists). - [ ] **Step 2: Remove old `~/.claude/providers.json`** No code change needed — the old file is just ignored. The new storage path is `~/.claude/cc-haha/providers.json`. - [ ] **Step 3: Commit** ```bash git add desktop/src/pages/Settings.tsx git commit -m "chore: clean up unused imports after provider preset migration" ``` --- Plan complete and saved to `docs/superpowers/plans/2026-04-07-provider-preset-redesign.md`. Two execution options: **1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration **2. Inline Execution** - Execute tasks in this session, batch execution with checkpoints Which approach?