feat: desktop setting provider
Browse files- desktop/index.html +8 -1
- desktop/package.json +2 -1
- desktop/src-tauri/tauri.conf.json +1 -1
- desktop/src/api/cliAuth.ts +31 -0
- desktop/src/components/settings/AnthropicLoginPanel.tsx +101 -0
- desktop/src/components/settings/CliLoginProviderSettings.tsx +125 -0
- desktop/src/components/settings/LocalLoginPanel.tsx +78 -0
- desktop/src/components/settings/NvidiaLoginPanel.tsx +66 -0
- desktop/src/components/settings/OpenAILoginPanel.tsx +66 -0
- desktop/src/components/settings/OpenCodeLoginPanel.tsx +115 -0
- desktop/src/components/settings/OpenRouterLoginPanel.tsx +66 -0
- desktop/src/main.tsx +16 -2
- desktop/src/pages/Settings.tsx +2 -1226
- desktop/src/stores/cliAuthStore.ts +141 -0
- desktop/tsconfig.tsbuildinfo +1 -1
- src/server/api/cli-auth.ts +157 -0
- src/server/api/models.ts +55 -9
- src/server/router.ts +4 -0
- src/server/services/conversationService.ts +95 -3
- src/server/services/providerService.ts +81 -0
- src/server/ws/handler.ts +56 -1
desktop/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
</style>
|
| 13 |
<script>
|
| 14 |
(function () {
|
| 15 |
-
var bootDeadlineMs =
|
| 16 |
|
| 17 |
function summarize(value) {
|
| 18 |
if (!value) return 'Unknown startup error'
|
|
@@ -27,6 +27,8 @@
|
|
| 27 |
|
| 28 |
function renderStartupError(reason) {
|
| 29 |
if (window.__CC_HAHA_BOOTSTRAPPED__) return
|
|
|
|
|
|
|
| 30 |
var root = document.getElementById('root')
|
| 31 |
if (!root) return
|
| 32 |
|
|
@@ -94,8 +96,13 @@
|
|
| 94 |
})
|
| 95 |
|
| 96 |
window.setTimeout(function () {
|
|
|
|
| 97 |
renderStartupError('Desktop app did not finish bootstrapping within ' + bootDeadlineMs + 'ms. This usually means the WebView could not execute the app bundle or startup code stopped before React mounted.')
|
| 98 |
}, bootDeadlineMs)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
})()
|
| 100 |
</script>
|
| 101 |
</head>
|
|
|
|
| 12 |
</style>
|
| 13 |
<script>
|
| 14 |
(function () {
|
| 15 |
+
var bootDeadlineMs = 30000
|
| 16 |
|
| 17 |
function summarize(value) {
|
| 18 |
if (!value) return 'Unknown startup error'
|
|
|
|
| 27 |
|
| 28 |
function renderStartupError(reason) {
|
| 29 |
if (window.__CC_HAHA_BOOTSTRAPPED__) return
|
| 30 |
+
if (window.__CC_HAHA_STARTUP_ERROR_SHOWN__) return
|
| 31 |
+
window.__CC_HAHA_STARTUP_ERROR_SHOWN__ = true
|
| 32 |
var root = document.getElementById('root')
|
| 33 |
if (!root) return
|
| 34 |
|
|
|
|
| 96 |
})
|
| 97 |
|
| 98 |
window.setTimeout(function () {
|
| 99 |
+
if (window.__CC_HAHA_STARTUP_ERROR_SHOWN__) return
|
| 100 |
renderStartupError('Desktop app did not finish bootstrapping within ' + bootDeadlineMs + 'ms. This usually means the WebView could not execute the app bundle or startup code stopped before React mounted.')
|
| 101 |
}, bootDeadlineMs)
|
| 102 |
+
|
| 103 |
+
window.addEventListener('load', function () {
|
| 104 |
+
window.__CC_HAHA_PAGE_LOADED__ = true
|
| 105 |
+
})
|
| 106 |
})()
|
| 107 |
</script>
|
| 108 |
</head>
|
desktop/package.json
CHANGED
|
@@ -5,12 +5,13 @@
|
|
| 5 |
"type": "module",
|
| 6 |
"scripts": {
|
| 7 |
"dev": "vite",
|
|
|
|
| 8 |
"build": "tsc -b && vite build",
|
| 9 |
"build:sidecars": "bun run ./scripts/build-sidecars.ts",
|
| 10 |
"build:macos-arm64": "bash ./scripts/build-macos-arm64.sh",
|
| 11 |
"build:windows-x64": "powershell -ExecutionPolicy Bypass -File ./scripts/build-windows-x64.ps1",
|
| 12 |
"preview": "vite preview",
|
| 13 |
-
"tauri": "GDK_BACKEND=x11 WEBKIT_DISABLE_COMPOSITING_MODE=1 tauri",
|
| 14 |
"test": "vitest",
|
| 15 |
"test:ui": "vitest --ui",
|
| 16 |
"lint": "tsc --noEmit"
|
|
|
|
| 5 |
"type": "module",
|
| 6 |
"scripts": {
|
| 7 |
"dev": "vite",
|
| 8 |
+
"dev:kill": "fuser -k 1420/tcp 2>/dev/null; bun run dev",
|
| 9 |
"build": "tsc -b && vite build",
|
| 10 |
"build:sidecars": "bun run ./scripts/build-sidecars.ts",
|
| 11 |
"build:macos-arm64": "bash ./scripts/build-macos-arm64.sh",
|
| 12 |
"build:windows-x64": "powershell -ExecutionPolicy Bypass -File ./scripts/build-windows-x64.ps1",
|
| 13 |
"preview": "vite preview",
|
| 14 |
+
"tauri": "lsof -ti:1420 | xargs kill -9 2>/dev/null; GDK_BACKEND=x11 WEBKIT_DISABLE_COMPOSITING_MODE=1 tauri",
|
| 15 |
"test": "vitest",
|
| 16 |
"test:ui": "vitest --ui",
|
| 17 |
"lint": "tsc --noEmit"
|
desktop/src-tauri/tauri.conf.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
"build": {
|
| 7 |
"frontendDist": "../dist",
|
| 8 |
"devUrl": "http://localhost:1420",
|
| 9 |
-
"beforeDevCommand": "bun run build:sidecars && bun run dev",
|
| 10 |
"beforeBuildCommand": "bun run build && bun run build:sidecars"
|
| 11 |
},
|
| 12 |
"app": {
|
|
|
|
| 6 |
"build": {
|
| 7 |
"frontendDist": "../dist",
|
| 8 |
"devUrl": "http://localhost:1420",
|
| 9 |
+
"beforeDevCommand": "lsof -ti:1420 | xargs kill -9 2>/dev/null; bun run build:sidecars && bun run dev",
|
| 10 |
"beforeBuildCommand": "bun run build && bun run build:sidecars"
|
| 11 |
},
|
| 12 |
"app": {
|
desktop/src/api/cliAuth.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { api } from './client'
|
| 2 |
+
|
| 3 |
+
type AuthProvider = 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
|
| 4 |
+
|
| 5 |
+
type CliAuthConfig = {
|
| 6 |
+
authProvider: AuthProvider | null
|
| 7 |
+
nvidiaApiKey?: string
|
| 8 |
+
openRouterApiKey?: string
|
| 9 |
+
openAiApiKey?: string
|
| 10 |
+
openAiAccessToken?: string
|
| 11 |
+
openCodeApiKey?: string
|
| 12 |
+
openCodeModelName?: string
|
| 13 |
+
localBaseUrl?: string
|
| 14 |
+
localModelName?: string
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export type { AuthProvider, CliAuthConfig }
|
| 18 |
+
|
| 19 |
+
export const cliAuthApi = {
|
| 20 |
+
get(): Promise<CliAuthConfig> {
|
| 21 |
+
return api.get<CliAuthConfig>('/api/cli-auth')
|
| 22 |
+
},
|
| 23 |
+
|
| 24 |
+
update(config: Partial<CliAuthConfig>): Promise<{ ok: true }> {
|
| 25 |
+
return api.put<{ ok: true }>('/api/cli-auth', config)
|
| 26 |
+
},
|
| 27 |
+
|
| 28 |
+
invalidateCache(): Promise<{ ok: true }> {
|
| 29 |
+
return api.post<{ ok: true }>('/api/cli-auth/invalidate', {})
|
| 30 |
+
},
|
| 31 |
+
}
|
desktop/src/components/settings/AnthropicLoginPanel.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect } from 'react'
|
| 2 |
+
import { open as shellOpen } from '@tauri-apps/plugin-shell'
|
| 3 |
+
import { useHahaOAuthStore } from '../../stores/hahaOAuthStore'
|
| 4 |
+
import { useTranslation } from '../../i18n'
|
| 5 |
+
|
| 6 |
+
export function AnthropicLoginPanel() {
|
| 7 |
+
const t = useTranslation()
|
| 8 |
+
const {
|
| 9 |
+
status,
|
| 10 |
+
isLoading,
|
| 11 |
+
error,
|
| 12 |
+
fetchStatus,
|
| 13 |
+
login,
|
| 14 |
+
logout,
|
| 15 |
+
startPolling,
|
| 16 |
+
stopPolling,
|
| 17 |
+
} = useHahaOAuthStore()
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
fetchStatus()
|
| 21 |
+
return () => stopPolling()
|
| 22 |
+
}, [fetchStatus, stopPolling])
|
| 23 |
+
|
| 24 |
+
const handleLogin = async () => {
|
| 25 |
+
try {
|
| 26 |
+
const { authorizeUrl } = await login()
|
| 27 |
+
try {
|
| 28 |
+
await shellOpen(authorizeUrl)
|
| 29 |
+
startPolling()
|
| 30 |
+
} catch (err) {
|
| 31 |
+
console.error('[AnthropicLoginPanel] shellOpen failed:', err)
|
| 32 |
+
useHahaOAuthStore.setState({
|
| 33 |
+
error: t('settings.claudeOfficialLogin.openBrowserFailed'),
|
| 34 |
+
})
|
| 35 |
+
}
|
| 36 |
+
} catch {
|
| 37 |
+
// store.login() errors are already captured into store.error
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if (status === null) {
|
| 42 |
+
if (error) {
|
| 43 |
+
return (
|
| 44 |
+
<div className="text-xs text-[var(--color-error)] py-2">
|
| 45 |
+
{t('settings.claudeOfficialLogin.errorPrefix')}{error}
|
| 46 |
+
</div>
|
| 47 |
+
)
|
| 48 |
+
}
|
| 49 |
+
return (
|
| 50 |
+
<div className="text-xs text-[var(--color-text-tertiary)] py-2">
|
| 51 |
+
{t('common.loading')}
|
| 52 |
+
</div>
|
| 53 |
+
)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (status.loggedIn) {
|
| 57 |
+
const subTypeLabel = status.subscriptionType
|
| 58 |
+
? status.subscriptionType.toUpperCase()
|
| 59 |
+
: t('settings.claudeOfficialLogin.subTypeUnknown')
|
| 60 |
+
return (
|
| 61 |
+
<div className="flex items-center gap-3 py-2">
|
| 62 |
+
<span className="text-sm text-[var(--color-success)]">
|
| 63 |
+
✓ {t('settings.claudeOfficialLogin.loggedInPrefix')} {subTypeLabel})
|
| 64 |
+
</span>
|
| 65 |
+
<button
|
| 66 |
+
type="button"
|
| 67 |
+
onClick={logout}
|
| 68 |
+
disabled={isLoading}
|
| 69 |
+
className="px-3 py-1 text-xs rounded-md border border-[var(--color-border-separator)] bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)] disabled:opacity-50 transition-colors"
|
| 70 |
+
>
|
| 71 |
+
{isLoading
|
| 72 |
+
? t('settings.claudeOfficialLogin.logoutProcessing')
|
| 73 |
+
: t('settings.claudeOfficialLogin.logoutButton')}
|
| 74 |
+
</button>
|
| 75 |
+
</div>
|
| 76 |
+
)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
<div className="flex flex-col gap-2 py-2">
|
| 81 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 82 |
+
{t('settings.claudeOfficialLogin.intro')}
|
| 83 |
+
</div>
|
| 84 |
+
<button
|
| 85 |
+
type="button"
|
| 86 |
+
onClick={handleLogin}
|
| 87 |
+
disabled={isLoading}
|
| 88 |
+
className="self-start rounded-md bg-[image:var(--gradient-btn-primary)] px-4 py-2 text-sm text-[var(--color-btn-primary-fg)] shadow-[var(--shadow-button-primary)] hover:brightness-105 disabled:opacity-50 transition-opacity"
|
| 89 |
+
>
|
| 90 |
+
{isLoading
|
| 91 |
+
? t('settings.claudeOfficialLogin.loginStarting')
|
| 92 |
+
: t('settings.claudeOfficialLogin.loginButton')}
|
| 93 |
+
</button>
|
| 94 |
+
{error && (
|
| 95 |
+
<div className="text-xs text-[var(--color-error)]">
|
| 96 |
+
{t('settings.claudeOfficialLogin.errorPrefix')}{error}
|
| 97 |
+
</div>
|
| 98 |
+
)}
|
| 99 |
+
</div>
|
| 100 |
+
)
|
| 101 |
+
}
|
desktop/src/components/settings/CliLoginProviderSettings.tsx
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react'
|
| 2 |
+
import { useCliAuthStore } from '../../stores/cliAuthStore'
|
| 3 |
+
import { AnthropicLoginPanel } from './AnthropicLoginPanel'
|
| 4 |
+
import { OpenAILoginPanel } from './OpenAILoginPanel'
|
| 5 |
+
import { NvidiaLoginPanel } from './NvidiaLoginPanel'
|
| 6 |
+
import { OpenRouterLoginPanel } from './OpenRouterLoginPanel'
|
| 7 |
+
import { OpenCodeLoginPanel } from './OpenCodeLoginPanel'
|
| 8 |
+
import { LocalLoginPanel } from './LocalLoginPanel'
|
| 9 |
+
import type { AuthProvider } from '../../api/cliAuth'
|
| 10 |
+
|
| 11 |
+
const PROVIDER_OPTIONS: { id: AuthProvider; name: string; description: string; icon: string }[] = [
|
| 12 |
+
{ id: 'anthropic', name: 'Anthropic', description: 'Subscription login, Console API billing, or Bedrock/Foundry/Vertex', icon: '🤖' },
|
| 13 |
+
{ id: 'openai', name: 'OpenAI / Codex', description: 'Codex login, Codex auth import, or OpenAI API key', icon: '🤖' },
|
| 14 |
+
{ id: 'openrouter', name: 'OpenRouter', description: 'OpenRouter API key via Responses API', icon: '🔀' },
|
| 15 |
+
{ id: 'opencode', name: 'OpenCode Zen', description: 'Free models (Big Pickle, GPT 5 Nano) or Zen API key', icon: '✨' },
|
| 16 |
+
{ id: 'local', name: 'Local', description: 'Local model server (Ollama, LM Studio, vLLM, etc.)', icon: '💻' },
|
| 17 |
+
{ id: 'nvidia', name: 'NVIDIA', description: 'NVIDIA NIM API key from build.nvidia.com', icon: '🔷' },
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
const LOGIN_PANEL_MAP: Record<AuthProvider, React.FC> = {
|
| 21 |
+
anthropic: AnthropicLoginPanel,
|
| 22 |
+
openai: OpenAILoginPanel,
|
| 23 |
+
openrouter: OpenRouterLoginPanel,
|
| 24 |
+
opencode: OpenCodeLoginPanel,
|
| 25 |
+
local: LocalLoginPanel,
|
| 26 |
+
nvidia: NvidiaLoginPanel,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export function CliLoginProviderSettings() {
|
| 30 |
+
const { authProvider, isLoading, fetchAuth, clearAuth } = useCliAuthStore()
|
| 31 |
+
const [expandedProvider, setExpandedProvider] = useState<AuthProvider | null>(null)
|
| 32 |
+
|
| 33 |
+
useEffect(() => {
|
| 34 |
+
fetchAuth()
|
| 35 |
+
}, [fetchAuth])
|
| 36 |
+
|
| 37 |
+
const handleProviderClick = (providerId: AuthProvider) => {
|
| 38 |
+
if (expandedProvider === providerId) {
|
| 39 |
+
setExpandedProvider(null)
|
| 40 |
+
} else {
|
| 41 |
+
setExpandedProvider(providerId)
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className="max-w-2xl">
|
| 47 |
+
<div className="mb-4">
|
| 48 |
+
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">Provider</h2>
|
| 49 |
+
<p className="text-sm text-[var(--color-text-tertiary)] mt-0.5">
|
| 50 |
+
Choose which provider to use. Pick a provider first, then configure the login method.
|
| 51 |
+
</p>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
{authProvider && (
|
| 55 |
+
<div className="mb-3 px-4 py-2 rounded-lg bg-[var(--color-surface-container)] border border-[var(--color-border)]">
|
| 56 |
+
<div className="flex items-center justify-between">
|
| 57 |
+
<span className="text-sm text-[var(--color-text-secondary)]">
|
| 58 |
+
Current provider: <strong className="text-[var(--color-text-primary)]">{PROVIDER_OPTIONS.find(p => p.id === authProvider)?.name || authProvider}</strong>
|
| 59 |
+
</span>
|
| 60 |
+
<button
|
| 61 |
+
onClick={clearAuth}
|
| 62 |
+
disabled={isLoading}
|
| 63 |
+
className="text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-error)] disabled:opacity-50"
|
| 64 |
+
>
|
| 65 |
+
Clear & change
|
| 66 |
+
</button>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
)}
|
| 70 |
+
|
| 71 |
+
<div className="flex flex-col gap-2">
|
| 72 |
+
{PROVIDER_OPTIONS.map((provider) => {
|
| 73 |
+
const isActive = provider.id === authProvider
|
| 74 |
+
const isExpanded = expandedProvider === provider.id
|
| 75 |
+
const LoginPanel = LOGIN_PANEL_MAP[provider.id]
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
<div
|
| 79 |
+
key={provider.id}
|
| 80 |
+
className={`rounded-xl border transition-all ${
|
| 81 |
+
isExpanded && isActive
|
| 82 |
+
? 'border-[var(--color-brand)] bg-[var(--color-surface-container)] shadow-[var(--shadow-focus-ring)]'
|
| 83 |
+
: isActive
|
| 84 |
+
? 'border-[var(--color-success)]/30 bg-[var(--color-surface-container)]'
|
| 85 |
+
: isExpanded
|
| 86 |
+
? 'border-[var(--color-border-focus)]'
|
| 87 |
+
: 'border-[var(--color-border)] hover:border-[var(--color-border-focus)]'
|
| 88 |
+
}`}
|
| 89 |
+
>
|
| 90 |
+
<button
|
| 91 |
+
className={`w-full flex items-center gap-4 px-4 py-3.5 text-left ${!isExpanded && !isActive ? 'cursor-pointer' : ''}`}
|
| 92 |
+
onClick={() => handleProviderClick(provider.id)}
|
| 93 |
+
>
|
| 94 |
+
<span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${
|
| 95 |
+
isActive ? 'bg-[var(--color-success)]' : isExpanded ? 'bg-[var(--color-brand)]' : 'bg-[var(--color-text-tertiary)]'
|
| 96 |
+
}`} />
|
| 97 |
+
<div className="flex-1 min-w-0">
|
| 98 |
+
<div className="flex items-center gap-2">
|
| 99 |
+
<span className="text-sm font-semibold text-[var(--color-text-primary)]">{provider.name}</span>
|
| 100 |
+
{isActive && authProvider && (
|
| 101 |
+
<span className="px-1.5 py-0.5 text-[10px] font-bold rounded border border-[var(--color-success)]/18 bg-[var(--color-success)]/14 text-[var(--color-success)] leading-none">Active</span>
|
| 102 |
+
)}
|
| 103 |
+
</div>
|
| 104 |
+
<div className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{provider.description}</div>
|
| 105 |
+
</div>
|
| 106 |
+
<svg
|
| 107 |
+
className={`w-4 h-4 text-[var(--color-text-tertiary)] transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
| 108 |
+
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
| 109 |
+
>
|
| 110 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
| 111 |
+
</svg>
|
| 112 |
+
</button>
|
| 113 |
+
|
| 114 |
+
{isExpanded && (
|
| 115 |
+
<div className="px-4 pb-4 pt-2 border-t border-[var(--color-border-separator)]">
|
| 116 |
+
<LoginPanel />
|
| 117 |
+
</div>
|
| 118 |
+
)}
|
| 119 |
+
</div>
|
| 120 |
+
)
|
| 121 |
+
})}
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
)
|
| 125 |
+
}
|
desktop/src/components/settings/LocalLoginPanel.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react'
|
| 2 |
+
import { useCliAuthStore } from '../../stores/cliAuthStore'
|
| 3 |
+
|
| 4 |
+
export function LocalLoginPanel() {
|
| 5 |
+
const { localBaseUrl, localModelName, saveLocalModelConfig, isLoading } = useCliAuthStore()
|
| 6 |
+
const [url, setUrl] = useState('')
|
| 7 |
+
const [modelName, setModelName] = useState('')
|
| 8 |
+
const [status, setStatus] = useState<string | null>(null)
|
| 9 |
+
const defaultUrl = 'http://127.0.0.1:8001'
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
if (localBaseUrl) setUrl(localBaseUrl)
|
| 13 |
+
if (localModelName) setModelName(localModelName)
|
| 14 |
+
}, [localBaseUrl, localModelName])
|
| 15 |
+
|
| 16 |
+
const handleSave = async () => {
|
| 17 |
+
const finalUrl = url.trim() || defaultUrl
|
| 18 |
+
const finalModel = modelName.trim() || 'default'
|
| 19 |
+
if (!finalUrl) { setStatus('Please enter a URL'); return }
|
| 20 |
+
try {
|
| 21 |
+
new URL(finalUrl) // validate URL
|
| 22 |
+
} catch {
|
| 23 |
+
setStatus('Invalid URL format')
|
| 24 |
+
return
|
| 25 |
+
}
|
| 26 |
+
setStatus(null)
|
| 27 |
+
try {
|
| 28 |
+
await saveLocalModelConfig(finalUrl, finalModel)
|
| 29 |
+
setStatus('Saved!')
|
| 30 |
+
} catch (err) {
|
| 31 |
+
setStatus(err instanceof Error ? err.message : String(err))
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<div className="flex flex-col gap-3 py-2">
|
| 37 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 38 |
+
Configure a local model server (Ollama, LM Studio, vLLM, etc.).
|
| 39 |
+
</div>
|
| 40 |
+
<div className="flex flex-col gap-2">
|
| 41 |
+
<div>
|
| 42 |
+
<label className="text-xs text-[var(--color-text-tertiary)] mb-1 block">Server URL</label>
|
| 43 |
+
<input
|
| 44 |
+
type="text"
|
| 45 |
+
value={url}
|
| 46 |
+
onChange={e => setUrl(e.target.value)}
|
| 47 |
+
placeholder={localBaseUrl || defaultUrl}
|
| 48 |
+
className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none"
|
| 49 |
+
/>
|
| 50 |
+
</div>
|
| 51 |
+
<div>
|
| 52 |
+
<label className="text-xs text-[var(--color-text-tertiary)] mb-1 block">Model Name</label>
|
| 53 |
+
<input
|
| 54 |
+
type="text"
|
| 55 |
+
value={modelName}
|
| 56 |
+
onChange={e => setModelName(e.target.value)}
|
| 57 |
+
placeholder={localModelName || 'default'}
|
| 58 |
+
className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none"
|
| 59 |
+
/>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
<div className="flex items-center gap-2">
|
| 63 |
+
<button
|
| 64 |
+
onClick={handleSave}
|
| 65 |
+
disabled={isLoading}
|
| 66 |
+
className="rounded-md bg-[var(--color-brand)] px-4 py-2 text-sm text-white hover:brightness-105 disabled:opacity-50"
|
| 67 |
+
>
|
| 68 |
+
{isLoading ? 'Saving...' : 'Save'}
|
| 69 |
+
</button>
|
| 70 |
+
</div>
|
| 71 |
+
{status && (
|
| 72 |
+
<div className={`text-xs ${status === 'Saved!' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 73 |
+
{status}
|
| 74 |
+
</div>
|
| 75 |
+
)}
|
| 76 |
+
</div>
|
| 77 |
+
)
|
| 78 |
+
}
|
desktop/src/components/settings/NvidiaLoginPanel.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react'
|
| 2 |
+
import { useCliAuthStore } from '../../stores/cliAuthStore'
|
| 3 |
+
|
| 4 |
+
export function NvidiaLoginPanel() {
|
| 5 |
+
const { nvidiaApiKey, saveNvidiaApiKey, isLoading } = useCliAuthStore()
|
| 6 |
+
const [apiKey, setApiKey] = useState('')
|
| 7 |
+
const [status, setStatus] = useState<string | null>(null)
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
if (nvidiaApiKey) setApiKey(nvidiaApiKey)
|
| 11 |
+
}, [nvidiaApiKey])
|
| 12 |
+
|
| 13 |
+
const handleSave = async () => {
|
| 14 |
+
if (!apiKey.trim()) { setStatus('Please enter an API key'); return }
|
| 15 |
+
setStatus(null)
|
| 16 |
+
try {
|
| 17 |
+
await saveNvidiaApiKey(apiKey.trim())
|
| 18 |
+
setStatus('Saved!')
|
| 19 |
+
} catch (err) {
|
| 20 |
+
setStatus(err instanceof Error ? err.message : String(err))
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const handleClear = async () => {
|
| 25 |
+
await useCliAuthStore.getState().clearAuth()
|
| 26 |
+
setApiKey('')
|
| 27 |
+
setStatus('Cleared')
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className="flex flex-col gap-3 py-2">
|
| 32 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 33 |
+
Paste your NVIDIA API key to use models from the NVIDIA API catalog via the OpenAI-compatible endpoint.
|
| 34 |
+
</div>
|
| 35 |
+
<div className="flex items-center gap-2">
|
| 36 |
+
<input
|
| 37 |
+
type="password"
|
| 38 |
+
value={apiKey}
|
| 39 |
+
onChange={e => setApiKey(e.target.value)}
|
| 40 |
+
placeholder={nvidiaApiKey ? '••••••••' : 'sk-...'}
|
| 41 |
+
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none"
|
| 42 |
+
/>
|
| 43 |
+
<button
|
| 44 |
+
onClick={handleSave}
|
| 45 |
+
disabled={isLoading}
|
| 46 |
+
className="rounded-md bg-[var(--color-brand)] px-4 py-2 text-sm text-white hover:brightness-105 disabled:opacity-50"
|
| 47 |
+
>
|
| 48 |
+
{isLoading ? 'Saving...' : 'Save'}
|
| 49 |
+
</button>
|
| 50 |
+
{nvidiaApiKey && (
|
| 51 |
+
<button
|
| 52 |
+
onClick={handleClear}
|
| 53 |
+
className="rounded-md border border-[var(--color-border)] px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]"
|
| 54 |
+
>
|
| 55 |
+
Clear
|
| 56 |
+
</button>
|
| 57 |
+
)}
|
| 58 |
+
</div>
|
| 59 |
+
{status && (
|
| 60 |
+
<div className={`text-xs ${status === 'Saved!' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 61 |
+
{status}
|
| 62 |
+
</div>
|
| 63 |
+
)}
|
| 64 |
+
</div>
|
| 65 |
+
)
|
| 66 |
+
}
|
desktop/src/components/settings/OpenAILoginPanel.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react'
|
| 2 |
+
import { useCliAuthStore } from '../../stores/cliAuthStore'
|
| 3 |
+
|
| 4 |
+
export function OpenAILoginPanel() {
|
| 5 |
+
const { openAiApiKey, saveOpenAIApiKey, isLoading } = useCliAuthStore()
|
| 6 |
+
const [apiKey, setApiKey] = useState('')
|
| 7 |
+
const [status, setStatus] = useState<string | null>(null)
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
if (openAiApiKey) setApiKey(openAiApiKey)
|
| 11 |
+
}, [openAiApiKey])
|
| 12 |
+
|
| 13 |
+
const handleSave = async () => {
|
| 14 |
+
if (!apiKey.trim()) { setStatus('Please enter an API key'); return }
|
| 15 |
+
setStatus(null)
|
| 16 |
+
try {
|
| 17 |
+
await saveOpenAIApiKey(apiKey.trim())
|
| 18 |
+
setStatus('Saved!')
|
| 19 |
+
} catch (err) {
|
| 20 |
+
setStatus(err instanceof Error ? err.message : String(err))
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const handleClear = async () => {
|
| 25 |
+
await useCliAuthStore.getState().clearAuth()
|
| 26 |
+
setApiKey('')
|
| 27 |
+
setStatus('Cleared')
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className="flex flex-col gap-3 py-2">
|
| 32 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 33 |
+
Paste your OpenAI API key to use OpenAI models with standard platform billing.
|
| 34 |
+
</div>
|
| 35 |
+
<div className="flex items-center gap-2">
|
| 36 |
+
<input
|
| 37 |
+
type="password"
|
| 38 |
+
value={apiKey}
|
| 39 |
+
onChange={e => setApiKey(e.target.value)}
|
| 40 |
+
placeholder={openAiApiKey ? '••••••••' : 'sk-...'}
|
| 41 |
+
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none"
|
| 42 |
+
/>
|
| 43 |
+
<button
|
| 44 |
+
onClick={handleSave}
|
| 45 |
+
disabled={isLoading}
|
| 46 |
+
className="rounded-md bg-[var(--color-brand)] px-4 py-2 text-sm text-white hover:brightness-105 disabled:opacity-50"
|
| 47 |
+
>
|
| 48 |
+
{isLoading ? 'Saving...' : 'Save'}
|
| 49 |
+
</button>
|
| 50 |
+
{openAiApiKey && (
|
| 51 |
+
<button
|
| 52 |
+
onClick={handleClear}
|
| 53 |
+
className="rounded-md border border-[var(--color-border)] px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]"
|
| 54 |
+
>
|
| 55 |
+
Clear
|
| 56 |
+
</button>
|
| 57 |
+
)}
|
| 58 |
+
</div>
|
| 59 |
+
{status && (
|
| 60 |
+
<div className={`text-xs ${status === 'Saved!' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 61 |
+
{status}
|
| 62 |
+
</div>
|
| 63 |
+
)}
|
| 64 |
+
</div>
|
| 65 |
+
)
|
| 66 |
+
}
|
desktop/src/components/settings/OpenCodeLoginPanel.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { useCliAuthStore } from '../../stores/cliAuthStore'
|
| 3 |
+
|
| 4 |
+
export function OpenCodeLoginPanel() {
|
| 5 |
+
const { openCodeApiKey, saveOpenCodeApiKey, isLoading } = useCliAuthStore()
|
| 6 |
+
const [mode, setMode] = useState<'menu' | 'api_key'>('menu')
|
| 7 |
+
const [apiKey, setApiKey] = useState('')
|
| 8 |
+
const [status, setStatus] = useState<string | null>(null)
|
| 9 |
+
|
| 10 |
+
const handleFreeModels = async () => {
|
| 11 |
+
setStatus(null)
|
| 12 |
+
try {
|
| 13 |
+
await saveOpenCodeApiKey('', '')
|
| 14 |
+
setStatus('Free models configured!')
|
| 15 |
+
} catch (err) {
|
| 16 |
+
setStatus(err instanceof Error ? err.message : String(err))
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const handleSave = async () => {
|
| 21 |
+
if (!apiKey.trim()) { setStatus('Please enter an API key'); return }
|
| 22 |
+
setStatus(null)
|
| 23 |
+
try {
|
| 24 |
+
await saveOpenCodeApiKey(apiKey.trim(), '')
|
| 25 |
+
setStatus('Saved!')
|
| 26 |
+
} catch (err) {
|
| 27 |
+
setStatus(err instanceof Error ? err.message : String(err))
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const handleClear = async () => {
|
| 32 |
+
await useCliAuthStore.getState().clearAuth()
|
| 33 |
+
setApiKey('')
|
| 34 |
+
setStatus('Cleared')
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (mode === 'api_key') {
|
| 38 |
+
return (
|
| 39 |
+
<div className="flex flex-col gap-3 py-2">
|
| 40 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 41 |
+
OpenCode Zen API keys use pay-as-you-go billing. Sign up at <a href="https://opencode.ai/zen" className="text-[var(--color-brand)]" target="_blank">opencode.ai/zen</a> to get your key.
|
| 42 |
+
</div>
|
| 43 |
+
<div className="flex items-center gap-2">
|
| 44 |
+
<input
|
| 45 |
+
type="password"
|
| 46 |
+
value={apiKey}
|
| 47 |
+
onChange={e => setApiKey(e.target.value)}
|
| 48 |
+
placeholder={openCodeApiKey ? '••••••••' : 'oc-...'}
|
| 49 |
+
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none"
|
| 50 |
+
/>
|
| 51 |
+
<button
|
| 52 |
+
onClick={handleSave}
|
| 53 |
+
disabled={isLoading}
|
| 54 |
+
className="rounded-md bg-[var(--color-brand)] px-4 py-2 text-sm text-white hover:brightness-105 disabled:opacity-50"
|
| 55 |
+
>
|
| 56 |
+
{isLoading ? 'Saving...' : 'Save'}
|
| 57 |
+
</button>
|
| 58 |
+
</div>
|
| 59 |
+
<button
|
| 60 |
+
onClick={() => { setMode('menu'); setApiKey(''); setStatus(null) }}
|
| 61 |
+
className="self-start text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]"
|
| 62 |
+
>
|
| 63 |
+
← Back to options
|
| 64 |
+
</button>
|
| 65 |
+
{status && (
|
| 66 |
+
<div className={`text-xs ${status === 'Saved!' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 67 |
+
{status}
|
| 68 |
+
</div>
|
| 69 |
+
)}
|
| 70 |
+
</div>
|
| 71 |
+
)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return (
|
| 75 |
+
<div className="flex flex-col gap-3 py-2">
|
| 76 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 77 |
+
OpenCode Zen offers free models (no API key needed) or paid models via API key.
|
| 78 |
+
</div>
|
| 79 |
+
{status && (
|
| 80 |
+
<div className={`text-xs ${status === 'Saved!' || status === 'Free models configured!' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 81 |
+
{status}
|
| 82 |
+
</div>
|
| 83 |
+
)}
|
| 84 |
+
<div className="flex flex-col gap-2">
|
| 85 |
+
<button
|
| 86 |
+
onClick={handleFreeModels}
|
| 87 |
+
disabled={isLoading}
|
| 88 |
+
className="flex items-center gap-3 rounded-lg border border-[var(--color-border)] px-4 py-3 text-left hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
| 89 |
+
>
|
| 90 |
+
<div>
|
| 91 |
+
<div className="text-sm font-medium text-[var(--color-text-primary)]">Use free models</div>
|
| 92 |
+
<div className="text-xs text-[var(--color-text-tertiary)]">No API key required</div>
|
| 93 |
+
</div>
|
| 94 |
+
</button>
|
| 95 |
+
<button
|
| 96 |
+
onClick={() => { setMode('api_key'); setApiKey('') }}
|
| 97 |
+
className="flex items-center gap-3 rounded-lg border border-[var(--color-border)] px-4 py-3 text-left hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)]"
|
| 98 |
+
>
|
| 99 |
+
<div>
|
| 100 |
+
<div className="text-sm font-medium text-[var(--color-text-primary)]">Paste OpenCode Zen API key</div>
|
| 101 |
+
<div className="text-xs text-[var(--color-text-tertiary)]">Access paid models, pay-as-you-go</div>
|
| 102 |
+
</div>
|
| 103 |
+
</button>
|
| 104 |
+
{openCodeApiKey && (
|
| 105 |
+
<button
|
| 106 |
+
onClick={handleClear}
|
| 107 |
+
className="self-start text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-error)]"
|
| 108 |
+
>
|
| 109 |
+
Clear credentials
|
| 110 |
+
</button>
|
| 111 |
+
)}
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
)
|
| 115 |
+
}
|
desktop/src/components/settings/OpenRouterLoginPanel.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react'
|
| 2 |
+
import { useCliAuthStore } from '../../stores/cliAuthStore'
|
| 3 |
+
|
| 4 |
+
export function OpenRouterLoginPanel() {
|
| 5 |
+
const { openRouterApiKey, saveOpenRouterApiKey, isLoading } = useCliAuthStore()
|
| 6 |
+
const [apiKey, setApiKey] = useState('')
|
| 7 |
+
const [status, setStatus] = useState<string | null>(null)
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
if (openRouterApiKey) setApiKey(openRouterApiKey)
|
| 11 |
+
}, [openRouterApiKey])
|
| 12 |
+
|
| 13 |
+
const handleSave = async () => {
|
| 14 |
+
if (!apiKey.trim()) { setStatus('Please enter an API key'); return }
|
| 15 |
+
setStatus(null)
|
| 16 |
+
try {
|
| 17 |
+
await saveOpenRouterApiKey(apiKey.trim())
|
| 18 |
+
setStatus('Saved!')
|
| 19 |
+
} catch (err) {
|
| 20 |
+
setStatus(err instanceof Error ? err.message : String(err))
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const handleClear = async () => {
|
| 25 |
+
await useCliAuthStore.getState().clearAuth()
|
| 26 |
+
setApiKey('')
|
| 27 |
+
setStatus('Cleared')
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className="flex flex-col gap-3 py-2">
|
| 32 |
+
<div className="text-sm text-[var(--color-text-secondary)]">
|
| 33 |
+
Paste your OpenRouter key to use the Anthropic-compatible OpenRouter base URL at <code>https://openrouter.ai/api</code>.
|
| 34 |
+
</div>
|
| 35 |
+
<div className="flex items-center gap-2">
|
| 36 |
+
<input
|
| 37 |
+
type="password"
|
| 38 |
+
value={apiKey}
|
| 39 |
+
onChange={e => setApiKey(e.target.value)}
|
| 40 |
+
placeholder={openRouterApiKey ? '••••••••' : 'sk-or-...'}
|
| 41 |
+
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none"
|
| 42 |
+
/>
|
| 43 |
+
<button
|
| 44 |
+
onClick={handleSave}
|
| 45 |
+
disabled={isLoading}
|
| 46 |
+
className="rounded-md bg-[var(--color-brand)] px-4 py-2 text-sm text-white hover:brightness-105 disabled:opacity-50"
|
| 47 |
+
>
|
| 48 |
+
{isLoading ? 'Saving...' : 'Save'}
|
| 49 |
+
</button>
|
| 50 |
+
{openRouterApiKey && (
|
| 51 |
+
<button
|
| 52 |
+
onClick={handleClear}
|
| 53 |
+
className="rounded-md border border-[var(--color-border)] px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]"
|
| 54 |
+
>
|
| 55 |
+
Clear
|
| 56 |
+
</button>
|
| 57 |
+
)}
|
| 58 |
+
</div>
|
| 59 |
+
{status && (
|
| 60 |
+
<div className={`text-xs ${status === 'Saved!' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 61 |
+
{status}
|
| 62 |
+
</div>
|
| 63 |
+
)}
|
| 64 |
+
</div>
|
| 65 |
+
)
|
| 66 |
+
}
|
desktop/src/main.tsx
CHANGED
|
@@ -7,6 +7,8 @@ import { runDesktopPersistenceMigrations } from './lib/persistenceMigrations'
|
|
| 7 |
declare global {
|
| 8 |
interface Window {
|
| 9 |
__CC_HAHA_BOOTSTRAPPED__?: boolean
|
|
|
|
|
|
|
| 10 |
__CC_HAHA_SHOW_STARTUP_ERROR__?: (reason: unknown) => void
|
| 11 |
}
|
| 12 |
}
|
|
@@ -32,7 +34,10 @@ export async function bootstrapDesktopApp(
|
|
| 32 |
loadModules: () => Promise<DesktopBootstrapModules> = loadDesktopBootstrapModules,
|
| 33 |
) {
|
| 34 |
try {
|
|
|
|
| 35 |
const [{ App }, { ErrorBoundary }, { installClientDiagnosticsCapture }, { initializeTheme }] = await loadModules()
|
|
|
|
|
|
|
| 36 |
initializeTheme()
|
| 37 |
installClientDiagnosticsCapture()
|
| 38 |
|
|
@@ -40,6 +45,7 @@ export async function bootstrapDesktopApp(
|
|
| 40 |
throw new Error('Desktop root element not found')
|
| 41 |
}
|
| 42 |
|
|
|
|
| 43 |
ReactDOM.createRoot(root).render(
|
| 44 |
<React.StrictMode>
|
| 45 |
<ErrorBoundary>
|
|
@@ -48,6 +54,7 @@ export async function bootstrapDesktopApp(
|
|
| 48 |
</React.StrictMode>,
|
| 49 |
)
|
| 50 |
window.__CC_HAHA_BOOTSTRAPPED__ = true
|
|
|
|
| 51 |
} catch (error) {
|
| 52 |
console.error('[desktop] Failed to bootstrap app', error)
|
| 53 |
if (root) {
|
|
@@ -60,7 +67,14 @@ export async function bootstrapDesktopApp(
|
|
| 60 |
}
|
| 61 |
}
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
|
|
|
| 66 |
void bootstrapDesktopApp()
|
|
|
|
| 7 |
declare global {
|
| 8 |
interface Window {
|
| 9 |
__CC_HAHA_BOOTSTRAPPED__?: boolean
|
| 10 |
+
__CC_HAHA_PAGE_LOADED__?: boolean
|
| 11 |
+
__CC_HAHA_STARTUP_ERROR_SHOWN__?: boolean
|
| 12 |
__CC_HAHA_SHOW_STARTUP_ERROR__?: (reason: unknown) => void
|
| 13 |
}
|
| 14 |
}
|
|
|
|
| 34 |
loadModules: () => Promise<DesktopBootstrapModules> = loadDesktopBootstrapModules,
|
| 35 |
) {
|
| 36 |
try {
|
| 37 |
+
console.log('[desktop] Loading desktop bootstrap modules...')
|
| 38 |
const [{ App }, { ErrorBoundary }, { installClientDiagnosticsCapture }, { initializeTheme }] = await loadModules()
|
| 39 |
+
console.log('[desktop] Bootstrap modules loaded')
|
| 40 |
+
|
| 41 |
initializeTheme()
|
| 42 |
installClientDiagnosticsCapture()
|
| 43 |
|
|
|
|
| 45 |
throw new Error('Desktop root element not found')
|
| 46 |
}
|
| 47 |
|
| 48 |
+
console.log('[desktop] Rendering React...')
|
| 49 |
ReactDOM.createRoot(root).render(
|
| 50 |
<React.StrictMode>
|
| 51 |
<ErrorBoundary>
|
|
|
|
| 54 |
</React.StrictMode>,
|
| 55 |
)
|
| 56 |
window.__CC_HAHA_BOOTSTRAPPED__ = true
|
| 57 |
+
console.log('[desktop] React rendered, app bootstrapped')
|
| 58 |
} catch (error) {
|
| 59 |
console.error('[desktop] Failed to bootstrap app', error)
|
| 60 |
if (root) {
|
|
|
|
| 67 |
}
|
| 68 |
}
|
| 69 |
|
| 70 |
+
console.log('[desktop] Starting migrations...')
|
| 71 |
+
const migrationReport = runDesktopPersistenceMigrations()
|
| 72 |
+
console.log('[desktop] Migrations complete', migrationReport)
|
| 73 |
+
|
| 74 |
+
console.log('[desktop] Initializing app zoom...')
|
| 75 |
+
const zoomPromise = initializeAppZoom()
|
| 76 |
+
zoomPromise.then(level => console.log('[desktop] App zoom initialized:', level))
|
| 77 |
+
.catch(err => console.error('[desktop] App zoom failed:', err))
|
| 78 |
|
| 79 |
+
console.log('[desktop] Bootstrapping desktop app...')
|
| 80 |
void bootstrapDesktopApp()
|
desktop/src/pages/Settings.tsx
CHANGED
|
@@ -2,17 +2,13 @@ import { useState, useEffect, useMemo, useRef, type CSSProperties, type ReactNod
|
|
| 2 |
import QRCode from 'qrcode'
|
| 3 |
import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react'
|
| 4 |
import { useSettingsStore, UI_ZOOM_DEFAULT, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore'
|
| 5 |
-
import { useProviderStore } from '../stores/providerStore'
|
| 6 |
import { useTranslation } from '../i18n'
|
| 7 |
-
import { Modal } from '../components/shared/Modal'
|
| 8 |
import { ConfirmDialog } from '../components/shared/ConfirmDialog'
|
| 9 |
import { Input } from '../components/shared/Input'
|
| 10 |
import { Button } from '../components/shared/Button'
|
| 11 |
import { Dropdown } from '../components/shared/Dropdown'
|
| 12 |
import type { PermissionMode, EffortLevel, ThemeMode, UpdateProxyMode, NetworkProxyMode, WebSearchMode, AppMode } from '../types/settings'
|
| 13 |
import type { Locale } from '../i18n'
|
| 14 |
-
import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping, ApiFormat, ProviderAuthStrategy } from '../types/provider'
|
| 15 |
-
import type { ProviderPreset } from '../types/providerPreset'
|
| 16 |
import { AdapterSettings } from './AdapterSettings'
|
| 17 |
import { useAgentStore } from '../stores/agentStore'
|
| 18 |
import { useSessionStore } from '../stores/sessionStore'
|
|
@@ -31,9 +27,7 @@ import { DiagnosticsSettings } from './DiagnosticsSettings'
|
|
| 31 |
import { ActivitySettings } from './ActivitySettings'
|
| 32 |
import { MemorySettings } from './MemorySettings'
|
| 33 |
import { useUIStore, type SettingsTab } from '../stores/uiStore'
|
| 34 |
-
import {
|
| 35 |
-
import { ChatGPTOfficialLogin } from '../components/settings/ChatGPTOfficialLogin'
|
| 36 |
-
import { OPENAI_OFFICIAL_PROVIDER_ID } from '../constants/openaiOfficialProvider'
|
| 37 |
import { useUpdateStore } from '../stores/updateStore'
|
| 38 |
import { formatBytes } from '../lib/formatBytes'
|
| 39 |
import { isTauriRuntime } from '../lib/desktopRuntime'
|
|
@@ -44,12 +38,6 @@ import {
|
|
| 44 |
requestDesktopNotificationPermission,
|
| 45 |
type DesktopNotificationPermission,
|
| 46 |
} from '../lib/desktopNotifications'
|
| 47 |
-
import {
|
| 48 |
-
API_KEY_JSON_PLACEHOLDER,
|
| 49 |
-
maskSettingsJsonSecrets,
|
| 50 |
-
restoreSettingsJsonSecrets,
|
| 51 |
-
stripProviderSettingsJsonEnv,
|
| 52 |
-
} from '../lib/providerSettingsJson'
|
| 53 |
import { copyTextToClipboard } from '../components/chat/clipboard'
|
| 54 |
|
| 55 |
const NETWORK_TIMEOUT_MIN_SECONDS = 5
|
|
@@ -204,1221 +192,9 @@ function TabButton({ icon, label, active, onClick }: { icon: string; label: stri
|
|
| 204 |
// ─── Provider Settings ──────────────────────────────────────
|
| 205 |
|
| 206 |
function ProviderSettings() {
|
| 207 |
-
|
| 208 |
-
providers,
|
| 209 |
-
activeId,
|
| 210 |
-
hasLoadedProviders,
|
| 211 |
-
presets,
|
| 212 |
-
isLoading,
|
| 213 |
-
isPresetsLoading,
|
| 214 |
-
fetchProviders,
|
| 215 |
-
fetchPresets,
|
| 216 |
-
deleteProvider,
|
| 217 |
-
activateProvider,
|
| 218 |
-
activateOfficial,
|
| 219 |
-
testProvider,
|
| 220 |
-
} = useProviderStore()
|
| 221 |
-
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
| 222 |
-
const t = useTranslation()
|
| 223 |
-
const [editingProvider, setEditingProvider] = useState<SavedProvider | null>(null)
|
| 224 |
-
const [showCreateModal, setShowCreateModal] = useState(false)
|
| 225 |
-
const [pendingDeleteProvider, setPendingDeleteProvider] = useState<SavedProvider | null>(null)
|
| 226 |
-
const [isDeletingProvider, setIsDeletingProvider] = useState(false)
|
| 227 |
-
const [testResults, setTestResults] = useState<Record<string, { loading: boolean; result?: ProviderTestResult }>>({})
|
| 228 |
-
|
| 229 |
-
useEffect(() => {
|
| 230 |
-
void fetchProviders()
|
| 231 |
-
void fetchPresets()
|
| 232 |
-
}, [fetchPresets, fetchProviders])
|
| 233 |
-
|
| 234 |
-
const presetMap = useMemo(
|
| 235 |
-
() => new Map(presets.map((preset) => [preset.id, preset])),
|
| 236 |
-
[presets],
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
const handleDelete = async (provider: SavedProvider) => {
|
| 240 |
-
if (activeId === provider.id) return
|
| 241 |
-
setPendingDeleteProvider(provider)
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
const confirmDelete = async () => {
|
| 245 |
-
if (!pendingDeleteProvider) return
|
| 246 |
-
setIsDeletingProvider(true)
|
| 247 |
-
try {
|
| 248 |
-
await deleteProvider(pendingDeleteProvider.id)
|
| 249 |
-
setPendingDeleteProvider(null)
|
| 250 |
-
} catch (error) {
|
| 251 |
-
console.error(error)
|
| 252 |
-
} finally {
|
| 253 |
-
setIsDeletingProvider(false)
|
| 254 |
-
}
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
const handleTest = async (provider: SavedProvider) => {
|
| 258 |
-
setTestResults((r) => ({ ...r, [provider.id]: { loading: true } }))
|
| 259 |
-
try {
|
| 260 |
-
const result = await testProvider(provider.id)
|
| 261 |
-
setTestResults((r) => ({ ...r, [provider.id]: { loading: false, result } }))
|
| 262 |
-
} catch {
|
| 263 |
-
setTestResults((r) => ({ ...r, [provider.id]: { loading: false, result: { connectivity: { success: false, latencyMs: 0, error: t('settings.providers.requestFailed') } } } }))
|
| 264 |
-
}
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
const handleActivate = async (id: string) => {
|
| 268 |
-
await activateProvider(id)
|
| 269 |
-
await fetchSettings()
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
const handleActivateOfficial = async () => {
|
| 273 |
-
await activateOfficial()
|
| 274 |
-
await fetchSettings()
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
const isClaudeOfficialActive = hasLoadedProviders && activeId === null
|
| 278 |
-
const isOpenAIOfficialActive = hasLoadedProviders && activeId === OPENAI_OFFICIAL_PROVIDER_ID
|
| 279 |
-
|
| 280 |
-
return (
|
| 281 |
-
<div className="max-w-2xl">
|
| 282 |
-
<div className="flex items-center justify-between mb-4">
|
| 283 |
-
<div>
|
| 284 |
-
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">{t('settings.providers.title')}</h2>
|
| 285 |
-
<p className="text-sm text-[var(--color-text-tertiary)] mt-0.5">{t('settings.providers.description')}</p>
|
| 286 |
-
</div>
|
| 287 |
-
<Button size="sm" onClick={() => setShowCreateModal(true)} disabled={isPresetsLoading || presets.length === 0}>
|
| 288 |
-
<span className="material-symbols-outlined text-[16px]">add</span>
|
| 289 |
-
{t('settings.providers.addProvider')}
|
| 290 |
-
</Button>
|
| 291 |
-
</div>
|
| 292 |
-
|
| 293 |
-
{/* Official provider — always visible at top */}
|
| 294 |
-
<div
|
| 295 |
-
data-testid="claude-official-provider"
|
| 296 |
-
className={`relative flex flex-col rounded-xl border transition-all mb-2 ${
|
| 297 |
-
isClaudeOfficialActive
|
| 298 |
-
? 'border-[var(--color-brand)] bg-[var(--color-surface-container)] shadow-[var(--shadow-focus-ring)]'
|
| 299 |
-
: 'border-[var(--color-border)] hover:border-[var(--color-border-focus)] cursor-pointer'
|
| 300 |
-
}`}
|
| 301 |
-
>
|
| 302 |
-
<div
|
| 303 |
-
className="flex items-center gap-4 px-4 py-3.5"
|
| 304 |
-
onClick={() => !isClaudeOfficialActive && handleActivateOfficial()}
|
| 305 |
-
>
|
| 306 |
-
<span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isClaudeOfficialActive ? 'bg-[var(--color-success)]' : 'bg-[var(--color-text-tertiary)]'}`} />
|
| 307 |
-
<div className="flex-1 min-w-0">
|
| 308 |
-
<div className="flex items-center gap-2">
|
| 309 |
-
<span className="text-sm font-semibold text-[var(--color-text-primary)]">{t('settings.providers.officialName')}</span>
|
| 310 |
-
{isClaudeOfficialActive && (
|
| 311 |
-
<span className="px-1.5 py-0.5 text-[10px] font-bold rounded border border-[var(--color-brand)]/18 bg-[var(--color-brand)]/14 text-[var(--color-brand)] leading-none">{t('settings.providers.default')}</span>
|
| 312 |
-
)}
|
| 313 |
-
</div>
|
| 314 |
-
<div className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{t('settings.providers.officialDesc')}</div>
|
| 315 |
-
</div>
|
| 316 |
-
</div>
|
| 317 |
-
|
| 318 |
-
{isClaudeOfficialActive && (
|
| 319 |
-
<div className="px-4 pb-4 pt-3 border-t border-[var(--color-border-separator)]">
|
| 320 |
-
<ClaudeOfficialLogin />
|
| 321 |
-
</div>
|
| 322 |
-
)}
|
| 323 |
-
</div>
|
| 324 |
-
|
| 325 |
-
<div
|
| 326 |
-
data-testid="openai-official-provider"
|
| 327 |
-
className={`relative flex flex-col rounded-xl border transition-all mb-2 ${
|
| 328 |
-
isOpenAIOfficialActive
|
| 329 |
-
? 'border-[var(--color-brand)] bg-[var(--color-surface-container)] shadow-[var(--shadow-focus-ring)]'
|
| 330 |
-
: 'border-[var(--color-border)] hover:border-[var(--color-border-focus)] cursor-pointer'
|
| 331 |
-
}`}
|
| 332 |
-
>
|
| 333 |
-
<div
|
| 334 |
-
className="flex items-center gap-4 px-4 py-3.5"
|
| 335 |
-
onClick={() => !isOpenAIOfficialActive && handleActivate(OPENAI_OFFICIAL_PROVIDER_ID)}
|
| 336 |
-
>
|
| 337 |
-
<span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isOpenAIOfficialActive ? 'bg-[var(--color-success)]' : 'bg-[var(--color-text-tertiary)]'}`} />
|
| 338 |
-
<div className="flex-1 min-w-0">
|
| 339 |
-
<div className="flex items-center gap-2">
|
| 340 |
-
<span className="text-sm font-semibold text-[var(--color-text-primary)]">{t('settings.providers.openaiOfficialName')}</span>
|
| 341 |
-
{isOpenAIOfficialActive && (
|
| 342 |
-
<span className="px-1.5 py-0.5 text-[10px] font-bold rounded border border-[var(--color-brand)]/18 bg-[var(--color-brand)]/14 text-[var(--color-brand)] leading-none">{t('settings.providers.default')}</span>
|
| 343 |
-
)}
|
| 344 |
-
</div>
|
| 345 |
-
<div className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{t('settings.providers.openaiOfficialDesc')}</div>
|
| 346 |
-
</div>
|
| 347 |
-
</div>
|
| 348 |
-
|
| 349 |
-
{isOpenAIOfficialActive && (
|
| 350 |
-
<div className="px-4 pb-4 pt-3 border-t border-[var(--color-border-separator)]">
|
| 351 |
-
<ChatGPTOfficialLogin />
|
| 352 |
-
</div>
|
| 353 |
-
)}
|
| 354 |
-
</div>
|
| 355 |
-
|
| 356 |
-
{/* Saved providers */}
|
| 357 |
-
{isLoading && providers.length === 0 ? (
|
| 358 |
-
<div className="flex justify-center py-8">
|
| 359 |
-
<div className="animate-spin w-5 h-5 border-2 border-[var(--color-brand)] border-t-transparent rounded-full" />
|
| 360 |
-
</div>
|
| 361 |
-
) : (
|
| 362 |
-
<div className="flex flex-col gap-2">
|
| 363 |
-
{providers.map((provider) => {
|
| 364 |
-
const isActive = activeId === provider.id
|
| 365 |
-
const test = testResults[provider.id]
|
| 366 |
-
const preset = presetMap.get(provider.presetId)
|
| 367 |
-
return (
|
| 368 |
-
<div
|
| 369 |
-
key={provider.id}
|
| 370 |
-
className={`relative flex items-center gap-4 px-4 py-3.5 rounded-xl border transition-all group ${
|
| 371 |
-
isActive
|
| 372 |
-
? 'border-[var(--color-brand)] bg-[var(--color-surface-container)] shadow-[var(--shadow-focus-ring)]'
|
| 373 |
-
: 'border-[var(--color-border)] hover:border-[var(--color-border-focus)]'
|
| 374 |
-
}`}
|
| 375 |
-
>
|
| 376 |
-
<span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${isActive ? 'bg-[var(--color-success)]' : 'bg-[var(--color-text-tertiary)]'}`} />
|
| 377 |
-
<div className="flex-1 min-w-0">
|
| 378 |
-
<div className="flex items-center gap-2">
|
| 379 |
-
<span className="text-sm font-semibold text-[var(--color-text-primary)] truncate">{provider.name}</span>
|
| 380 |
-
{preset && preset.id !== 'custom' && (
|
| 381 |
-
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-[var(--color-surface-container-high)] text-[var(--color-text-tertiary)] leading-none">{preset.name}</span>
|
| 382 |
-
)}
|
| 383 |
-
{provider.apiFormat && provider.apiFormat !== 'anthropic' && (
|
| 384 |
-
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-[var(--color-surface-container-high)] text-[var(--color-warning)] leading-none">
|
| 385 |
-
{provider.apiFormat === 'openai_chat' ? 'OpenAI Chat' : 'OpenAI Responses'}
|
| 386 |
-
</span>
|
| 387 |
-
)}
|
| 388 |
-
{isActive && (
|
| 389 |
-
<span className="px-1.5 py-0.5 text-[10px] font-bold rounded border border-[var(--color-brand)]/18 bg-[var(--color-brand)]/14 text-[var(--color-brand)] leading-none">{t('settings.providers.default')}</span>
|
| 390 |
-
)}
|
| 391 |
-
</div>
|
| 392 |
-
<div className="text-xs text-[var(--color-text-tertiary)] truncate mt-0.5">
|
| 393 |
-
{provider.baseUrl} · {provider.models.main}
|
| 394 |
-
</div>
|
| 395 |
-
{test && !test.loading && test.result && (
|
| 396 |
-
<div className="text-xs mt-1 flex flex-col gap-0.5">
|
| 397 |
-
<span className={test.result.connectivity.success ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}>
|
| 398 |
-
{test.result.connectivity.success
|
| 399 |
-
? t('settings.providers.connectivityOk', { latency: String(test.result.connectivity.latencyMs) })
|
| 400 |
-
: t('settings.providers.connectivityFailed', { error: test.result.connectivity.error || '' })}
|
| 401 |
-
</span>
|
| 402 |
-
{test.result.proxy && (
|
| 403 |
-
<span className={test.result.proxy.success ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}>
|
| 404 |
-
{test.result.proxy.success
|
| 405 |
-
? t('settings.providers.proxyOk', { latency: String(test.result.proxy.latencyMs) })
|
| 406 |
-
: t('settings.providers.proxyFailed', { error: test.result.proxy.error || '' })}
|
| 407 |
-
</span>
|
| 408 |
-
)}
|
| 409 |
-
</div>
|
| 410 |
-
)}
|
| 411 |
-
</div>
|
| 412 |
-
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
| 413 |
-
{!isActive && (
|
| 414 |
-
<Button variant="ghost" size="sm" onClick={() => handleActivate(provider.id)}>{t('settings.providers.setDefault')}</Button>
|
| 415 |
-
)}
|
| 416 |
-
<Button variant="ghost" size="sm" onClick={() => handleTest(provider)} loading={test?.loading}>{t('settings.providers.test')}</Button>
|
| 417 |
-
<Button variant="ghost" size="sm" onClick={() => setEditingProvider(provider)}>{t('settings.providers.edit')}</Button>
|
| 418 |
-
{!isActive && (
|
| 419 |
-
<Button variant="ghost" size="sm" onClick={() => handleDelete(provider)} className="text-[var(--color-error)] hover:text-[var(--color-error)]">{t('common.delete')}</Button>
|
| 420 |
-
)}
|
| 421 |
-
</div>
|
| 422 |
-
</div>
|
| 423 |
-
)
|
| 424 |
-
})}
|
| 425 |
-
</div>
|
| 426 |
-
)}
|
| 427 |
-
|
| 428 |
-
{/* Create Modal — conditionally rendered so state resets on close */}
|
| 429 |
-
{showCreateModal && (
|
| 430 |
-
<ProviderFormModal open={true} onClose={() => setShowCreateModal(false)} mode="create" presets={presets} />
|
| 431 |
-
)}
|
| 432 |
-
|
| 433 |
-
{/* Edit Modal */}
|
| 434 |
-
{editingProvider && (
|
| 435 |
-
<ProviderFormModal key={editingProvider.id} open={true} onClose={() => setEditingProvider(null)} mode="edit" provider={editingProvider} presets={presets} />
|
| 436 |
-
)}
|
| 437 |
-
|
| 438 |
-
<ConfirmDialog
|
| 439 |
-
open={pendingDeleteProvider !== null}
|
| 440 |
-
onClose={() => {
|
| 441 |
-
if (isDeletingProvider) return
|
| 442 |
-
setPendingDeleteProvider(null)
|
| 443 |
-
}}
|
| 444 |
-
onConfirm={confirmDelete}
|
| 445 |
-
title={t('common.delete')}
|
| 446 |
-
body={pendingDeleteProvider ? t('settings.providers.confirmDelete', { name: pendingDeleteProvider.name }) : ''}
|
| 447 |
-
confirmLabel={t('common.delete')}
|
| 448 |
-
cancelLabel={t('common.cancel')}
|
| 449 |
-
confirmVariant="danger"
|
| 450 |
-
loading={isDeletingProvider}
|
| 451 |
-
/>
|
| 452 |
-
</div>
|
| 453 |
-
)
|
| 454 |
-
}
|
| 455 |
-
|
| 456 |
-
// ─── Provider Form Modal ──────────────────────────────────────
|
| 457 |
-
|
| 458 |
-
type ProviderFormProps = {
|
| 459 |
-
open: boolean
|
| 460 |
-
onClose: () => void
|
| 461 |
-
mode: 'create' | 'edit'
|
| 462 |
-
provider?: SavedProvider
|
| 463 |
-
presets: ProviderPreset[]
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
function requirePreset(preset: ProviderPreset | undefined): ProviderPreset {
|
| 467 |
-
if (!preset) {
|
| 468 |
-
throw new Error('Provider presets are not configured')
|
| 469 |
-
}
|
| 470 |
-
return preset
|
| 471 |
-
}
|
| 472 |
-
|
| 473 |
-
const AUTO_COMPACT_WINDOW_ENV_KEY = 'CLAUDE_CODE_AUTO_COMPACT_WINDOW'
|
| 474 |
-
const MODEL_CONTEXT_WINDOWS_ENV_KEY = 'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS'
|
| 475 |
-
const MODEL_CONTEXT_WINDOW_MIN = 16000
|
| 476 |
-
const MODEL_CONTEXT_WINDOW_MAX = 10000000
|
| 477 |
-
const MODEL_SLOTS = ['main', 'haiku', 'sonnet', 'opus'] as const
|
| 478 |
-
const DEFAULT_PROVIDER_AUTH_STRATEGY: ProviderAuthStrategy = 'auth_token'
|
| 479 |
-
const AUTH_ENV_KEYS = new Set(['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'])
|
| 480 |
-
type ModelSlot = typeof MODEL_SLOTS[number]
|
| 481 |
-
type ModelContextInputs = Record<ModelSlot, string>
|
| 482 |
-
|
| 483 |
-
function formatContextWindow(value: number): string {
|
| 484 |
-
return value.toLocaleString('en-US')
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
function getPresetAutoCompactWindow(preset: ProviderPreset): string {
|
| 488 |
-
return preset.defaultEnv?.[AUTO_COMPACT_WINDOW_ENV_KEY] ?? ''
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
function getPresetAuthStrategy(preset: ProviderPreset): ProviderAuthStrategy {
|
| 492 |
-
return preset.authStrategy ?? DEFAULT_PROVIDER_AUTH_STRATEGY
|
| 493 |
-
}
|
| 494 |
-
|
| 495 |
-
function omitAuthEnv(env: Record<string, string> | undefined): Record<string, string> {
|
| 496 |
-
if (!env) return {}
|
| 497 |
-
return Object.fromEntries(
|
| 498 |
-
Object.entries(env).filter(([key]) => !AUTH_ENV_KEYS.has(key.toUpperCase())),
|
| 499 |
-
)
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
function getProviderAuthValue(apiKey: string, preset: ProviderPreset): string {
|
| 503 |
-
return apiKey || preset.defaultEnv?.ANTHROPIC_AUTH_TOKEN || preset.defaultEnv?.ANTHROPIC_API_KEY || (preset.needsApiKey ? '(your API key)' : '')
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
function buildSettingsJsonAuthEnv(
|
| 507 |
-
apiFormat: ApiFormat,
|
| 508 |
-
authStrategy: ProviderAuthStrategy,
|
| 509 |
-
apiKey: string,
|
| 510 |
-
preset: ProviderPreset,
|
| 511 |
-
): Record<string, string> {
|
| 512 |
-
if (apiFormat !== 'anthropic') {
|
| 513 |
-
return { ANTHROPIC_API_KEY: 'proxy-managed' }
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
const value = getProviderAuthValue(apiKey, preset)
|
| 517 |
-
switch (authStrategy) {
|
| 518 |
-
case 'api_key':
|
| 519 |
-
return value ? { ANTHROPIC_API_KEY: value } : {}
|
| 520 |
-
case 'auth_token':
|
| 521 |
-
return value ? { ANTHROPIC_AUTH_TOKEN: value } : {}
|
| 522 |
-
case 'auth_token_empty_api_key':
|
| 523 |
-
return {
|
| 524 |
-
ANTHROPIC_API_KEY: '',
|
| 525 |
-
...(value ? { ANTHROPIC_AUTH_TOKEN: value } : {}),
|
| 526 |
-
}
|
| 527 |
-
case 'dual_same_token':
|
| 528 |
-
return value ? { ANTHROPIC_API_KEY: value, ANTHROPIC_AUTH_TOKEN: value } : {}
|
| 529 |
-
case 'dual_dummy':
|
| 530 |
-
return { ANTHROPIC_API_KEY: 'dummy', ANTHROPIC_AUTH_TOKEN: 'dummy' }
|
| 531 |
-
}
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
function inferAuthStrategyFromEnv(env: Record<string, string>): ProviderAuthStrategy | null {
|
| 535 |
-
if (env.ANTHROPIC_API_KEY === 'dummy' && env.ANTHROPIC_AUTH_TOKEN === 'dummy') return 'dual_dummy'
|
| 536 |
-
if (env.ANTHROPIC_API_KEY === '' && env.ANTHROPIC_AUTH_TOKEN) return 'auth_token_empty_api_key'
|
| 537 |
-
if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_AUTH_TOKEN && env.ANTHROPIC_API_KEY === env.ANTHROPIC_AUTH_TOKEN) return 'dual_same_token'
|
| 538 |
-
if (env.ANTHROPIC_AUTH_TOKEN) return 'auth_token'
|
| 539 |
-
if (env.ANTHROPIC_API_KEY) return 'api_key'
|
| 540 |
-
return null
|
| 541 |
-
}
|
| 542 |
-
|
| 543 |
-
function parseAutoCompactWindowInput(value: string): number | undefined {
|
| 544 |
-
const trimmed = value.trim()
|
| 545 |
-
if (!trimmed) return undefined
|
| 546 |
-
const parsed = Number(trimmed)
|
| 547 |
-
if (!Number.isInteger(parsed)) return undefined
|
| 548 |
-
if (parsed < MODEL_CONTEXT_WINDOW_MIN || parsed > MODEL_CONTEXT_WINDOW_MAX) return undefined
|
| 549 |
-
return parsed
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
function getAutoCompactWindowErrorKey(value: string): 'number' | 'range' | null {
|
| 553 |
-
const trimmed = value.trim()
|
| 554 |
-
if (!trimmed) return null
|
| 555 |
-
const parsed = Number(trimmed)
|
| 556 |
-
if (!Number.isInteger(parsed)) return 'number'
|
| 557 |
-
if (parsed < MODEL_CONTEXT_WINDOW_MIN || parsed > MODEL_CONTEXT_WINDOW_MAX) return 'range'
|
| 558 |
-
return null
|
| 559 |
-
}
|
| 560 |
-
|
| 561 |
-
function parseModelContextWindowsInput(value: string): number | undefined {
|
| 562 |
-
return parseAutoCompactWindowInput(value)
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
function getModelContextWindowErrorKey(value: string): 'number' | 'range' | null {
|
| 566 |
-
return getAutoCompactWindowErrorKey(value)
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
function getModelContextInputValue(
|
| 570 |
-
model: string | undefined,
|
| 571 |
-
preset: ProviderPreset,
|
| 572 |
-
provider?: SavedProvider,
|
| 573 |
-
): string {
|
| 574 |
-
const trimmedModel = model?.trim()
|
| 575 |
-
if (!trimmedModel) return ''
|
| 576 |
-
const value = provider?.modelContextWindows?.[trimmedModel] ?? preset.modelContextWindows?.[trimmedModel]
|
| 577 |
-
return value !== undefined ? String(value) : ''
|
| 578 |
-
}
|
| 579 |
-
|
| 580 |
-
function getModelContextInputs(
|
| 581 |
-
models: ModelMapping,
|
| 582 |
-
preset: ProviderPreset,
|
| 583 |
-
provider?: SavedProvider,
|
| 584 |
-
): ModelContextInputs {
|
| 585 |
-
const inputs = {} as ModelContextInputs
|
| 586 |
-
for (const slot of MODEL_SLOTS) {
|
| 587 |
-
inputs[slot] = getModelContextInputValue(models[slot], preset, provider)
|
| 588 |
-
}
|
| 589 |
-
return inputs
|
| 590 |
-
}
|
| 591 |
-
|
| 592 |
-
function buildModelContextWindows(
|
| 593 |
-
models: ModelMapping,
|
| 594 |
-
inputs: ModelContextInputs,
|
| 595 |
-
): Record<string, number> {
|
| 596 |
-
const windows: Record<string, number> = {}
|
| 597 |
-
for (const slot of MODEL_SLOTS) {
|
| 598 |
-
const model = models[slot]?.trim()
|
| 599 |
-
const parsed = parseModelContextWindowsInput(inputs[slot])
|
| 600 |
-
if (model && parsed !== undefined) {
|
| 601 |
-
windows[model] = parsed
|
| 602 |
-
}
|
| 603 |
-
}
|
| 604 |
-
return windows
|
| 605 |
-
}
|
| 606 |
-
|
| 607 |
-
function normalizeModelMapping(models: ModelMapping): ModelMapping {
|
| 608 |
-
const main = models.main.trim()
|
| 609 |
-
return {
|
| 610 |
-
main,
|
| 611 |
-
haiku: models.haiku.trim() || main,
|
| 612 |
-
sonnet: models.sonnet.trim() || main,
|
| 613 |
-
opus: models.opus.trim() || main,
|
| 614 |
-
}
|
| 615 |
-
}
|
| 616 |
-
|
| 617 |
-
function updateSettingsJsonAutoCompactWindow(raw: string, value: string): string {
|
| 618 |
-
try {
|
| 619 |
-
const parsed = JSON.parse(raw || '{}') as { env?: Record<string, unknown> }
|
| 620 |
-
const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
|
| 621 |
-
? parsed.env
|
| 622 |
-
: {}
|
| 623 |
-
const env = { ...existingEnv }
|
| 624 |
-
const trimmed = value.trim()
|
| 625 |
-
if (trimmed) {
|
| 626 |
-
env[AUTO_COMPACT_WINDOW_ENV_KEY] = trimmed
|
| 627 |
-
} else {
|
| 628 |
-
delete env[AUTO_COMPACT_WINDOW_ENV_KEY]
|
| 629 |
-
}
|
| 630 |
-
parsed.env = env
|
| 631 |
-
return JSON.stringify(parsed, null, 2)
|
| 632 |
-
} catch {
|
| 633 |
-
return raw
|
| 634 |
-
}
|
| 635 |
-
}
|
| 636 |
-
|
| 637 |
-
function updateSettingsJsonModelContextWindows(
|
| 638 |
-
raw: string,
|
| 639 |
-
modelContextWindows: Record<string, number>,
|
| 640 |
-
): string {
|
| 641 |
-
try {
|
| 642 |
-
const parsed = JSON.parse(raw || '{}') as { env?: Record<string, unknown> }
|
| 643 |
-
const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
|
| 644 |
-
? parsed.env
|
| 645 |
-
: {}
|
| 646 |
-
const env = { ...existingEnv }
|
| 647 |
-
if (Object.keys(modelContextWindows).length > 0) {
|
| 648 |
-
env[MODEL_CONTEXT_WINDOWS_ENV_KEY] = JSON.stringify(modelContextWindows)
|
| 649 |
-
} else {
|
| 650 |
-
delete env[MODEL_CONTEXT_WINDOWS_ENV_KEY]
|
| 651 |
-
}
|
| 652 |
-
parsed.env = env
|
| 653 |
-
return JSON.stringify(parsed, null, 2)
|
| 654 |
-
} catch {
|
| 655 |
-
return raw
|
| 656 |
-
}
|
| 657 |
-
}
|
| 658 |
-
|
| 659 |
-
function updateSettingsJsonModels(raw: string, models: ModelMapping): string {
|
| 660 |
-
try {
|
| 661 |
-
const parsed = JSON.parse(raw || '{}') as { env?: Record<string, unknown> }
|
| 662 |
-
const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
|
| 663 |
-
? parsed.env
|
| 664 |
-
: {}
|
| 665 |
-
parsed.env = {
|
| 666 |
-
...existingEnv,
|
| 667 |
-
ANTHROPIC_MODEL: models.main,
|
| 668 |
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
| 669 |
-
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
| 670 |
-
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
| 671 |
-
}
|
| 672 |
-
return JSON.stringify(parsed, null, 2)
|
| 673 |
-
} catch {
|
| 674 |
-
return raw
|
| 675 |
-
}
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
function updateSettingsJsonProviderConnection(
|
| 679 |
-
raw: string,
|
| 680 |
-
apiFormat: ApiFormat,
|
| 681 |
-
authStrategy: ProviderAuthStrategy,
|
| 682 |
-
apiKey: string,
|
| 683 |
-
preset: ProviderPreset,
|
| 684 |
-
baseUrl: string,
|
| 685 |
-
): string {
|
| 686 |
-
try {
|
| 687 |
-
const parsed = JSON.parse(raw || '{}') as { env?: Record<string, unknown> }
|
| 688 |
-
const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
|
| 689 |
-
? parsed.env
|
| 690 |
-
: {}
|
| 691 |
-
const env = { ...existingEnv }
|
| 692 |
-
delete env.ANTHROPIC_API_KEY
|
| 693 |
-
delete env.ANTHROPIC_AUTH_TOKEN
|
| 694 |
-
env.ANTHROPIC_BASE_URL = apiFormat !== 'anthropic' ? 'http://127.0.0.1:3456/proxy' : baseUrl
|
| 695 |
-
Object.assign(env, buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, preset))
|
| 696 |
-
parsed.env = env
|
| 697 |
-
return JSON.stringify(parsed, null, 2)
|
| 698 |
-
} catch {
|
| 699 |
-
return raw
|
| 700 |
-
}
|
| 701 |
-
}
|
| 702 |
-
|
| 703 |
-
function buildFallbackPreset(provider?: SavedProvider): ProviderPreset {
|
| 704 |
-
return {
|
| 705 |
-
id: provider?.presetId ?? 'custom',
|
| 706 |
-
name: provider?.name ?? 'Custom',
|
| 707 |
-
baseUrl: provider?.baseUrl ?? '',
|
| 708 |
-
apiFormat: provider?.apiFormat ?? 'anthropic',
|
| 709 |
-
authStrategy: provider?.authStrategy,
|
| 710 |
-
defaultModels: provider?.models ?? { main: '', haiku: '', sonnet: '', opus: '' },
|
| 711 |
-
modelContextWindows: provider?.modelContextWindows,
|
| 712 |
-
defaultEnv: provider?.autoCompactWindow !== undefined
|
| 713 |
-
? { [AUTO_COMPACT_WINDOW_ENV_KEY]: String(provider.autoCompactWindow) }
|
| 714 |
-
: undefined,
|
| 715 |
-
needsApiKey: true,
|
| 716 |
-
websiteUrl: '',
|
| 717 |
-
}
|
| 718 |
-
}
|
| 719 |
-
|
| 720 |
-
function openExternalUrl(url: string) {
|
| 721 |
-
if (!isTauriRuntime()) {
|
| 722 |
-
window.open(url, '_blank', 'noopener,noreferrer')
|
| 723 |
-
return
|
| 724 |
-
}
|
| 725 |
-
|
| 726 |
-
void import('@tauri-apps/plugin-shell')
|
| 727 |
-
.then((mod) => mod.open(url))
|
| 728 |
-
.catch(() => window.open(url, '_blank', 'noopener,noreferrer'))
|
| 729 |
-
}
|
| 730 |
-
|
| 731 |
-
function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderFormProps) {
|
| 732 |
-
const { createProvider, updateProvider, testConfig } = useProviderStore()
|
| 733 |
-
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
| 734 |
-
const t = useTranslation()
|
| 735 |
-
|
| 736 |
-
const availablePresets = presets.filter((p) => p.id !== 'official')
|
| 737 |
-
const regularPresets = availablePresets.filter((p) => !p.featured)
|
| 738 |
-
const featuredPresets = availablePresets.filter((p) => p.featured)
|
| 739 |
-
const presetDefaultEnvKeys = useMemo(
|
| 740 |
-
() => presets.flatMap((preset) => Object.keys(preset.defaultEnv ?? {})),
|
| 741 |
-
[presets],
|
| 742 |
-
)
|
| 743 |
-
const fallbackPreset = provider
|
| 744 |
-
? buildFallbackPreset(provider)
|
| 745 |
-
: requirePreset(availablePresets[availablePresets.length - 1])
|
| 746 |
-
const initialPreset = requirePreset(
|
| 747 |
-
provider
|
| 748 |
-
? availablePresets.find((p) => p.id === provider.presetId) ?? fallbackPreset
|
| 749 |
-
: availablePresets[0] ?? fallbackPreset,
|
| 750 |
-
)
|
| 751 |
-
|
| 752 |
-
const [selectedPreset, setSelectedPreset] = useState<ProviderPreset>(initialPreset)
|
| 753 |
-
const [name, setName] = useState(provider?.name ?? initialPreset.name)
|
| 754 |
-
const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? initialPreset.baseUrl)
|
| 755 |
-
const [apiFormat, setApiFormat] = useState<ApiFormat>(provider?.apiFormat ?? initialPreset.apiFormat ?? 'anthropic')
|
| 756 |
-
const [authStrategy, setAuthStrategy] = useState<ProviderAuthStrategy>(provider?.authStrategy ?? getPresetAuthStrategy(initialPreset))
|
| 757 |
-
const [apiKey, setApiKey] = useState(provider?.apiKey ?? '')
|
| 758 |
-
const [showApiKey, setShowApiKey] = useState(false)
|
| 759 |
-
const [notes, setNotes] = useState(provider?.notes ?? '')
|
| 760 |
-
const [models, setModels] = useState<ModelMapping>(provider?.models ?? { ...initialPreset.defaultModels })
|
| 761 |
-
const [modelContextInputs, setModelContextInputs] = useState<ModelContextInputs>(
|
| 762 |
-
getModelContextInputs(provider?.models ?? initialPreset.defaultModels, initialPreset, provider),
|
| 763 |
-
)
|
| 764 |
-
const [autoCompactWindow, setAutoCompactWindow] = useState(
|
| 765 |
-
provider?.autoCompactWindow !== undefined
|
| 766 |
-
? String(provider.autoCompactWindow)
|
| 767 |
-
: getPresetAutoCompactWindow(initialPreset),
|
| 768 |
-
)
|
| 769 |
-
const [showContextSettings, setShowContextSettings] = useState(false)
|
| 770 |
-
const [isSubmitting, setIsSubmitting] = useState(false)
|
| 771 |
-
const [testResult, setTestResult] = useState<ProviderTestResult | null>(null)
|
| 772 |
-
const [isTesting, setIsTesting] = useState(false)
|
| 773 |
-
const [settingsJson, setSettingsJson] = useState('')
|
| 774 |
-
const [settingsJsonError, setSettingsJsonError] = useState<string | null>(null)
|
| 775 |
-
const jsonPastedRef = useRef(false)
|
| 776 |
-
|
| 777 |
-
// Load current settings.json and merge provider env vars
|
| 778 |
-
useEffect(() => {
|
| 779 |
-
// Skip if JSON was just populated by user paste
|
| 780 |
-
if (jsonPastedRef.current) {
|
| 781 |
-
jsonPastedRef.current = false
|
| 782 |
-
return
|
| 783 |
-
}
|
| 784 |
-
import('../api/providers').then(({ providersApi }) => {
|
| 785 |
-
providersApi.getSettings().then((settings) => {
|
| 786 |
-
const needsProxy = apiFormat !== 'anthropic'
|
| 787 |
-
const autoCompactWindowEnv = autoCompactWindow.trim()
|
| 788 |
-
const modelContextWindows = buildModelContextWindows(models, modelContextInputs)
|
| 789 |
-
const normalizedModels = normalizeModelMapping(models)
|
| 790 |
-
const existingEnv = (settings.env as Record<string, string>) || {}
|
| 791 |
-
const cleanedEnv = stripProviderSettingsJsonEnv(existingEnv, presetDefaultEnvKeys)
|
| 792 |
-
const merged = {
|
| 793 |
-
...settings,
|
| 794 |
-
skipWebFetchPreflight: settings.skipWebFetchPreflight ?? true,
|
| 795 |
-
env: {
|
| 796 |
-
...cleanedEnv,
|
| 797 |
-
...omitAuthEnv(selectedPreset.defaultEnv),
|
| 798 |
-
...(autoCompactWindowEnv ? { [AUTO_COMPACT_WINDOW_ENV_KEY]: autoCompactWindowEnv } : {}),
|
| 799 |
-
...(Object.keys(modelContextWindows).length > 0
|
| 800 |
-
? { [MODEL_CONTEXT_WINDOWS_ENV_KEY]: JSON.stringify(modelContextWindows) }
|
| 801 |
-
: {}),
|
| 802 |
-
ANTHROPIC_BASE_URL: needsProxy ? 'http://127.0.0.1:3456/proxy' : baseUrl,
|
| 803 |
-
...buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, selectedPreset),
|
| 804 |
-
ANTHROPIC_MODEL: normalizedModels.main,
|
| 805 |
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: normalizedModels.haiku,
|
| 806 |
-
ANTHROPIC_DEFAULT_SONNET_MODEL: normalizedModels.sonnet,
|
| 807 |
-
ANTHROPIC_DEFAULT_OPUS_MODEL: normalizedModels.opus,
|
| 808 |
-
},
|
| 809 |
-
}
|
| 810 |
-
setSettingsJson(JSON.stringify(merged, null, 2))
|
| 811 |
-
}).catch(() => {
|
| 812 |
-
setSettingsJson(JSON.stringify({}, null, 2))
|
| 813 |
-
})
|
| 814 |
-
})
|
| 815 |
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 816 |
-
}, [selectedPreset.id])
|
| 817 |
-
|
| 818 |
-
const handlePresetChange = (preset: ProviderPreset) => {
|
| 819 |
-
setSelectedPreset(preset)
|
| 820 |
-
setName(preset.name)
|
| 821 |
-
setBaseUrl(preset.baseUrl)
|
| 822 |
-
setApiFormat(preset.apiFormat ?? 'anthropic')
|
| 823 |
-
setAuthStrategy(getPresetAuthStrategy(preset))
|
| 824 |
-
setModels({ ...preset.defaultModels })
|
| 825 |
-
setModelContextInputs(getModelContextInputs(preset.defaultModels, preset))
|
| 826 |
-
setAutoCompactWindow(getPresetAutoCompactWindow(preset))
|
| 827 |
-
setShowContextSettings(false)
|
| 828 |
-
setTestResult(null)
|
| 829 |
-
}
|
| 830 |
-
|
| 831 |
-
const isCustom = selectedPreset.id === 'custom'
|
| 832 |
-
const requiresApiKey = selectedPreset.needsApiKey !== false
|
| 833 |
-
const autoCompactWindowErrorKey = getAutoCompactWindowErrorKey(autoCompactWindow)
|
| 834 |
-
const modelContextWindowErrorSlots = MODEL_SLOTS.filter((slot) => getModelContextWindowErrorKey(modelContextInputs[slot]))
|
| 835 |
-
const canSubmit = name.trim() && baseUrl.trim() && (mode === 'edit' || !requiresApiKey || apiKey.trim()) && models.main.trim() && !settingsJsonError && !autoCompactWindowErrorKey && modelContextWindowErrorSlots.length === 0
|
| 836 |
-
const apiKeyUrl = selectedPreset.apiKeyUrl?.trim()
|
| 837 |
-
const promoText = selectedPreset.promoText?.trim()
|
| 838 |
-
const displayedSettingsJson = showApiKey
|
| 839 |
-
? settingsJson
|
| 840 |
-
: maskSettingsJsonSecrets(settingsJson)
|
| 841 |
-
const apiFormatItems = [
|
| 842 |
-
{
|
| 843 |
-
value: 'anthropic' as const,
|
| 844 |
-
label: t('settings.providers.apiFormatAnthropic'),
|
| 845 |
-
icon: <span className="material-symbols-outlined text-[17px]">hub</span>,
|
| 846 |
-
},
|
| 847 |
-
{
|
| 848 |
-
value: 'openai_chat' as const,
|
| 849 |
-
label: t('settings.providers.apiFormatOpenaiChat'),
|
| 850 |
-
icon: <span className="material-symbols-outlined text-[17px]">forum</span>,
|
| 851 |
-
},
|
| 852 |
-
{
|
| 853 |
-
value: 'openai_responses' as const,
|
| 854 |
-
label: t('settings.providers.apiFormatOpenaiResponses'),
|
| 855 |
-
icon: <span className="material-symbols-outlined text-[17px]">route</span>,
|
| 856 |
-
},
|
| 857 |
-
]
|
| 858 |
-
const selectedApiFormatLabel = apiFormatItems.find((item) => item.value === apiFormat)?.label ?? t('settings.providers.apiFormatAnthropic')
|
| 859 |
-
const authStrategyItems = [
|
| 860 |
-
{
|
| 861 |
-
value: 'auth_token' as const,
|
| 862 |
-
label: t('settings.providers.authStrategyAuthToken'),
|
| 863 |
-
description: t('settings.providers.authStrategyAuthTokenDesc'),
|
| 864 |
-
icon: <span className="material-symbols-outlined text-[17px]">key</span>,
|
| 865 |
-
},
|
| 866 |
-
{
|
| 867 |
-
value: 'auth_token_empty_api_key' as const,
|
| 868 |
-
label: t('settings.providers.authStrategyAuthTokenEmptyApiKey'),
|
| 869 |
-
description: t('settings.providers.authStrategyAuthTokenEmptyApiKeyDesc'),
|
| 870 |
-
icon: <span className="material-symbols-outlined text-[17px]">key_off</span>,
|
| 871 |
-
},
|
| 872 |
-
{
|
| 873 |
-
value: 'api_key' as const,
|
| 874 |
-
label: t('settings.providers.authStrategyApiKey'),
|
| 875 |
-
description: t('settings.providers.authStrategyApiKeyDesc'),
|
| 876 |
-
icon: <span className="material-symbols-outlined text-[17px]">vpn_key</span>,
|
| 877 |
-
},
|
| 878 |
-
{
|
| 879 |
-
value: 'dual_same_token' as const,
|
| 880 |
-
label: t('settings.providers.authStrategyDualSameToken'),
|
| 881 |
-
description: t('settings.providers.authStrategyDualSameTokenDesc'),
|
| 882 |
-
icon: <span className="material-symbols-outlined text-[17px]">sync_alt</span>,
|
| 883 |
-
},
|
| 884 |
-
{
|
| 885 |
-
value: 'dual_dummy' as const,
|
| 886 |
-
label: t('settings.providers.authStrategyDualDummy'),
|
| 887 |
-
description: t('settings.providers.authStrategyDualDummyDesc'),
|
| 888 |
-
icon: <span className="material-symbols-outlined text-[17px]">construction</span>,
|
| 889 |
-
},
|
| 890 |
-
] satisfies Array<{ value: ProviderAuthStrategy; label: string; description: string; icon: ReactNode }>
|
| 891 |
-
const selectedAuthStrategyLabel = authStrategyItems.find((item) => item.value === authStrategy)?.label ?? t('settings.providers.authStrategyAuthToken')
|
| 892 |
-
const configuredContextWindows = buildModelContextWindows(models, modelContextInputs)
|
| 893 |
-
const configuredContextSummary = Object.entries(configuredContextWindows)
|
| 894 |
-
.filter(([model], index, entries) => entries.findIndex(([candidate]) => candidate === model) === index)
|
| 895 |
-
.map(([model, value]) => `${model}: ${formatContextWindow(value)}`)
|
| 896 |
-
const parsedFallbackContextWindow = parseAutoCompactWindowInput(autoCompactWindow)
|
| 897 |
-
const fallbackContextSummary = parsedFallbackContextWindow !== undefined
|
| 898 |
-
? t('settings.providers.contextFallbackSummary', {
|
| 899 |
-
tokens: formatContextWindow(parsedFallbackContextWindow),
|
| 900 |
-
})
|
| 901 |
-
: t('settings.providers.contextFallbackAuto')
|
| 902 |
-
const contextSummary = configuredContextSummary.length > 0
|
| 903 |
-
? [...configuredContextSummary, fallbackContextSummary].join(' · ')
|
| 904 |
-
: t('settings.providers.contextSummaryAuto')
|
| 905 |
-
const shouldShowContextFields = showContextSettings || modelContextWindowErrorSlots.length > 0 || !!autoCompactWindowErrorKey
|
| 906 |
-
const handleAutoCompactWindowChange = (value: string) => {
|
| 907 |
-
setAutoCompactWindow(value)
|
| 908 |
-
setSettingsJson((current) => updateSettingsJsonAutoCompactWindow(current, value))
|
| 909 |
-
}
|
| 910 |
-
const handleBaseUrlChange = (value: string) => {
|
| 911 |
-
setBaseUrl(value)
|
| 912 |
-
setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, authStrategy, apiKey, selectedPreset, value))
|
| 913 |
-
}
|
| 914 |
-
const handleApiKeyChange = (value: string) => {
|
| 915 |
-
setApiKey(value)
|
| 916 |
-
setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, authStrategy, value, selectedPreset, baseUrl))
|
| 917 |
-
}
|
| 918 |
-
const handleApiFormatChange = (value: ApiFormat) => {
|
| 919 |
-
setApiFormat(value)
|
| 920 |
-
setSettingsJson((current) => updateSettingsJsonProviderConnection(current, value, authStrategy, apiKey, selectedPreset, baseUrl))
|
| 921 |
-
}
|
| 922 |
-
const handleAuthStrategyChange = (value: ProviderAuthStrategy) => {
|
| 923 |
-
setAuthStrategy(value)
|
| 924 |
-
setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, value, apiKey, selectedPreset, baseUrl))
|
| 925 |
-
}
|
| 926 |
-
const handleModelChange = (slot: ModelSlot, value: string) => {
|
| 927 |
-
const nextModels = { ...models, [slot]: value }
|
| 928 |
-
const nextInputs = {
|
| 929 |
-
...modelContextInputs,
|
| 930 |
-
[slot]: getModelContextInputValue(value, selectedPreset, provider),
|
| 931 |
-
}
|
| 932 |
-
setModels(nextModels)
|
| 933 |
-
setModelContextInputs(nextInputs)
|
| 934 |
-
setSettingsJson((current) => updateSettingsJsonModelContextWindows(
|
| 935 |
-
updateSettingsJsonModels(current, normalizeModelMapping(nextModels)),
|
| 936 |
-
buildModelContextWindows(nextModels, nextInputs),
|
| 937 |
-
))
|
| 938 |
-
}
|
| 939 |
-
const handleModelContextWindowChange = (slot: ModelSlot, value: string) => {
|
| 940 |
-
const nextInputs = { ...modelContextInputs, [slot]: value }
|
| 941 |
-
setModelContextInputs(nextInputs)
|
| 942 |
-
setSettingsJson((current) => updateSettingsJsonModelContextWindows(
|
| 943 |
-
current,
|
| 944 |
-
buildModelContextWindows(models, nextInputs),
|
| 945 |
-
))
|
| 946 |
-
}
|
| 947 |
-
const renderPresetButton = (preset: ProviderPreset) => (
|
| 948 |
-
<button
|
| 949 |
-
key={preset.id}
|
| 950 |
-
onClick={() => handlePresetChange(preset)}
|
| 951 |
-
className={`px-3 py-1.5 text-xs font-medium rounded-full border transition-all ${
|
| 952 |
-
selectedPreset.id === preset.id
|
| 953 |
-
? 'border-[var(--color-brand)] bg-[var(--color-surface-container-high)] text-[var(--color-brand)] shadow-[var(--shadow-focus-ring)]'
|
| 954 |
-
: 'border-[var(--color-border)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)]'
|
| 955 |
-
}`}
|
| 956 |
-
>
|
| 957 |
-
{preset.name}
|
| 958 |
-
</button>
|
| 959 |
-
)
|
| 960 |
-
|
| 961 |
-
const handleSubmit = async () => {
|
| 962 |
-
if (!canSubmit) return
|
| 963 |
-
const normalizedModels = normalizeModelMapping(models)
|
| 964 |
-
const parsedAutoCompactWindow = parseAutoCompactWindowInput(autoCompactWindow)
|
| 965 |
-
const parsedModelContextWindows = buildModelContextWindows(models, modelContextInputs)
|
| 966 |
-
setIsSubmitting(true)
|
| 967 |
-
try {
|
| 968 |
-
// Write the edited cc-haha settings.json first so provider-specific model
|
| 969 |
-
// settings never conflict with the user's global ~/.claude/settings.json.
|
| 970 |
-
if (settingsJson.trim()) {
|
| 971 |
-
try {
|
| 972 |
-
const parsed = restoreSettingsJsonSecrets(JSON.parse(settingsJson), settingsJson, apiKey)
|
| 973 |
-
const { providersApi } = await import('../api/providers')
|
| 974 |
-
await providersApi.updateSettings(parsed)
|
| 975 |
-
} catch {
|
| 976 |
-
// JSON validation already prevents this
|
| 977 |
-
}
|
| 978 |
-
}
|
| 979 |
-
|
| 980 |
-
if (mode === 'create') {
|
| 981 |
-
await createProvider({
|
| 982 |
-
presetId: selectedPreset.id,
|
| 983 |
-
name: name.trim(),
|
| 984 |
-
apiKey: apiKey.trim(),
|
| 985 |
-
authStrategy,
|
| 986 |
-
baseUrl: baseUrl.trim(),
|
| 987 |
-
apiFormat,
|
| 988 |
-
models: normalizedModels,
|
| 989 |
-
...(parsedAutoCompactWindow !== undefined && { autoCompactWindow: parsedAutoCompactWindow }),
|
| 990 |
-
...(Object.keys(parsedModelContextWindows).length > 0 && { modelContextWindows: parsedModelContextWindows }),
|
| 991 |
-
notes: notes.trim() || undefined,
|
| 992 |
-
})
|
| 993 |
-
} else if (provider) {
|
| 994 |
-
const input: UpdateProviderInput = {
|
| 995 |
-
name: name.trim(),
|
| 996 |
-
baseUrl: baseUrl.trim(),
|
| 997 |
-
authStrategy,
|
| 998 |
-
apiFormat,
|
| 999 |
-
models: normalizedModels,
|
| 1000 |
-
autoCompactWindow: parsedAutoCompactWindow ?? null,
|
| 1001 |
-
modelContextWindows: Object.keys(parsedModelContextWindows).length > 0
|
| 1002 |
-
? parsedModelContextWindows
|
| 1003 |
-
: null,
|
| 1004 |
-
notes: notes.trim() || undefined,
|
| 1005 |
-
}
|
| 1006 |
-
if (apiKey.trim()) input.apiKey = apiKey.trim()
|
| 1007 |
-
await updateProvider(provider.id, input)
|
| 1008 |
-
}
|
| 1009 |
-
await fetchSettings()
|
| 1010 |
-
onClose()
|
| 1011 |
-
} catch (err) {
|
| 1012 |
-
console.error('Failed to save provider:', err)
|
| 1013 |
-
} finally {
|
| 1014 |
-
setIsSubmitting(false)
|
| 1015 |
-
}
|
| 1016 |
-
}
|
| 1017 |
-
|
| 1018 |
-
const handleTest = async () => {
|
| 1019 |
-
if (!baseUrl.trim() || !models.main.trim()) return
|
| 1020 |
-
setIsTesting(true)
|
| 1021 |
-
setTestResult(null)
|
| 1022 |
-
try {
|
| 1023 |
-
let result: ProviderTestResult
|
| 1024 |
-
if (mode === 'edit' && provider && !apiKey.trim()) {
|
| 1025 |
-
result = await useProviderStore.getState().testProvider(provider.id, {
|
| 1026 |
-
baseUrl: baseUrl.trim(),
|
| 1027 |
-
modelId: models.main.trim(),
|
| 1028 |
-
apiFormat,
|
| 1029 |
-
authStrategy,
|
| 1030 |
-
})
|
| 1031 |
-
} else {
|
| 1032 |
-
if (requiresApiKey && !apiKey.trim()) return
|
| 1033 |
-
result = await testConfig({
|
| 1034 |
-
baseUrl: baseUrl.trim(),
|
| 1035 |
-
apiKey: apiKey.trim() || selectedPreset.defaultEnv?.ANTHROPIC_AUTH_TOKEN || 'local',
|
| 1036 |
-
modelId: models.main.trim(),
|
| 1037 |
-
authStrategy,
|
| 1038 |
-
apiFormat,
|
| 1039 |
-
})
|
| 1040 |
-
}
|
| 1041 |
-
setTestResult(result)
|
| 1042 |
-
} catch {
|
| 1043 |
-
setTestResult({ connectivity: { success: false, latencyMs: 0, error: t('settings.providers.requestFailed') } })
|
| 1044 |
-
} finally {
|
| 1045 |
-
setIsTesting(false)
|
| 1046 |
-
}
|
| 1047 |
-
}
|
| 1048 |
-
|
| 1049 |
-
return (
|
| 1050 |
-
<Modal
|
| 1051 |
-
open={open}
|
| 1052 |
-
onClose={onClose}
|
| 1053 |
-
title={mode === 'create' ? t('settings.providers.addTitle') : t('settings.providers.editTitle')}
|
| 1054 |
-
width={720}
|
| 1055 |
-
footer={
|
| 1056 |
-
<>
|
| 1057 |
-
<Button variant="secondary" onClick={onClose}>{t('common.cancel')}</Button>
|
| 1058 |
-
<Button onClick={handleSubmit} disabled={!canSubmit} loading={isSubmitting}>
|
| 1059 |
-
{mode === 'create' ? t('common.add') : t('common.save')}
|
| 1060 |
-
</Button>
|
| 1061 |
-
</>
|
| 1062 |
-
}
|
| 1063 |
-
>
|
| 1064 |
-
<div className="flex flex-col gap-4">
|
| 1065 |
-
{/* Preset chips */}
|
| 1066 |
-
{mode === 'create' && (
|
| 1067 |
-
<div>
|
| 1068 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-2 block">{t('settings.providers.preset')}</label>
|
| 1069 |
-
<div className="flex flex-col gap-2">
|
| 1070 |
-
<div className="flex flex-wrap gap-2">
|
| 1071 |
-
{regularPresets.map(renderPresetButton)}
|
| 1072 |
-
</div>
|
| 1073 |
-
{featuredPresets.length > 0 && (
|
| 1074 |
-
<div className="flex flex-wrap gap-2 border-t border-[var(--color-border)]/60 pt-2">
|
| 1075 |
-
{featuredPresets.map(renderPresetButton)}
|
| 1076 |
-
</div>
|
| 1077 |
-
)}
|
| 1078 |
-
</div>
|
| 1079 |
-
</div>
|
| 1080 |
-
)}
|
| 1081 |
-
|
| 1082 |
-
<Input label={t('settings.providers.name')} required value={name} onChange={(e) => setName(e.target.value)} placeholder={t('settings.providers.namePlaceholder')} />
|
| 1083 |
-
|
| 1084 |
-
<Input label={t('settings.providers.notes')} value={notes} onChange={(e) => setNotes(e.target.value)} placeholder={t('settings.providers.notesPlaceholder')} />
|
| 1085 |
-
|
| 1086 |
-
<Input label={t('settings.providers.baseUrl')} required value={baseUrl} onChange={(e) => handleBaseUrlChange(e.target.value)} placeholder={t('settings.providers.baseUrlPlaceholder')} />
|
| 1087 |
-
|
| 1088 |
-
{/* API Format */}
|
| 1089 |
-
{(isCustom || mode === 'edit') ? (
|
| 1090 |
-
<div>
|
| 1091 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-1 block">{t('settings.providers.apiFormat')}</label>
|
| 1092 |
-
<Dropdown<ApiFormat>
|
| 1093 |
-
items={apiFormatItems}
|
| 1094 |
-
value={apiFormat}
|
| 1095 |
-
onChange={handleApiFormatChange}
|
| 1096 |
-
width="100%"
|
| 1097 |
-
className="block w-full"
|
| 1098 |
-
trigger={
|
| 1099 |
-
<button
|
| 1100 |
-
type="button"
|
| 1101 |
-
className="flex h-10 w-full items-center gap-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-left text-sm text-[var(--color-text-primary)] outline-none transition-colors hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-container-low)] focus-visible:border-[var(--color-border-focus)] focus-visible:shadow-[var(--shadow-focus-ring)]"
|
| 1102 |
-
>
|
| 1103 |
-
<span className="min-w-0 flex-1 truncate">{selectedApiFormatLabel}</span>
|
| 1104 |
-
<span className="material-symbols-outlined flex-shrink-0 text-[18px] text-[var(--color-text-secondary)]">expand_more</span>
|
| 1105 |
-
</button>
|
| 1106 |
-
}
|
| 1107 |
-
/>
|
| 1108 |
-
{apiFormat !== 'anthropic' && (
|
| 1109 |
-
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">{t('settings.providers.proxyHint')}</p>
|
| 1110 |
-
)}
|
| 1111 |
-
</div>
|
| 1112 |
-
) : apiFormat !== 'anthropic' ? (
|
| 1113 |
-
<div>
|
| 1114 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-1 block">{t('settings.providers.apiFormat')}</label>
|
| 1115 |
-
<div className="text-xs text-[var(--color-text-tertiary)] px-3 py-2 rounded-[var(--radius-md)] bg-[var(--color-surface-container-low)] border border-[var(--color-border)]">
|
| 1116 |
-
{apiFormat === 'openai_chat' ? t('settings.providers.apiFormatOpenaiChat') : t('settings.providers.apiFormatOpenaiResponses')}
|
| 1117 |
-
</div>
|
| 1118 |
-
</div>
|
| 1119 |
-
) : null}
|
| 1120 |
-
|
| 1121 |
-
{apiFormat === 'anthropic' && (
|
| 1122 |
-
<div>
|
| 1123 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-1 block">{t('settings.providers.authStrategy')}</label>
|
| 1124 |
-
<Dropdown<ProviderAuthStrategy>
|
| 1125 |
-
items={authStrategyItems}
|
| 1126 |
-
value={authStrategy}
|
| 1127 |
-
onChange={handleAuthStrategyChange}
|
| 1128 |
-
width="100%"
|
| 1129 |
-
className="block w-full"
|
| 1130 |
-
trigger={
|
| 1131 |
-
<button
|
| 1132 |
-
type="button"
|
| 1133 |
-
className="flex min-h-10 w-full items-center gap-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-left text-sm text-[var(--color-text-primary)] outline-none transition-colors hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-container-low)] focus-visible:border-[var(--color-border-focus)] focus-visible:shadow-[var(--shadow-focus-ring)]"
|
| 1134 |
-
>
|
| 1135 |
-
<span className="min-w-0 flex-1 truncate">{selectedAuthStrategyLabel}</span>
|
| 1136 |
-
<span className="material-symbols-outlined flex-shrink-0 text-[18px] text-[var(--color-text-secondary)]">expand_more</span>
|
| 1137 |
-
</button>
|
| 1138 |
-
}
|
| 1139 |
-
/>
|
| 1140 |
-
</div>
|
| 1141 |
-
)}
|
| 1142 |
-
|
| 1143 |
-
<div className="flex flex-col gap-1">
|
| 1144 |
-
<label htmlFor="provider-api-key" className="text-sm font-medium text-[var(--color-text-primary)]">
|
| 1145 |
-
{t('settings.providers.apiKey')}
|
| 1146 |
-
{mode === 'create' && requiresApiKey && <span className="text-[var(--color-error)] ml-0.5">*</span>}
|
| 1147 |
-
</label>
|
| 1148 |
-
<div className="relative">
|
| 1149 |
-
<input
|
| 1150 |
-
id="provider-api-key"
|
| 1151 |
-
type={showApiKey ? 'text' : 'password'}
|
| 1152 |
-
value={apiKey}
|
| 1153 |
-
onChange={(e) => handleApiKeyChange(e.target.value)}
|
| 1154 |
-
placeholder="sk-..."
|
| 1155 |
-
className="h-10 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 pr-10 text-sm text-[var(--color-text-primary)] outline-none transition-colors duration-150 placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-focus)] focus:shadow-[var(--shadow-focus-ring)]"
|
| 1156 |
-
/>
|
| 1157 |
-
<button
|
| 1158 |
-
type="button"
|
| 1159 |
-
onClick={() => setShowApiKey((visible) => !visible)}
|
| 1160 |
-
aria-label={showApiKey ? 'Hide API Key' : 'Show API Key'}
|
| 1161 |
-
className="absolute right-1.5 top-1/2 flex h-7 w-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[var(--radius-sm)] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus:outline-none focus:shadow-[var(--shadow-focus-ring)]"
|
| 1162 |
-
>
|
| 1163 |
-
<span className="material-symbols-outlined text-[16px]">
|
| 1164 |
-
{showApiKey ? 'visibility_off' : 'visibility'}
|
| 1165 |
-
</span>
|
| 1166 |
-
</button>
|
| 1167 |
-
</div>
|
| 1168 |
-
</div>
|
| 1169 |
-
|
| 1170 |
-
{(apiKeyUrl || promoText) && (
|
| 1171 |
-
<div className="-mt-2 flex flex-col gap-1.5">
|
| 1172 |
-
{apiKeyUrl && (
|
| 1173 |
-
<button
|
| 1174 |
-
type="button"
|
| 1175 |
-
onClick={() => openExternalUrl(apiKeyUrl)}
|
| 1176 |
-
className="group inline-flex h-6 w-fit cursor-pointer items-center gap-1 rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1.5 text-[11px] font-medium leading-none text-[var(--color-brand)] transition-colors hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)] focus:outline-none focus:shadow-[var(--shadow-focus-ring)]"
|
| 1177 |
-
>
|
| 1178 |
-
<span className="material-symbols-outlined text-[13px]">key</span>
|
| 1179 |
-
{t('settings.providers.getApiKey')}
|
| 1180 |
-
<span className="material-symbols-outlined text-[9px] opacity-60 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5">arrow_outward</span>
|
| 1181 |
-
</button>
|
| 1182 |
-
)}
|
| 1183 |
-
{promoText && (
|
| 1184 |
-
<button
|
| 1185 |
-
type="button"
|
| 1186 |
-
onClick={() => apiKeyUrl && openExternalUrl(apiKeyUrl)}
|
| 1187 |
-
disabled={!apiKeyUrl}
|
| 1188 |
-
className="group flex w-full cursor-pointer items-start gap-1.5 rounded-[var(--radius-sm)] border border-[var(--color-brand)]/25 bg-[var(--color-brand)]/8 px-2.5 py-1.5 text-left text-[11px] leading-5 text-[var(--color-text-primary)] transition-colors hover:border-[var(--color-brand)]/45 hover:bg-[var(--color-brand)]/12 focus:outline-none focus:shadow-[var(--shadow-focus-ring)] disabled:cursor-default disabled:hover:border-[var(--color-brand)]/25 disabled:hover:bg-[var(--color-brand)]/8"
|
| 1189 |
-
>
|
| 1190 |
-
<span className="material-symbols-outlined mt-0.5 text-[13px] text-[var(--color-brand)]">tips_and_updates</span>
|
| 1191 |
-
<span>{promoText}</span>
|
| 1192 |
-
{apiKeyUrl && (
|
| 1193 |
-
<span className="material-symbols-outlined ml-auto mt-1 text-[10px] text-[var(--color-brand)] opacity-45 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5">arrow_outward</span>
|
| 1194 |
-
)}
|
| 1195 |
-
</button>
|
| 1196 |
-
)}
|
| 1197 |
-
</div>
|
| 1198 |
-
)}
|
| 1199 |
-
|
| 1200 |
-
{/* Model Mapping */}
|
| 1201 |
-
<div>
|
| 1202 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-2 block">{t('settings.providers.modelMapping')}</label>
|
| 1203 |
-
<div className="grid grid-cols-2 gap-2">
|
| 1204 |
-
<Input label={t('settings.providers.mainModel')} required value={models.main} onChange={(e) => handleModelChange('main', e.target.value)} placeholder="Model ID" />
|
| 1205 |
-
<Input label={t('settings.providers.haikuModel')} value={models.haiku} onChange={(e) => handleModelChange('haiku', e.target.value)} placeholder={t('settings.providers.sameAsMain')} />
|
| 1206 |
-
<Input label={t('settings.providers.sonnetModel')} value={models.sonnet} onChange={(e) => handleModelChange('sonnet', e.target.value)} placeholder={t('settings.providers.sameAsMain')} />
|
| 1207 |
-
<Input label={t('settings.providers.opusModel')} value={models.opus} onChange={(e) => handleModelChange('opus', e.target.value)} placeholder={t('settings.providers.sameAsMain')} />
|
| 1208 |
-
</div>
|
| 1209 |
-
</div>
|
| 1210 |
-
|
| 1211 |
-
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)]">
|
| 1212 |
-
<button
|
| 1213 |
-
type="button"
|
| 1214 |
-
onClick={() => setShowContextSettings((visible) => !visible)}
|
| 1215 |
-
className="flex w-full items-start gap-3 px-3 py-3 text-left outline-none transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:shadow-[var(--shadow-focus-ring)]"
|
| 1216 |
-
aria-expanded={shouldShowContextFields}
|
| 1217 |
-
>
|
| 1218 |
-
<span className="material-symbols-outlined mt-0.5 text-[18px] text-[var(--color-brand)]">compress</span>
|
| 1219 |
-
<span className="min-w-0 flex-1">
|
| 1220 |
-
<span className="block text-sm font-medium text-[var(--color-text-primary)]">
|
| 1221 |
-
{t('settings.providers.contextSettingsTitle')}
|
| 1222 |
-
</span>
|
| 1223 |
-
<span className="mt-1 block truncate text-xs text-[var(--color-text-secondary)]">
|
| 1224 |
-
{contextSummary}
|
| 1225 |
-
</span>
|
| 1226 |
-
<span className="mt-1 block text-[11px] leading-5 text-[var(--color-text-tertiary)]">
|
| 1227 |
-
{t('settings.providers.contextSettingsDesc')}
|
| 1228 |
-
</span>
|
| 1229 |
-
</span>
|
| 1230 |
-
<span className="mt-0.5 inline-flex items-center gap-1 text-xs font-medium text-[var(--color-brand)]">
|
| 1231 |
-
{shouldShowContextFields
|
| 1232 |
-
? t('settings.providers.contextSettingsHide')
|
| 1233 |
-
: t('settings.providers.contextSettingsEdit')}
|
| 1234 |
-
<span className="material-symbols-outlined text-[16px]">
|
| 1235 |
-
{shouldShowContextFields ? 'expand_less' : 'expand_more'}
|
| 1236 |
-
</span>
|
| 1237 |
-
</span>
|
| 1238 |
-
</button>
|
| 1239 |
-
|
| 1240 |
-
{shouldShowContextFields && (
|
| 1241 |
-
<div className="border-t border-[var(--color-border)] px-3 pb-3 pt-3">
|
| 1242 |
-
<div>
|
| 1243 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-2 block">{t('settings.providers.modelContextWindows')}</label>
|
| 1244 |
-
<div className="grid grid-cols-2 gap-2">
|
| 1245 |
-
{MODEL_SLOTS.map((slot) => {
|
| 1246 |
-
const errorKey = getModelContextWindowErrorKey(modelContextInputs[slot])
|
| 1247 |
-
const labelKey = slot === 'main'
|
| 1248 |
-
? 'settings.providers.mainContextWindow'
|
| 1249 |
-
: slot === 'haiku'
|
| 1250 |
-
? 'settings.providers.haikuContextWindow'
|
| 1251 |
-
: slot === 'sonnet'
|
| 1252 |
-
? 'settings.providers.sonnetContextWindow'
|
| 1253 |
-
: 'settings.providers.opusContextWindow'
|
| 1254 |
-
return (
|
| 1255 |
-
<div key={slot}>
|
| 1256 |
-
<Input
|
| 1257 |
-
label={t(labelKey)}
|
| 1258 |
-
value={modelContextInputs[slot]}
|
| 1259 |
-
onChange={(e) => handleModelContextWindowChange(slot, e.target.value)}
|
| 1260 |
-
placeholder={t('settings.providers.contextWindowPlaceholder')}
|
| 1261 |
-
/>
|
| 1262 |
-
{errorKey && (
|
| 1263 |
-
<p className="text-[11px] text-[var(--color-error)] mt-1">
|
| 1264 |
-
{errorKey === 'number'
|
| 1265 |
-
? t('settings.providers.modelContextWindowNumberError')
|
| 1266 |
-
: t('settings.providers.modelContextWindowRangeError')}
|
| 1267 |
-
</p>
|
| 1268 |
-
)}
|
| 1269 |
-
</div>
|
| 1270 |
-
)
|
| 1271 |
-
})}
|
| 1272 |
-
</div>
|
| 1273 |
-
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">
|
| 1274 |
-
{t('settings.providers.modelContextWindowsDesc')}
|
| 1275 |
-
</p>
|
| 1276 |
-
</div>
|
| 1277 |
-
|
| 1278 |
-
<div className="mt-3">
|
| 1279 |
-
<Input
|
| 1280 |
-
label={t('settings.providers.autoCompactWindow')}
|
| 1281 |
-
value={autoCompactWindow}
|
| 1282 |
-
onChange={(e) => handleAutoCompactWindowChange(e.target.value)}
|
| 1283 |
-
placeholder={t('settings.providers.autoCompactWindowPlaceholder')}
|
| 1284 |
-
/>
|
| 1285 |
-
{autoCompactWindowErrorKey ? (
|
| 1286 |
-
<p className="text-[11px] text-[var(--color-error)] mt-1">
|
| 1287 |
-
{autoCompactWindowErrorKey === 'number'
|
| 1288 |
-
? t('settings.providers.autoCompactWindowNumberError')
|
| 1289 |
-
: t('settings.providers.autoCompactWindowRangeError')}
|
| 1290 |
-
</p>
|
| 1291 |
-
) : (
|
| 1292 |
-
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">
|
| 1293 |
-
{t('settings.providers.autoCompactWindowDesc')}
|
| 1294 |
-
</p>
|
| 1295 |
-
)}
|
| 1296 |
-
</div>
|
| 1297 |
-
</div>
|
| 1298 |
-
)}
|
| 1299 |
-
</div>
|
| 1300 |
-
|
| 1301 |
-
{/* Test connection */}
|
| 1302 |
-
<div className="flex items-center gap-3">
|
| 1303 |
-
<Button variant="secondary" size="sm" onClick={handleTest} loading={isTesting} disabled={!baseUrl.trim() || !models.main.trim()}>
|
| 1304 |
-
{t('settings.providers.testConnection')}
|
| 1305 |
-
</Button>
|
| 1306 |
-
{testResult && (
|
| 1307 |
-
<div className="flex flex-col gap-0.5">
|
| 1308 |
-
<span className={`text-xs ${testResult.connectivity.success ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 1309 |
-
{testResult.connectivity.success
|
| 1310 |
-
? t('settings.providers.connectivityOk', { latency: String(testResult.connectivity.latencyMs) })
|
| 1311 |
-
: t('settings.providers.connectivityFailed', { error: testResult.connectivity.error || '' })}
|
| 1312 |
-
</span>
|
| 1313 |
-
{testResult.proxy && (
|
| 1314 |
-
<span className={`text-xs ${testResult.proxy.success ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'}`}>
|
| 1315 |
-
{testResult.proxy.success
|
| 1316 |
-
? t('settings.providers.proxyOk', { latency: String(testResult.proxy.latencyMs) })
|
| 1317 |
-
: t('settings.providers.proxyFailed', { error: testResult.proxy.error || '' })}
|
| 1318 |
-
</span>
|
| 1319 |
-
)}
|
| 1320 |
-
</div>
|
| 1321 |
-
)}
|
| 1322 |
-
</div>
|
| 1323 |
-
|
| 1324 |
-
{/* Settings JSON — editable, shown for all presets including official */}
|
| 1325 |
-
<div>
|
| 1326 |
-
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-2 block">{t('settings.providers.settingsJson')}</label>
|
| 1327 |
-
<textarea
|
| 1328 |
-
value={displayedSettingsJson}
|
| 1329 |
-
onChange={(e) => {
|
| 1330 |
-
const raw = e.target.value
|
| 1331 |
-
try {
|
| 1332 |
-
const parsed = restoreSettingsJsonSecrets(JSON.parse(raw), settingsJson, apiKey)
|
| 1333 |
-
setSettingsJson(JSON.stringify(parsed, null, 2))
|
| 1334 |
-
setSettingsJsonError(null)
|
| 1335 |
-
// Auto-fill form fields from parsed JSON env
|
| 1336 |
-
const env = parsed.env as Record<string, string> | undefined
|
| 1337 |
-
if (env) {
|
| 1338 |
-
if (env.ANTHROPIC_BASE_URL) {
|
| 1339 |
-
setBaseUrl(env.ANTHROPIC_BASE_URL)
|
| 1340 |
-
// Auto-switch to matching preset or Custom
|
| 1341 |
-
if (mode === 'create') {
|
| 1342 |
-
const matchedPreset = availablePresets.find((p) => p.id !== 'custom' && p.baseUrl === env.ANTHROPIC_BASE_URL)
|
| 1343 |
-
const targetPreset = requirePreset(
|
| 1344 |
-
matchedPreset ?? availablePresets.find((p) => p.id === 'custom'),
|
| 1345 |
-
)
|
| 1346 |
-
if (targetPreset.id !== selectedPreset.id) {
|
| 1347 |
-
jsonPastedRef.current = true
|
| 1348 |
-
setSelectedPreset(targetPreset)
|
| 1349 |
-
}
|
| 1350 |
-
}
|
| 1351 |
-
}
|
| 1352 |
-
const nextApiKey = env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY
|
| 1353 |
-
if (nextApiKey && nextApiKey !== '(your API key)' && nextApiKey !== API_KEY_JSON_PLACEHOLDER) {
|
| 1354 |
-
setApiKey(nextApiKey)
|
| 1355 |
-
}
|
| 1356 |
-
const nextAuthStrategy = inferAuthStrategyFromEnv(env)
|
| 1357 |
-
if (nextAuthStrategy) {
|
| 1358 |
-
setAuthStrategy(nextAuthStrategy)
|
| 1359 |
-
}
|
| 1360 |
-
if (env[AUTO_COMPACT_WINDOW_ENV_KEY] !== undefined) {
|
| 1361 |
-
setAutoCompactWindow(String(env[AUTO_COMPACT_WINDOW_ENV_KEY]))
|
| 1362 |
-
} else {
|
| 1363 |
-
setAutoCompactWindow('')
|
| 1364 |
-
}
|
| 1365 |
-
let parsedContextWindows: Record<string, number> = {}
|
| 1366 |
-
if (typeof env[MODEL_CONTEXT_WINDOWS_ENV_KEY] === 'string') {
|
| 1367 |
-
try {
|
| 1368 |
-
const parsedContext = JSON.parse(env[MODEL_CONTEXT_WINDOWS_ENV_KEY]) as Record<string, unknown>
|
| 1369 |
-
parsedContextWindows = Object.fromEntries(
|
| 1370 |
-
Object.entries(parsedContext)
|
| 1371 |
-
.filter(([, value]) => typeof value === 'number' && Number.isInteger(value)),
|
| 1372 |
-
) as Record<string, number>
|
| 1373 |
-
} catch {
|
| 1374 |
-
parsedContextWindows = {}
|
| 1375 |
-
}
|
| 1376 |
-
}
|
| 1377 |
-
const newModels: Partial<ModelMapping> = {}
|
| 1378 |
-
if (env.ANTHROPIC_MODEL) newModels.main = env.ANTHROPIC_MODEL
|
| 1379 |
-
if (env.ANTHROPIC_DEFAULT_HAIKU_MODEL) newModels.haiku = env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
| 1380 |
-
if (env.ANTHROPIC_DEFAULT_SONNET_MODEL) newModels.sonnet = env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
| 1381 |
-
if (env.ANTHROPIC_DEFAULT_OPUS_MODEL) newModels.opus = env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
| 1382 |
-
if (Object.keys(newModels).length > 0) {
|
| 1383 |
-
setModels((prev) => {
|
| 1384 |
-
const nextModels = { ...prev, ...newModels }
|
| 1385 |
-
setModelContextInputs(getModelContextInputs(nextModels, {
|
| 1386 |
-
...selectedPreset,
|
| 1387 |
-
modelContextWindows: parsedContextWindows,
|
| 1388 |
-
}))
|
| 1389 |
-
return nextModels
|
| 1390 |
-
})
|
| 1391 |
-
} else if (Object.keys(parsedContextWindows).length > 0) {
|
| 1392 |
-
setModelContextInputs(getModelContextInputs(models, {
|
| 1393 |
-
...selectedPreset,
|
| 1394 |
-
modelContextWindows: parsedContextWindows,
|
| 1395 |
-
}))
|
| 1396 |
-
}
|
| 1397 |
-
}
|
| 1398 |
-
} catch (err) {
|
| 1399 |
-
setSettingsJson(raw)
|
| 1400 |
-
setSettingsJsonError(err instanceof Error ? err.message : 'Invalid JSON')
|
| 1401 |
-
}
|
| 1402 |
-
}}
|
| 1403 |
-
rows={16}
|
| 1404 |
-
spellCheck={false}
|
| 1405 |
-
className={`w-full text-xs px-3 py-3 rounded-[var(--radius-md)] bg-[var(--color-surface-container-low)] border font-mono leading-relaxed resize-y text-[var(--color-text-secondary)] outline-none ${
|
| 1406 |
-
settingsJsonError
|
| 1407 |
-
? 'border-[var(--color-error)] focus:border-[var(--color-error)]'
|
| 1408 |
-
: 'border-[var(--color-border)] focus:border-[var(--color-border-focus)]'
|
| 1409 |
-
}`}
|
| 1410 |
-
/>
|
| 1411 |
-
{settingsJsonError && (
|
| 1412 |
-
<p className="text-[11px] text-[var(--color-error)] mt-1">{t('settings.providers.jsonError', { error: settingsJsonError })}</p>
|
| 1413 |
-
)}
|
| 1414 |
-
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">{t('settings.providers.settingsJsonDesc')}</p>
|
| 1415 |
-
</div>
|
| 1416 |
-
</div>
|
| 1417 |
-
</Modal>
|
| 1418 |
-
)
|
| 1419 |
}
|
| 1420 |
|
| 1421 |
-
|
| 1422 |
// ─── Permission Settings ──────────────────────────────────────
|
| 1423 |
|
| 1424 |
function PermissionSettings() {
|
|
|
|
| 2 |
import QRCode from 'qrcode'
|
| 3 |
import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react'
|
| 4 |
import { useSettingsStore, UI_ZOOM_DEFAULT, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore'
|
|
|
|
| 5 |
import { useTranslation } from '../i18n'
|
|
|
|
| 6 |
import { ConfirmDialog } from '../components/shared/ConfirmDialog'
|
| 7 |
import { Input } from '../components/shared/Input'
|
| 8 |
import { Button } from '../components/shared/Button'
|
| 9 |
import { Dropdown } from '../components/shared/Dropdown'
|
| 10 |
import type { PermissionMode, EffortLevel, ThemeMode, UpdateProxyMode, NetworkProxyMode, WebSearchMode, AppMode } from '../types/settings'
|
| 11 |
import type { Locale } from '../i18n'
|
|
|
|
|
|
|
| 12 |
import { AdapterSettings } from './AdapterSettings'
|
| 13 |
import { useAgentStore } from '../stores/agentStore'
|
| 14 |
import { useSessionStore } from '../stores/sessionStore'
|
|
|
|
| 27 |
import { ActivitySettings } from './ActivitySettings'
|
| 28 |
import { MemorySettings } from './MemorySettings'
|
| 29 |
import { useUIStore, type SettingsTab } from '../stores/uiStore'
|
| 30 |
+
import { CliLoginProviderSettings } from '../components/settings/CliLoginProviderSettings'
|
|
|
|
|
|
|
| 31 |
import { useUpdateStore } from '../stores/updateStore'
|
| 32 |
import { formatBytes } from '../lib/formatBytes'
|
| 33 |
import { isTauriRuntime } from '../lib/desktopRuntime'
|
|
|
|
| 38 |
requestDesktopNotificationPermission,
|
| 39 |
type DesktopNotificationPermission,
|
| 40 |
} from '../lib/desktopNotifications'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
import { copyTextToClipboard } from '../components/chat/clipboard'
|
| 42 |
|
| 43 |
const NETWORK_TIMEOUT_MIN_SECONDS = 5
|
|
|
|
| 192 |
// ─── Provider Settings ──────────────────────────────────────
|
| 193 |
|
| 194 |
function ProviderSettings() {
|
| 195 |
+
return <CliLoginProviderSettings />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
}
|
| 197 |
|
|
|
|
| 198 |
// ─── Permission Settings ──────────────────────────────────────
|
| 199 |
|
| 200 |
function PermissionSettings() {
|
desktop/src/stores/cliAuthStore.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand'
|
| 2 |
+
import { cliAuthApi, type AuthProvider, type CliAuthConfig } from '../api/cliAuth'
|
| 3 |
+
|
| 4 |
+
type CliAuthStore = {
|
| 5 |
+
authProvider: AuthProvider | null
|
| 6 |
+
nvidiaApiKey: string | null
|
| 7 |
+
openRouterApiKey: string | null
|
| 8 |
+
openAiApiKey: string | null
|
| 9 |
+
openCodeApiKey: string | null
|
| 10 |
+
openCodeModelName: string | null
|
| 11 |
+
localBaseUrl: string | null
|
| 12 |
+
localModelName: string | null
|
| 13 |
+
isLoading: boolean
|
| 14 |
+
error: string | null
|
| 15 |
+
|
| 16 |
+
fetchAuth: () => Promise<void>
|
| 17 |
+
setAuthProvider: (provider: AuthProvider) => Promise<void>
|
| 18 |
+
saveNvidiaApiKey: (apiKey: string) => Promise<void>
|
| 19 |
+
saveOpenRouterApiKey: (apiKey: string) => Promise<void>
|
| 20 |
+
saveOpenAIApiKey: (apiKey: string) => Promise<void>
|
| 21 |
+
saveOpenCodeApiKey: (apiKey: string, modelName?: string) => Promise<void>
|
| 22 |
+
saveLocalModelConfig: (baseUrl: string, modelName: string) => Promise<void>
|
| 23 |
+
clearAuth: () => Promise<void>
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export const useCliAuthStore = create<CliAuthStore>((set) => ({
|
| 27 |
+
authProvider: null,
|
| 28 |
+
nvidiaApiKey: null,
|
| 29 |
+
openRouterApiKey: null,
|
| 30 |
+
openAiApiKey: null,
|
| 31 |
+
openCodeApiKey: null,
|
| 32 |
+
openCodeModelName: null,
|
| 33 |
+
localBaseUrl: null,
|
| 34 |
+
localModelName: null,
|
| 35 |
+
isLoading: false,
|
| 36 |
+
error: null,
|
| 37 |
+
|
| 38 |
+
fetchAuth: async () => {
|
| 39 |
+
set({ isLoading: true, error: null })
|
| 40 |
+
try {
|
| 41 |
+
const config = await cliAuthApi.get()
|
| 42 |
+
set({
|
| 43 |
+
authProvider: config.authProvider,
|
| 44 |
+
nvidiaApiKey: config.nvidiaApiKey || null,
|
| 45 |
+
openRouterApiKey: config.openRouterApiKey || null,
|
| 46 |
+
openAiApiKey: config.openAiApiKey || config.openAiAccessToken || null,
|
| 47 |
+
openCodeApiKey: config.openCodeApiKey || null,
|
| 48 |
+
openCodeModelName: config.openCodeModelName || null,
|
| 49 |
+
localBaseUrl: config.localBaseUrl || null,
|
| 50 |
+
localModelName: config.localModelName || null,
|
| 51 |
+
isLoading: false,
|
| 52 |
+
})
|
| 53 |
+
} catch (err) {
|
| 54 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
|
| 58 |
+
setAuthProvider: async (provider: AuthProvider) => {
|
| 59 |
+
set({ isLoading: true, error: null })
|
| 60 |
+
try {
|
| 61 |
+
await cliAuthApi.update({ authProvider: provider })
|
| 62 |
+
await cliAuthApi.invalidateCache()
|
| 63 |
+
set({ authProvider: provider, isLoading: false })
|
| 64 |
+
} catch (err) {
|
| 65 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
|
| 69 |
+
saveNvidiaApiKey: async (apiKey: string) => {
|
| 70 |
+
set({ isLoading: true, error: null })
|
| 71 |
+
try {
|
| 72 |
+
await cliAuthApi.update({ authProvider: 'nvidia', nvidiaApiKey: apiKey })
|
| 73 |
+
await cliAuthApi.invalidateCache()
|
| 74 |
+
set({ authProvider: 'nvidia', nvidiaApiKey: apiKey, isLoading: false })
|
| 75 |
+
} catch (err) {
|
| 76 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 77 |
+
}
|
| 78 |
+
},
|
| 79 |
+
|
| 80 |
+
saveOpenRouterApiKey: async (apiKey: string) => {
|
| 81 |
+
set({ isLoading: true, error: null })
|
| 82 |
+
try {
|
| 83 |
+
await cliAuthApi.update({ authProvider: 'openrouter', openRouterApiKey: apiKey })
|
| 84 |
+
await cliAuthApi.invalidateCache()
|
| 85 |
+
set({ authProvider: 'openrouter', openRouterApiKey: apiKey, isLoading: false })
|
| 86 |
+
} catch (err) {
|
| 87 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
|
| 91 |
+
saveOpenAIApiKey: async (apiKey: string) => {
|
| 92 |
+
set({ isLoading: true, error: null })
|
| 93 |
+
try {
|
| 94 |
+
await cliAuthApi.update({ authProvider: 'openai', openAiApiKey: apiKey })
|
| 95 |
+
await cliAuthApi.invalidateCache()
|
| 96 |
+
set({ authProvider: 'openai', openAiApiKey: apiKey, isLoading: false })
|
| 97 |
+
} catch (err) {
|
| 98 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
|
| 102 |
+
saveOpenCodeApiKey: async (apiKey: string, modelName?: string) => {
|
| 103 |
+
set({ isLoading: true, error: null })
|
| 104 |
+
try {
|
| 105 |
+
const updates: Record<string, unknown> = { authProvider: 'opencode', openCodeApiKey: apiKey }
|
| 106 |
+
if (modelName) updates.openCodeModelName = modelName
|
| 107 |
+
await cliAuthApi.update(updates as Partial<CliAuthConfig>)
|
| 108 |
+
await cliAuthApi.invalidateCache()
|
| 109 |
+
set({
|
| 110 |
+
authProvider: 'opencode',
|
| 111 |
+
openCodeApiKey: apiKey,
|
| 112 |
+
...(modelName ? { openCodeModelName: modelName } : {}),
|
| 113 |
+
isLoading: false,
|
| 114 |
+
})
|
| 115 |
+
} catch (err) {
|
| 116 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
|
| 120 |
+
saveLocalModelConfig: async (baseUrl: string, modelName: string) => {
|
| 121 |
+
set({ isLoading: true, error: null })
|
| 122 |
+
try {
|
| 123 |
+
await cliAuthApi.update({ authProvider: 'local', localBaseUrl: baseUrl, localModelName: modelName })
|
| 124 |
+
await cliAuthApi.invalidateCache()
|
| 125 |
+
set({ authProvider: 'local', localBaseUrl: baseUrl, localModelName: modelName, isLoading: false })
|
| 126 |
+
} catch (err) {
|
| 127 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 128 |
+
}
|
| 129 |
+
},
|
| 130 |
+
|
| 131 |
+
clearAuth: async () => {
|
| 132 |
+
set({ isLoading: true, error: null })
|
| 133 |
+
try {
|
| 134 |
+
await cliAuthApi.update({ authProvider: null })
|
| 135 |
+
await cliAuthApi.invalidateCache()
|
| 136 |
+
set({ authProvider: null, nvidiaApiKey: null, openRouterApiKey: null, openAiApiKey: null, openCodeApiKey: null, localBaseUrl: null, localModelName: null, isLoading: false })
|
| 137 |
+
} catch (err) {
|
| 138 |
+
set({ isLoading: false, error: err instanceof Error ? err.message : String(err) })
|
| 139 |
+
}
|
| 140 |
+
},
|
| 141 |
+
}))
|
desktop/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{"root":["./src/App.tsx","./src/main.test.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/__tests__/agentsSettings.test.tsx","./src/__tests__/diagnosticsSettings.test.tsx","./src/__tests__/generalSettings.test.tsx","./src/__tests__/mcpSettings.test.tsx","./src/__tests__/memorySettings.test.tsx","./src/__tests__/pages.test.tsx","./src/__tests__/pluginsSettings.test.tsx","./src/__tests__/skillsSettings.test.tsx","./src/__tests__/tauriCapabilities.test.ts","./src/api/activityStats.ts","./src/api/adapters.ts","./src/api/agents.ts","./src/api/cliTasks.ts","./src/api/client.test.ts","./src/api/client.ts","./src/api/computerUse.ts","./src/api/desktopUiPreferences.ts","./src/api/diagnostics.ts","./src/api/doctor.ts","./src/api/filesystem.ts","./src/api/h5Access.ts","./src/api/hahaOAuth.ts","./src/api/hahaOpenAIOAuth.ts","./src/api/mcp.ts","./src/api/memory.ts","./src/api/models.ts","./src/api/openTargets.test.ts","./src/api/openTargets.ts","./src/api/plugins.ts","./src/api/providers.ts","./src/api/search.ts","./src/api/sessions.test.ts","./src/api/sessions.ts","./src/api/settings.ts","./src/api/skills.ts","./src/api/tasks.ts","./src/api/teams.ts","./src/api/terminal.ts","./src/api/websocket.test.ts","./src/api/websocket.ts","./src/components/ErrorBoundary.test.tsx","./src/components/ErrorBoundary.tsx","./src/components/chat/AskUserQuestion.test.tsx","./src/components/chat/AskUserQuestion.tsx","./src/components/chat/AssistantMessage.tsx","./src/components/chat/AttachmentGallery.test.tsx","./src/components/chat/AttachmentGallery.tsx","./src/components/chat/ChatInput.test.tsx","./src/components/chat/ChatInput.tsx","./src/components/chat/CodeViewer.test.tsx","./src/components/chat/CodeViewer.tsx","./src/components/chat/ComposerDropOverlay.tsx","./src/components/chat/ComputerUsePermissionModal.test.tsx","./src/components/chat/ComputerUsePermissionModal.tsx","./src/components/chat/ContextUsageIndicator.test.tsx","./src/components/chat/ContextUsageIndicator.tsx","./src/components/chat/CurrentTurnChangeCard.tsx","./src/components/chat/DiffViewer.tsx","./src/components/chat/FileSearchMenu.test.tsx","./src/components/chat/FileSearchMenu.tsx","./src/components/chat/ImageGalleryModal.tsx","./src/components/chat/InlineImageGallery.tsx","./src/components/chat/InlineTaskSummary.tsx","./src/components/chat/LocalSlashCommandPanel.test.tsx","./src/components/chat/LocalSlashCommandPanel.tsx","./src/components/chat/MermaidRenderer.test.tsx","./src/components/chat/MermaidRenderer.tsx","./src/components/chat/MessageActionBar.tsx","./src/components/chat/MessageList.test.tsx","./src/components/chat/MessageList.tsx","./src/components/chat/PermissionDialog.tsx","./src/components/chat/SessionTaskBar.test.tsx","./src/components/chat/SessionTaskBar.tsx","./src/components/chat/StreamingIndicator.tsx","./src/components/chat/TerminalChrome.tsx","./src/components/chat/ThinkingBlock.tsx","./src/components/chat/ToolCallBlock.tsx","./src/components/chat/ToolCallGroup.tsx","./src/components/chat/ToolResultBlock.tsx","./src/components/chat/UserMessage.test.tsx","./src/components/chat/UserMessage.tsx","./src/components/chat/chatBlocks.test.tsx","./src/components/chat/clipboard.ts","./src/components/chat/composerUtils.test.ts","./src/components/chat/composerUtils.ts","./src/components/chat/useComposerFileDrop.ts","./src/components/companion/CompanionControls.tsx","./src/components/companion/CompanionTranscript.tsx","./src/components/companion/CompanionVideoPanel.tsx","./src/components/controls/ModelSelector.test.tsx","./src/components/controls/ModelSelector.tsx","./src/components/controls/PermissionModeSelector.test.tsx","./src/components/controls/PermissionModeSelector.tsx","./src/components/doctor/DoctorPanel.tsx","./src/components/layout/AppShell.test.tsx","./src/components/layout/AppShell.tsx","./src/components/layout/ContentRouter.test.tsx","./src/components/layout/ContentRouter.tsx","./src/components/layout/H5ConnectionView.tsx","./src/components/layout/OpenProjectMenu.test.tsx","./src/components/layout/OpenProjectMenu.tsx","./src/components/layout/Sidebar.test.tsx","./src/components/layout/Sidebar.tsx","./src/components/layout/StartupErrorView.test.tsx","./src/components/layout/StartupErrorView.tsx","./src/components/layout/StatusBar.tsx","./src/components/layout/TabBar.test.tsx","./src/components/layout/TabBar.tsx","./src/components/layout/TitleBar.tsx","./src/components/layout/WindowControls.test.tsx","./src/components/layout/WindowControls.tsx","./src/components/markdown/MarkdownRenderer.test.tsx","./src/components/markdown/MarkdownRenderer.tsx","./src/components/plugins/PluginDetail.tsx","./src/components/plugins/PluginList.tsx","./src/components/settings/ChatGPTOfficialLogin.test.tsx","./src/components/settings/ChatGPTOfficialLogin.tsx","./src/components/settings/ClaudeOfficialLogin.tsx","./src/components/shared/Button.tsx","./src/components/shared/ConfirmDialog.tsx","./src/components/shared/CopyButton.tsx","./src/components/shared/DirectoryPicker.test.tsx","./src/components/shared/DirectoryPicker.tsx","./src/components/shared/Dropdown.tsx","./src/components/shared/Input.tsx","./src/components/shared/MobileBottomSheet.tsx","./src/components/shared/Modal.test.tsx","./src/components/shared/Modal.tsx","./src/components/shared/ProjectContextChip.test.tsx","./src/components/shared/ProjectContextChip.tsx","./src/components/shared/RepositoryLaunchControls.test.tsx","./src/components/shared/RepositoryLaunchControls.tsx","./src/components/shared/Spinner.tsx","./src/components/shared/Textarea.tsx","./src/components/shared/Toast.tsx","./src/components/shared/UpdateChecker.test.tsx","./src/components/shared/UpdateChecker.tsx","./src/components/skills/SkillDetail.test.tsx","./src/components/skills/SkillDetail.tsx","./src/components/skills/SkillList.tsx","./src/components/tasks/DayOfWeekPicker.tsx","./src/components/tasks/NewTaskModal.test.tsx","./src/components/tasks/NewTaskModal.tsx","./src/components/tasks/PromptEditor.tsx","./src/components/tasks/TaskEmptyState.tsx","./src/components/tasks/TaskList.tsx","./src/components/tasks/TaskRow.tsx","./src/components/tasks/TaskRunsPanel.test.tsx","./src/components/tasks/TaskRunsPanel.tsx","./src/components/teams/TeamStatusBar.tsx","./src/components/workspace/WorkspaceCodeSurface.tsx","./src/components/workspace/WorkspacePanel.test.tsx","./src/components/workspace/WorkspacePanel.tsx","./src/config/spinnerVerbs.ts","./src/constants/modelCatalog.ts","./src/constants/openaiOfficialProvider.ts","./src/hooks/useCompanionAudio.ts","./src/hooks/useCompanionWebSocket.ts","./src/hooks/useKeyboardShortcuts.test.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useMicrophone.ts","./src/hooks/useMobileViewport.test.tsx","./src/hooks/useMobileViewport.ts","./src/hooks/useScheduledTaskDesktopNotifications.test.tsx","./src/hooks/useScheduledTaskDesktopNotifications.ts","./src/hooks/useSelectionPopoverDismiss.ts","./src/hooks/useWebcam.ts","./src/i18n/index.test.tsx","./src/i18n/index.ts","./src/i18n/locales/en.ts","./src/i18n/locales/zh.ts","./src/lib/appZoom.test.ts","./src/lib/appZoom.ts","./src/lib/audioEngine.ts","./src/lib/composerAttachments.test.ts","./src/lib/composerAttachments.ts","./src/lib/cronDescribe.ts","./src/lib/desktopNotificationNavigation.test.ts","./src/lib/desktopNotificationNavigation.ts","./src/lib/desktopNotifications.test.ts","./src/lib/desktopNotifications.ts","./src/lib/desktopRuntime.test.ts","./src/lib/desktopRuntime.ts","./src/lib/diagnosticsCapture.ts","./src/lib/doctorRepair.test.ts","./src/lib/doctorRepair.ts","./src/lib/formatBytes.ts","./src/lib/parseRunOutput.ts","./src/lib/persistenceMigrations.test.ts","./src/lib/persistenceMigrations.ts","./src/lib/providerSettingsJson.ts","./src/lib/sessionTitle.test.ts","./src/lib/sessionTitle.ts","./src/lib/__tests__/cronDescribe.test.ts","./src/lib/__tests__/providerSettingsJson.test.ts","./src/mocks/data.ts","./src/pages/ActiveSession.test.tsx","./src/pages/ActiveSession.tsx","./src/pages/ActivitySettings.test.tsx","./src/pages/ActivitySettings.tsx","./src/pages/AdapterSettings.test.tsx","./src/pages/AdapterSettings.tsx","./src/pages/AgentTeams.tsx","./src/pages/Companion.tsx","./src/pages/ComputerUseSettings.test.tsx","./src/pages/ComputerUseSettings.tsx","./src/pages/DiagnosticsSettings.tsx","./src/pages/EmptySession.test.tsx","./src/pages/EmptySession.tsx","./src/pages/McpSettings.tsx","./src/pages/MemorySettings.tsx","./src/pages/NewTaskModal.tsx","./src/pages/ScheduledTasks.tsx","./src/pages/ScheduledTasksEmpty.tsx","./src/pages/ScheduledTasksList.tsx","./src/pages/SessionControls.tsx","./src/pages/Settings.tsx","./src/pages/TerminalSettings.test.tsx","./src/pages/TerminalSettings.tsx","./src/pages/ToolInspection.tsx","./src/stores/adapterStore.test.ts","./src/stores/adapterStore.ts","./src/stores/agentStore.ts","./src/stores/chatStore.test.ts","./src/stores/chatStore.ts","./src/stores/cliTaskStore.test.ts","./src/stores/cliTaskStore.ts","./src/stores/companionStore.ts","./src/stores/hahaOAuthStore.test.ts","./src/stores/hahaOAuthStore.ts","./src/stores/hahaOpenAIOAuthStore.test.ts","./src/stores/hahaOpenAIOAuthStore.ts","./src/stores/mcpStore.ts","./src/stores/memoryStore.ts","./src/stores/openTargetStore.test.ts","./src/stores/openTargetStore.ts","./src/stores/pluginStore.test.ts","./src/stores/pluginStore.ts","./src/stores/providerStore.test.ts","./src/stores/providerStore.ts","./src/stores/sessionRuntimeStore.ts","./src/stores/sessionStore.test.ts","./src/stores/sessionStore.ts","./src/stores/settingsStore.test.ts","./src/stores/settingsStore.ts","./src/stores/skillStore.ts","./src/stores/tabStore.test.ts","./src/stores/tabStore.ts","./src/stores/taskStore.ts","./src/stores/teamStore.ts","./src/stores/terminalPanelStore.ts","./src/stores/uiStore.test.ts","./src/stores/uiStore.ts","./src/stores/updateStore.test.ts","./src/stores/updateStore.ts","./src/stores/workspaceChatContextStore.test.ts","./src/stores/workspaceChatContextStore.ts","./src/stores/workspacePanelStore.test.ts","./src/stores/workspacePanelStore.ts","./src/theme/globals.test.ts","./src/types/adapter.ts","./src/types/chat.ts","./src/types/cliTask.ts","./src/types/companion.ts","./src/types/mcp.ts","./src/types/memory.ts","./src/types/plugin.ts","./src/types/provider.ts","./src/types/providerPreset.ts","./src/types/qrcode.d.ts","./src/types/runtime.ts","./src/types/session.ts","./src/types/settings.ts","./src/types/skill.ts","./src/types/task.ts","./src/types/team.ts"],"errors":true,"version":"5.9.3"}
|
|
|
|
| 1 |
+
{"root":["./src/App.tsx","./src/main.test.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/__tests__/agentsSettings.test.tsx","./src/__tests__/diagnosticsSettings.test.tsx","./src/__tests__/generalSettings.test.tsx","./src/__tests__/mcpSettings.test.tsx","./src/__tests__/memorySettings.test.tsx","./src/__tests__/pages.test.tsx","./src/__tests__/pluginsSettings.test.tsx","./src/__tests__/skillsSettings.test.tsx","./src/__tests__/tauriCapabilities.test.ts","./src/api/activityStats.ts","./src/api/adapters.ts","./src/api/agents.ts","./src/api/cliTasks.ts","./src/api/client.test.ts","./src/api/client.ts","./src/api/computerUse.ts","./src/api/desktopUiPreferences.ts","./src/api/diagnostics.ts","./src/api/doctor.ts","./src/api/filesystem.ts","./src/api/h5Access.ts","./src/api/hahaOAuth.ts","./src/api/hahaOpenAIOAuth.ts","./src/api/mcp.ts","./src/api/memory.ts","./src/api/models.ts","./src/api/openTargets.test.ts","./src/api/openTargets.ts","./src/api/plugins.ts","./src/api/providers.ts","./src/api/search.ts","./src/api/sessions.test.ts","./src/api/sessions.ts","./src/api/settings.ts","./src/api/skills.ts","./src/api/tasks.ts","./src/api/teams.ts","./src/api/terminal.ts","./src/api/websocket.test.ts","./src/api/websocket.ts","./src/components/ErrorBoundary.test.tsx","./src/components/ErrorBoundary.tsx","./src/components/chat/AskUserQuestion.test.tsx","./src/components/chat/AskUserQuestion.tsx","./src/components/chat/AssistantMessage.tsx","./src/components/chat/AttachmentGallery.test.tsx","./src/components/chat/AttachmentGallery.tsx","./src/components/chat/ChatInput.test.tsx","./src/components/chat/ChatInput.tsx","./src/components/chat/CodeViewer.test.tsx","./src/components/chat/CodeViewer.tsx","./src/components/chat/ComposerDropOverlay.tsx","./src/components/chat/ComputerUsePermissionModal.test.tsx","./src/components/chat/ComputerUsePermissionModal.tsx","./src/components/chat/ContextUsageIndicator.test.tsx","./src/components/chat/ContextUsageIndicator.tsx","./src/components/chat/CurrentTurnChangeCard.tsx","./src/components/chat/DiffViewer.tsx","./src/components/chat/FileSearchMenu.test.tsx","./src/components/chat/FileSearchMenu.tsx","./src/components/chat/ImageGalleryModal.tsx","./src/components/chat/InlineImageGallery.tsx","./src/components/chat/InlineTaskSummary.tsx","./src/components/chat/LocalSlashCommandPanel.test.tsx","./src/components/chat/LocalSlashCommandPanel.tsx","./src/components/chat/MermaidRenderer.test.tsx","./src/components/chat/MermaidRenderer.tsx","./src/components/chat/MessageActionBar.tsx","./src/components/chat/MessageList.test.tsx","./src/components/chat/MessageList.tsx","./src/components/chat/PermissionDialog.tsx","./src/components/chat/SessionTaskBar.test.tsx","./src/components/chat/SessionTaskBar.tsx","./src/components/chat/StreamingIndicator.tsx","./src/components/chat/TerminalChrome.tsx","./src/components/chat/ThinkingBlock.tsx","./src/components/chat/ToolCallBlock.tsx","./src/components/chat/ToolCallGroup.tsx","./src/components/chat/ToolResultBlock.tsx","./src/components/chat/UserMessage.test.tsx","./src/components/chat/UserMessage.tsx","./src/components/chat/chatBlocks.test.tsx","./src/components/chat/clipboard.ts","./src/components/chat/composerUtils.test.ts","./src/components/chat/composerUtils.ts","./src/components/chat/useComposerFileDrop.ts","./src/components/companion/CompanionControls.tsx","./src/components/companion/CompanionTopBar.tsx","./src/components/companion/CompanionTranscript.tsx","./src/components/companion/CompanionVideoPanel.tsx","./src/components/companion/ScenarioSelector.tsx","./src/components/companion/ScreenShareDialog.tsx","./src/components/controls/ModelSelector.test.tsx","./src/components/controls/ModelSelector.tsx","./src/components/controls/PermissionModeSelector.test.tsx","./src/components/controls/PermissionModeSelector.tsx","./src/components/doctor/DoctorPanel.tsx","./src/components/layout/AppShell.test.tsx","./src/components/layout/AppShell.tsx","./src/components/layout/ContentRouter.test.tsx","./src/components/layout/ContentRouter.tsx","./src/components/layout/H5ConnectionView.tsx","./src/components/layout/OpenProjectMenu.test.tsx","./src/components/layout/OpenProjectMenu.tsx","./src/components/layout/Sidebar.test.tsx","./src/components/layout/Sidebar.tsx","./src/components/layout/StartupErrorView.test.tsx","./src/components/layout/StartupErrorView.tsx","./src/components/layout/StatusBar.tsx","./src/components/layout/TabBar.test.tsx","./src/components/layout/TabBar.tsx","./src/components/layout/TitleBar.tsx","./src/components/layout/WindowControls.test.tsx","./src/components/layout/WindowControls.tsx","./src/components/markdown/MarkdownRenderer.test.tsx","./src/components/markdown/MarkdownRenderer.tsx","./src/components/plugins/PluginDetail.tsx","./src/components/plugins/PluginList.tsx","./src/components/settings/ChatGPTOfficialLogin.test.tsx","./src/components/settings/ChatGPTOfficialLogin.tsx","./src/components/settings/ClaudeOfficialLogin.tsx","./src/components/shared/Button.tsx","./src/components/shared/ConfirmDialog.tsx","./src/components/shared/CopyButton.tsx","./src/components/shared/DirectoryPicker.test.tsx","./src/components/shared/DirectoryPicker.tsx","./src/components/shared/Dropdown.tsx","./src/components/shared/Input.tsx","./src/components/shared/MobileBottomSheet.tsx","./src/components/shared/Modal.test.tsx","./src/components/shared/Modal.tsx","./src/components/shared/ProjectContextChip.test.tsx","./src/components/shared/ProjectContextChip.tsx","./src/components/shared/RepositoryLaunchControls.test.tsx","./src/components/shared/RepositoryLaunchControls.tsx","./src/components/shared/Spinner.tsx","./src/components/shared/Textarea.tsx","./src/components/shared/Toast.tsx","./src/components/shared/UpdateChecker.test.tsx","./src/components/shared/UpdateChecker.tsx","./src/components/skills/SkillDetail.test.tsx","./src/components/skills/SkillDetail.tsx","./src/components/skills/SkillList.tsx","./src/components/tasks/DayOfWeekPicker.tsx","./src/components/tasks/NewTaskModal.test.tsx","./src/components/tasks/NewTaskModal.tsx","./src/components/tasks/PromptEditor.tsx","./src/components/tasks/TaskEmptyState.tsx","./src/components/tasks/TaskList.tsx","./src/components/tasks/TaskRow.tsx","./src/components/tasks/TaskRunsPanel.test.tsx","./src/components/tasks/TaskRunsPanel.tsx","./src/components/teams/TeamStatusBar.tsx","./src/components/workspace/WorkspaceCodeSurface.tsx","./src/components/workspace/WorkspacePanel.test.tsx","./src/components/workspace/WorkspacePanel.tsx","./src/config/spinnerVerbs.ts","./src/constants/modelCatalog.ts","./src/constants/openaiOfficialProvider.ts","./src/hooks/useCameraDevices.ts","./src/hooks/useCompanionAudio.ts","./src/hooks/useCompanionWebSocket.ts","./src/hooks/useKeyboardShortcuts.test.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useMicrophone.ts","./src/hooks/useMiniCPMoBackend.ts","./src/hooks/useMobileViewport.test.tsx","./src/hooks/useMobileViewport.ts","./src/hooks/useScheduledTaskDesktopNotifications.test.tsx","./src/hooks/useScheduledTaskDesktopNotifications.ts","./src/hooks/useScreenShare.ts","./src/hooks/useSelectionPopoverDismiss.ts","./src/hooks/useWebcam.ts","./src/i18n/index.test.tsx","./src/i18n/index.ts","./src/i18n/locales/en.ts","./src/i18n/locales/zh.ts","./src/lib/appZoom.test.ts","./src/lib/appZoom.ts","./src/lib/audioEngine.ts","./src/lib/composerAttachments.test.ts","./src/lib/composerAttachments.ts","./src/lib/cronDescribe.ts","./src/lib/desktopNotificationNavigation.test.ts","./src/lib/desktopNotificationNavigation.ts","./src/lib/desktopNotifications.test.ts","./src/lib/desktopNotifications.ts","./src/lib/desktopRuntime.test.ts","./src/lib/desktopRuntime.ts","./src/lib/diagnosticsCapture.ts","./src/lib/doctorRepair.test.ts","./src/lib/doctorRepair.ts","./src/lib/formatBytes.ts","./src/lib/parseRunOutput.ts","./src/lib/persistenceMigrations.test.ts","./src/lib/persistenceMigrations.ts","./src/lib/providerSettingsJson.ts","./src/lib/sessionTitle.test.ts","./src/lib/sessionTitle.ts","./src/lib/__tests__/cronDescribe.test.ts","./src/lib/__tests__/providerSettingsJson.test.ts","./src/mocks/data.ts","./src/pages/ActiveSession.test.tsx","./src/pages/ActiveSession.tsx","./src/pages/ActivitySettings.test.tsx","./src/pages/ActivitySettings.tsx","./src/pages/AdapterSettings.test.tsx","./src/pages/AdapterSettings.tsx","./src/pages/AgentTeams.tsx","./src/pages/Companion.tsx","./src/pages/ComputerUseSettings.test.tsx","./src/pages/ComputerUseSettings.tsx","./src/pages/DiagnosticsSettings.tsx","./src/pages/EmptySession.test.tsx","./src/pages/EmptySession.tsx","./src/pages/McpSettings.tsx","./src/pages/MemorySettings.tsx","./src/pages/NewTaskModal.tsx","./src/pages/ScheduledTasks.tsx","./src/pages/ScheduledTasksEmpty.tsx","./src/pages/ScheduledTasksList.tsx","./src/pages/SessionControls.tsx","./src/pages/Settings.tsx","./src/pages/TerminalSettings.test.tsx","./src/pages/TerminalSettings.tsx","./src/pages/ToolInspection.tsx","./src/stores/adapterStore.test.ts","./src/stores/adapterStore.ts","./src/stores/agentStore.ts","./src/stores/chatStore.test.ts","./src/stores/chatStore.ts","./src/stores/cliTaskStore.test.ts","./src/stores/cliTaskStore.ts","./src/stores/companionStore.ts","./src/stores/hahaOAuthStore.test.ts","./src/stores/hahaOAuthStore.ts","./src/stores/hahaOpenAIOAuthStore.test.ts","./src/stores/hahaOpenAIOAuthStore.ts","./src/stores/mcpStore.ts","./src/stores/memoryStore.ts","./src/stores/openTargetStore.test.ts","./src/stores/openTargetStore.ts","./src/stores/pluginStore.test.ts","./src/stores/pluginStore.ts","./src/stores/providerStore.test.ts","./src/stores/providerStore.ts","./src/stores/sessionRuntimeStore.ts","./src/stores/sessionStore.test.ts","./src/stores/sessionStore.ts","./src/stores/settingsStore.test.ts","./src/stores/settingsStore.ts","./src/stores/skillStore.ts","./src/stores/tabStore.test.ts","./src/stores/tabStore.ts","./src/stores/taskStore.ts","./src/stores/teamStore.ts","./src/stores/terminalPanelStore.ts","./src/stores/uiStore.test.ts","./src/stores/uiStore.ts","./src/stores/updateStore.test.ts","./src/stores/updateStore.ts","./src/stores/workspaceChatContextStore.test.ts","./src/stores/workspaceChatContextStore.ts","./src/stores/workspacePanelStore.test.ts","./src/stores/workspacePanelStore.ts","./src/theme/globals.test.ts","./src/types/adapter.ts","./src/types/chat.ts","./src/types/cliTask.ts","./src/types/companion.ts","./src/types/mcp.ts","./src/types/memory.ts","./src/types/plugin.ts","./src/types/provider.ts","./src/types/providerPreset.ts","./src/types/qrcode.d.ts","./src/types/runtime.ts","./src/types/session.ts","./src/types/settings.ts","./src/types/skill.ts","./src/types/task.ts","./src/types/team.ts"],"errors":true,"version":"5.9.3"}
|
src/server/api/cli-auth.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* CLI Auth Provider REST API
|
| 3 |
+
*
|
| 4 |
+
* GET /api/cli-auth — read current CLI auth provider config from ~/.claude.json
|
| 5 |
+
* PUT /api/cli-auth — update CLI auth provider config in ~/.claude.json
|
| 6 |
+
* POST /api/cli-auth/invalidate — invalidate the cliProviderModelCache
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { homedir } from 'node:os'
|
| 10 |
+
import { readFileSync, writeFileSync } from 'node:fs'
|
| 11 |
+
import { join } from 'node:path'
|
| 12 |
+
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
| 13 |
+
|
| 14 |
+
type AuthProvider = 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
|
| 15 |
+
|
| 16 |
+
type CliAuthConfig = {
|
| 17 |
+
authProvider: AuthProvider | null
|
| 18 |
+
nvidiaApiKey?: string
|
| 19 |
+
openRouterApiKey?: string
|
| 20 |
+
openAiApiKey?: string
|
| 21 |
+
openAiAccessToken?: string
|
| 22 |
+
openCodeApiKey?: string
|
| 23 |
+
openCodeModelName?: string
|
| 24 |
+
localBaseUrl?: string
|
| 25 |
+
localModelName?: string
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function getClaudeJsonPath(): string {
|
| 29 |
+
return join(homedir(), '.claude.json')
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function readCliAuthConfig(): CliAuthConfig {
|
| 33 |
+
try {
|
| 34 |
+
const raw = readFileSync(getClaudeJsonPath(), 'utf8')
|
| 35 |
+
const config = JSON.parse(raw) as Record<string, unknown>
|
| 36 |
+
return {
|
| 37 |
+
authProvider: (config.authProvider as AuthProvider) || null,
|
| 38 |
+
nvidiaApiKey: config.nvidiaApiKey as string | undefined,
|
| 39 |
+
openRouterApiKey: config.openRouterApiKey as string | undefined,
|
| 40 |
+
openAiApiKey: config.openAiApiKey as string | undefined,
|
| 41 |
+
openAiAccessToken: config.openAiAccessToken as string | undefined,
|
| 42 |
+
openCodeApiKey: config.openCodeApiKey as string | undefined,
|
| 43 |
+
openCodeModelName: config.openCodeModelName as string | undefined,
|
| 44 |
+
localBaseUrl: config.localBaseUrl as string | undefined,
|
| 45 |
+
localModelName: config.localModelName as string | undefined,
|
| 46 |
+
}
|
| 47 |
+
} catch {
|
| 48 |
+
return { authProvider: null }
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function writeCliAuthConfig(updates: Partial<CliAuthConfig>): void {
|
| 53 |
+
const filePath = getClaudeJsonPath()
|
| 54 |
+
let config: Record<string, unknown> = {}
|
| 55 |
+
try {
|
| 56 |
+
const raw = readFileSync(filePath, 'utf8')
|
| 57 |
+
config = JSON.parse(raw) as Record<string, unknown>
|
| 58 |
+
} catch {
|
| 59 |
+
// file doesn't exist yet, start fresh
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if (updates.authProvider !== undefined) {
|
| 63 |
+
config.authProvider = updates.authProvider || undefined
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if (updates.nvidiaApiKey !== undefined) {
|
| 67 |
+
config.nvidiaApiKey = updates.nvidiaApiKey || undefined
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if (updates.openRouterApiKey !== undefined) {
|
| 71 |
+
config.openRouterApiKey = updates.openRouterApiKey || undefined
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if (updates.openAiApiKey !== undefined) {
|
| 75 |
+
config.openAiApiKey = updates.openAiApiKey || undefined
|
| 76 |
+
config.openAiAccessToken = undefined
|
| 77 |
+
config.openAiRefreshToken = undefined
|
| 78 |
+
config.openAiTokenExpiresAt = undefined
|
| 79 |
+
config.openAiWorkspaceId = undefined
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (updates.openAiAccessToken !== undefined) {
|
| 83 |
+
config.openAiAccessToken = updates.openAiAccessToken || undefined
|
| 84 |
+
config.openAiApiKey = undefined
|
| 85 |
+
config.openAiRefreshToken = undefined
|
| 86 |
+
config.openAiTokenExpiresAt = undefined
|
| 87 |
+
config.openAiWorkspaceId = undefined
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if (updates.openCodeApiKey !== undefined) {
|
| 91 |
+
config.openCodeApiKey = updates.openCodeApiKey || undefined
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (updates.openCodeModelName !== undefined) {
|
| 95 |
+
config.openCodeModelName = updates.openCodeModelName || undefined
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
if (updates.localBaseUrl !== undefined) {
|
| 99 |
+
config.localBaseUrl = updates.localBaseUrl || undefined
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if (updates.localModelName !== undefined) {
|
| 103 |
+
config.localModelName = updates.localModelName || undefined
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n')
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export async function handleCliAuthApi(
|
| 110 |
+
req: Request,
|
| 111 |
+
_url: URL,
|
| 112 |
+
segments: string[],
|
| 113 |
+
): Promise<Response> {
|
| 114 |
+
try {
|
| 115 |
+
const sub = segments[2]
|
| 116 |
+
|
| 117 |
+
if (sub === 'invalidate') {
|
| 118 |
+
if (req.method !== 'POST') throw methodNotAllowed(req.method)
|
| 119 |
+
const { invalidateCliProviderModelCache } = await import('./models.js')
|
| 120 |
+
invalidateCliProviderModelCache()
|
| 121 |
+
return Response.json({ ok: true })
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (!sub) {
|
| 125 |
+
if (req.method === 'GET') {
|
| 126 |
+
const config = readCliAuthConfig()
|
| 127 |
+
return Response.json(config)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
if (req.method === 'PUT') {
|
| 131 |
+
const body = await parseJsonBody(req)
|
| 132 |
+
writeCliAuthConfig(body as Partial<CliAuthConfig>)
|
| 133 |
+
const { invalidateCliProviderModelCache } = await import('./models.js')
|
| 134 |
+
invalidateCliProviderModelCache()
|
| 135 |
+
return Response.json({ ok: true })
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
throw methodNotAllowed(req.method)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
throw ApiError.notFound(`Unknown cli-auth endpoint: ${sub}`)
|
| 142 |
+
} catch (error) {
|
| 143 |
+
return errorResponse(error)
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
| 148 |
+
try {
|
| 149 |
+
return (await req.json()) as Record<string, unknown>
|
| 150 |
+
} catch {
|
| 151 |
+
throw ApiError.badRequest('Invalid JSON body')
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
function methodNotAllowed(method: string): ApiError {
|
| 156 |
+
return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED')
|
| 157 |
+
}
|
src/server/api/models.ts
CHANGED
|
@@ -167,6 +167,11 @@ let cliProviderModelCache: ApiModelInfo[] | null = null
|
|
| 167 |
let cliProviderModelCacheTime = 0
|
| 168 |
const CLI_PROVIDER_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
async function fetchCliProviderModels(): Promise<ApiModelInfo[]> {
|
| 171 |
if (cliProviderModelCache && Date.now() - cliProviderModelCacheTime < CLI_PROVIDER_CACHE_TTL) {
|
| 172 |
return cliProviderModelCache
|
|
@@ -285,7 +290,57 @@ export async function handleModelsApi(
|
|
| 285 |
|
| 286 |
// ─── Handlers ─────────────────────────────────────────────────────────────────
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
async function handleModelsList(): Promise<Response> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
const { providers, activeId } = await providerService.listProviders()
|
| 290 |
if (isOpenAIOfficialProviderId(activeId)) {
|
| 291 |
return Response.json({
|
|
@@ -306,15 +361,6 @@ async function handleModelsList(): Promise<Response> {
|
|
| 306 |
})
|
| 307 |
}
|
| 308 |
|
| 309 |
-
// No cc-haha provider active — check if CLI has an auth provider configured
|
| 310 |
-
const cliModels = await fetchCliProviderModels()
|
| 311 |
-
if (cliModels.length > 0) {
|
| 312 |
-
return Response.json({
|
| 313 |
-
models: cliModels,
|
| 314 |
-
provider: { id: 'cli', name: 'CLI Provider' },
|
| 315 |
-
})
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
return Response.json({ models: getStandaloneModelList(), provider: null })
|
| 319 |
}
|
| 320 |
|
|
|
|
| 167 |
let cliProviderModelCacheTime = 0
|
| 168 |
const CLI_PROVIDER_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
| 169 |
|
| 170 |
+
export function invalidateCliProviderModelCache(): void {
|
| 171 |
+
cliProviderModelCache = null
|
| 172 |
+
cliProviderModelCacheTime = 0
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
async function fetchCliProviderModels(): Promise<ApiModelInfo[]> {
|
| 176 |
if (cliProviderModelCache && Date.now() - cliProviderModelCacheTime < CLI_PROVIDER_CACHE_TTL) {
|
| 177 |
return cliProviderModelCache
|
|
|
|
| 290 |
|
| 291 |
// ─── Handlers ─────────────────────────────────────────────────────────────────
|
| 292 |
|
| 293 |
+
async function readCliAuthProvider(): Promise<{
|
| 294 |
+
authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
|
| 295 |
+
} | null> {
|
| 296 |
+
try {
|
| 297 |
+
const { homedir } = await import('node:os')
|
| 298 |
+
const { readFileSync } = await import('node:fs')
|
| 299 |
+
const { join } = await import('node:path')
|
| 300 |
+
const raw = readFileSync(join(homedir(), '.claude.json'), 'utf8')
|
| 301 |
+
return JSON.parse(raw)
|
| 302 |
+
} catch {
|
| 303 |
+
return null
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
const CLI_PROVIDER_NAMES: Record<string, string> = {
|
| 308 |
+
opencode: 'OpenCode Zen',
|
| 309 |
+
nvidia: 'NVIDIA',
|
| 310 |
+
openrouter: 'OpenRouter',
|
| 311 |
+
local: 'Local',
|
| 312 |
+
anthropic: 'Anthropic',
|
| 313 |
+
openai: 'OpenAI',
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
async function handleModelsList(): Promise<Response> {
|
| 317 |
+
// First check if CLI has an authProvider configured — this takes precedence
|
| 318 |
+
// over cc-haha providers since CLI's /login command manages it
|
| 319 |
+
const cliConfig = await readCliAuthProvider()
|
| 320 |
+
const cliAuthProvider = cliConfig?.authProvider
|
| 321 |
+
|
| 322 |
+
if (cliAuthProvider && cliAuthProvider !== 'anthropic' && cliAuthProvider !== 'openai') {
|
| 323 |
+
const cliModels = await fetchCliProviderModels()
|
| 324 |
+
if (cliModels.length > 0) {
|
| 325 |
+
return Response.json({
|
| 326 |
+
models: cliModels,
|
| 327 |
+
provider: {
|
| 328 |
+
id: `cli-${cliAuthProvider}`,
|
| 329 |
+
name: CLI_PROVIDER_NAMES[cliAuthProvider] || cliAuthProvider,
|
| 330 |
+
},
|
| 331 |
+
})
|
| 332 |
+
}
|
| 333 |
+
// If no models fetched yet but authProvider is set, still return provider info
|
| 334 |
+
// The model list will be populated once available
|
| 335 |
+
return Response.json({
|
| 336 |
+
models: [],
|
| 337 |
+
provider: {
|
| 338 |
+
id: `cli-${cliAuthProvider}`,
|
| 339 |
+
name: CLI_PROVIDER_NAMES[cliAuthProvider] || cliAuthProvider,
|
| 340 |
+
},
|
| 341 |
+
})
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
const { providers, activeId } = await providerService.listProviders()
|
| 345 |
if (isOpenAIOfficialProviderId(activeId)) {
|
| 346 |
return Response.json({
|
|
|
|
| 361 |
})
|
| 362 |
}
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
return Response.json({ models: getStandaloneModelList(), provider: null })
|
| 365 |
}
|
| 366 |
|
src/server/router.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { handleActivityStatsApi } from './api/activityStats.js'
|
|
| 27 |
import { handleOpenTargetsApi } from './api/open-targets.js'
|
| 28 |
import { handleMemoryApi } from './api/memory.js'
|
| 29 |
import { handleDesktopUiApi } from './api/desktop-ui.js'
|
|
|
|
| 30 |
|
| 31 |
export async function handleApiRequest(req: Request, url: URL): Promise<Response> {
|
| 32 |
const path = url.pathname
|
|
@@ -119,6 +120,9 @@ export async function handleApiRequest(req: Request, url: URL): Promise<Response
|
|
| 119 |
case 'desktop-ui':
|
| 120 |
return handleDesktopUiApi(req, url, segments)
|
| 121 |
|
|
|
|
|
|
|
|
|
|
| 122 |
case 'filesystem':
|
| 123 |
return handleFilesystemRoute(url.pathname, url)
|
| 124 |
|
|
|
|
| 27 |
import { handleOpenTargetsApi } from './api/open-targets.js'
|
| 28 |
import { handleMemoryApi } from './api/memory.js'
|
| 29 |
import { handleDesktopUiApi } from './api/desktop-ui.js'
|
| 30 |
+
import { handleCliAuthApi } from './api/cli-auth.js'
|
| 31 |
|
| 32 |
export async function handleApiRequest(req: Request, url: URL): Promise<Response> {
|
| 33 |
const path = url.pathname
|
|
|
|
| 120 |
case 'desktop-ui':
|
| 121 |
return handleDesktopUiApi(req, url, segments)
|
| 122 |
|
| 123 |
+
case 'cli-auth':
|
| 124 |
+
return handleCliAuthApi(req, url, segments)
|
| 125 |
+
|
| 126 |
case 'filesystem':
|
| 127 |
return handleFilesystemRoute(url.pathname, url)
|
| 128 |
|
src/server/services/conversationService.ts
CHANGED
|
@@ -866,6 +866,61 @@ export class ConversationService {
|
|
| 866 |
return args
|
| 867 |
}
|
| 868 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
private async buildChildEnv(
|
| 870 |
workDir: string,
|
| 871 |
sdkUrl?: string,
|
|
@@ -923,8 +978,25 @@ export class ConversationService {
|
|
| 923 |
if (explicitProviderEnv && options?.model?.trim()) {
|
| 924 |
explicitProviderEnv.ANTHROPIC_MODEL = options.model.trim()
|
| 925 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
const attributionHeaderEnv = attributionHeaderEnvForModel(
|
| 927 |
options?.model?.trim() ||
|
|
|
|
| 928 |
explicitProviderEnv?.ANTHROPIC_MODEL ||
|
| 929 |
cleanEnv.ANTHROPIC_MODEL,
|
| 930 |
)
|
|
@@ -969,10 +1041,16 @@ export class ConversationService {
|
|
| 969 |
// 否则 CLI 会忽略 provider 的 AUTH_TOKEN、错误地走 OAuth 打到第三方
|
| 970 |
// endpoint。详见 src/utils/auth.ts isManagedOAuthContext()。
|
| 971 |
...(explicitProviderEnv ?? {}),
|
|
|
|
|
|
|
|
|
|
| 972 |
...networkEnv,
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
|
|
|
|
|
|
|
|
|
| 976 |
...attributionHeaderEnv,
|
| 977 |
}
|
| 978 |
}
|
|
@@ -1024,6 +1102,13 @@ export class ConversationService {
|
|
| 1024 |
return true
|
| 1025 |
}
|
| 1026 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1027 |
const configDir =
|
| 1028 |
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
| 1029 |
const ccHahaDir = path.join(configDir, 'cc-haha')
|
|
@@ -1078,6 +1163,13 @@ export class ConversationService {
|
|
| 1078 |
return false
|
| 1079 |
}
|
| 1080 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
const configDir =
|
| 1082 |
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
| 1083 |
const settingsPath = path.join(configDir, 'cc-haha', 'settings.json')
|
|
|
|
| 866 |
return args
|
| 867 |
}
|
| 868 |
|
| 869 |
+
private readCliGlobalConfig(): {
|
| 870 |
+
authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
|
| 871 |
+
openCodeApiKey?: string
|
| 872 |
+
openCodeModelName?: string
|
| 873 |
+
nvidiaApiKey?: string
|
| 874 |
+
openRouterApiKey?: string
|
| 875 |
+
localBaseUrl?: string
|
| 876 |
+
localModelName?: string
|
| 877 |
+
} | null {
|
| 878 |
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
| 879 |
+
const configPath = path.join(configDir, '.claude.json')
|
| 880 |
+
try {
|
| 881 |
+
const raw = fs.readFileSync(configPath, 'utf-8')
|
| 882 |
+
return JSON.parse(raw) as ReturnType<typeof this.readCliGlobalConfig>
|
| 883 |
+
} catch {
|
| 884 |
+
return null
|
| 885 |
+
}
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
private buildCliProviderEnv(
|
| 889 |
+
authProvider: 'opencode' | 'nvidia' | 'openrouter' | 'local',
|
| 890 |
+
config: NonNullable<ReturnType<typeof this.readCliGlobalConfig>>,
|
| 891 |
+
): Record<string, string> {
|
| 892 |
+
const env: Record<string, string> = {}
|
| 893 |
+
|
| 894 |
+
if (authProvider === 'opencode') {
|
| 895 |
+
if (config.openCodeApiKey) {
|
| 896 |
+
env.ANTHROPIC_API_KEY = config.openCodeApiKey
|
| 897 |
+
}
|
| 898 |
+
if (config.openCodeModelName) {
|
| 899 |
+
env.ANTHROPIC_MODEL = config.openCodeModelName
|
| 900 |
+
}
|
| 901 |
+
// opencode uses custom fetch override, no base URL override needed
|
| 902 |
+
} else if (authProvider === 'nvidia') {
|
| 903 |
+
if (config.nvidiaApiKey) {
|
| 904 |
+
env.ANTHROPIC_API_KEY = config.nvidiaApiKey
|
| 905 |
+
}
|
| 906 |
+
env.ANTHROPIC_BASE_URL = 'https://integrate.api.nvidia.com/v1'
|
| 907 |
+
} else if (authProvider === 'openrouter') {
|
| 908 |
+
if (config.openRouterApiKey) {
|
| 909 |
+
env.ANTHROPIC_API_KEY = config.openRouterApiKey
|
| 910 |
+
}
|
| 911 |
+
env.ANTHROPIC_BASE_URL = 'https://openrouter.ai/api/v1'
|
| 912 |
+
} else if (authProvider === 'local') {
|
| 913 |
+
if (config.localBaseUrl) {
|
| 914 |
+
env.ANTHROPIC_BASE_URL = config.localBaseUrl
|
| 915 |
+
}
|
| 916 |
+
if (config.localModelName) {
|
| 917 |
+
env.ANTHROPIC_MODEL = config.localModelName
|
| 918 |
+
}
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
return env
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
private async buildChildEnv(
|
| 925 |
workDir: string,
|
| 926 |
sdkUrl?: string,
|
|
|
|
| 978 |
if (explicitProviderEnv && options?.model?.trim()) {
|
| 979 |
explicitProviderEnv.ANTHROPIC_MODEL = options.model.trim()
|
| 980 |
}
|
| 981 |
+
|
| 982 |
+
// Check for CLI-managed authProvider (opencode, nvidia, openrouter, local)
|
| 983 |
+
// These are configured via CLI's /login command and stored in ~/.claude.json
|
| 984 |
+
const cliConfig = this.readCliGlobalConfig()
|
| 985 |
+
const cliAuthProvider = cliConfig?.authProvider
|
| 986 |
+
const isCliManagedProvider =
|
| 987 |
+
cliAuthProvider !== undefined &&
|
| 988 |
+
cliAuthProvider !== 'anthropic' &&
|
| 989 |
+
cliAuthProvider !== 'openai'
|
| 990 |
+
const cliProviderEnv = isCliManagedProvider
|
| 991 |
+
? this.buildCliProviderEnv(
|
| 992 |
+
cliAuthProvider as 'opencode' | 'nvidia' | 'openrouter' | 'local',
|
| 993 |
+
cliConfig!,
|
| 994 |
+
)
|
| 995 |
+
: null
|
| 996 |
+
|
| 997 |
const attributionHeaderEnv = attributionHeaderEnvForModel(
|
| 998 |
options?.model?.trim() ||
|
| 999 |
+
cliProviderEnv?.ANTHROPIC_MODEL ||
|
| 1000 |
explicitProviderEnv?.ANTHROPIC_MODEL ||
|
| 1001 |
cleanEnv.ANTHROPIC_MODEL,
|
| 1002 |
)
|
|
|
|
| 1041 |
// 否则 CLI 会忽略 provider 的 AUTH_TOKEN、错误地走 OAuth 打到第三方
|
| 1042 |
// endpoint。详见 src/utils/auth.ts isManagedOAuthContext()。
|
| 1043 |
...(explicitProviderEnv ?? {}),
|
| 1044 |
+
// CLI-managed provider env (opencode/nvidia/openrouter/local from ~/.claude.json)
|
| 1045 |
+
// takes precedence over cc-haha provider env
|
| 1046 |
+
...(cliProviderEnv ?? {}),
|
| 1047 |
...networkEnv,
|
| 1048 |
+
// Skip Desktop OAuth when using CLI-managed providers (they handle their own auth)
|
| 1049 |
+
...(isCliManagedProvider
|
| 1050 |
+
? {}
|
| 1051 |
+
: this.shouldMarkManagedOAuth(options?.providerId)
|
| 1052 |
+
? await this.buildOfficialOAuthEnv()
|
| 1053 |
+
: {}),
|
| 1054 |
...attributionHeaderEnv,
|
| 1055 |
}
|
| 1056 |
}
|
|
|
|
| 1102 |
return true
|
| 1103 |
}
|
| 1104 |
|
| 1105 |
+
// Check ~/.claude.json first - if CLI has a non-anthropic/non-openai authProvider,
|
| 1106 |
+
// strip inherited env so CLI reads from its own config
|
| 1107 |
+
const cliConfig = this.readCliGlobalConfig()
|
| 1108 |
+
if (cliConfig?.authProvider && cliConfig.authProvider !== 'anthropic' && cliConfig.authProvider !== 'openai') {
|
| 1109 |
+
return true
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
const configDir =
|
| 1113 |
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
| 1114 |
const ccHahaDir = path.join(configDir, 'cc-haha')
|
|
|
|
| 1163 |
return false
|
| 1164 |
}
|
| 1165 |
|
| 1166 |
+
// Check ~/.claude.json first - if CLI has a non-anthropic/non-openai authProvider,
|
| 1167 |
+
// it manages its own auth, skip Desktop OAuth injection
|
| 1168 |
+
const cliConfig = this.readCliGlobalConfig()
|
| 1169 |
+
if (cliConfig?.authProvider && cliConfig.authProvider !== 'anthropic' && cliConfig.authProvider !== 'openai') {
|
| 1170 |
+
return false
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
const configDir =
|
| 1174 |
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
| 1175 |
const settingsPath = path.join(configDir, 'cc-haha', 'settings.json')
|
src/server/services/providerService.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
| 35 |
normalizeModelMapping,
|
| 36 |
normalizeProvidersIndex,
|
| 37 |
} from './providerRuntimeEnv.js'
|
|
|
|
| 38 |
import { getProxyFetchOptions } from '../../utils/proxy.js'
|
| 39 |
import {
|
| 40 |
getManualNetworkProxyUrl,
|
|
@@ -82,6 +83,84 @@ export class ProviderService {
|
|
| 82 |
return path.join(this.getCcHahaDir(), 'providers.json')
|
| 83 |
}
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
private async readIndex(): Promise<ProvidersIndex> {
|
| 86 |
await ensurePersistentStorageUpgraded()
|
| 87 |
return readRecoverableJsonFile({
|
|
@@ -125,6 +204,8 @@ export class ProviderService {
|
|
| 125 |
// --- CRUD ---
|
| 126 |
|
| 127 |
async listProviders(): Promise<{ providers: SavedProvider[]; activeId: string | null }> {
|
|
|
|
|
|
|
| 128 |
const index = await this.readIndex()
|
| 129 |
return { providers: index.providers, activeId: index.activeId }
|
| 130 |
}
|
|
|
|
| 35 |
normalizeModelMapping,
|
| 36 |
normalizeProvidersIndex,
|
| 37 |
} from './providerRuntimeEnv.js'
|
| 38 |
+
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
| 39 |
import { getProxyFetchOptions } from '../../utils/proxy.js'
|
| 40 |
import {
|
| 41 |
getManualNetworkProxyUrl,
|
|
|
|
| 83 |
return path.join(this.getCcHahaDir(), 'providers.json')
|
| 84 |
}
|
| 85 |
|
| 86 |
+
/**
|
| 87 |
+
* Path to the TUI's global config file (~/.claude.json).
|
| 88 |
+
* Always at the home directory level, not under CLAUDE_CONFIG_DIR.
|
| 89 |
+
*/
|
| 90 |
+
private getTuiConfigPath(): string {
|
| 91 |
+
return path.join(os.homedir(), '.claude.json')
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Auto-import the provider configured in the TUI's global config (~/.claude.json).
|
| 96 |
+
* When the user configured a provider via /login in the TUI, this ensures the
|
| 97 |
+
* desktop app sees and uses the same provider automatically.
|
| 98 |
+
*
|
| 99 |
+
* Only imports when no desktop provider is currently active, to avoid overriding
|
| 100 |
+
* the user's desktop-specific choice.
|
| 101 |
+
*/
|
| 102 |
+
private async autoImportTuiProvider(): Promise<void> {
|
| 103 |
+
const tuiConfigPath = this.getTuiConfigPath()
|
| 104 |
+
let raw: string
|
| 105 |
+
try {
|
| 106 |
+
raw = await fs.readFile(tuiConfigPath, 'utf-8')
|
| 107 |
+
} catch {
|
| 108 |
+
return // No TUI config file
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
let tuiConfig: {
|
| 112 |
+
authProvider?: string
|
| 113 |
+
nvidiaApiKey?: string
|
| 114 |
+
}
|
| 115 |
+
try {
|
| 116 |
+
tuiConfig = JSON.parse(raw)
|
| 117 |
+
} catch {
|
| 118 |
+
return // Invalid JSON
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
const providerId = tuiConfig.authProvider
|
| 122 |
+
if (!providerId) return // No TUI provider configured
|
| 123 |
+
|
| 124 |
+
// Read current index to check if already imported
|
| 125 |
+
const index = await this.readIndex()
|
| 126 |
+
if (index.activeId) return // already has a desktop provider active
|
| 127 |
+
|
| 128 |
+
// Check if a TUI-imported provider already exists (e.g. from a previous import)
|
| 129 |
+
const tuiProviderTag = `tui-${providerId}`
|
| 130 |
+
const existing = index.providers.find(p => p.presetId === tuiProviderTag)
|
| 131 |
+
if (existing) {
|
| 132 |
+
index.activeId = existing.id
|
| 133 |
+
await this.writeIndex(index)
|
| 134 |
+
return
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Find the matching preset
|
| 138 |
+
const preset = PROVIDER_PRESETS.find(p => p.id === providerId)
|
| 139 |
+
if (!preset) return // No matching desktop preset for this TUI provider
|
| 140 |
+
|
| 141 |
+
// Extract API key based on provider type
|
| 142 |
+
let apiKey = ''
|
| 143 |
+
if (providerId === 'nvidia') {
|
| 144 |
+
apiKey = tuiConfig.nvidiaApiKey || ''
|
| 145 |
+
if (!apiKey) return
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const provider: SavedProvider = {
|
| 149 |
+
id: crypto.randomUUID(),
|
| 150 |
+
presetId: tuiProviderTag,
|
| 151 |
+
name: `TUI: ${preset.name}`,
|
| 152 |
+
apiKey,
|
| 153 |
+
baseUrl: preset.baseUrl,
|
| 154 |
+
apiFormat: preset.apiFormat,
|
| 155 |
+
runtimeKind: 'anthropic_compatible',
|
| 156 |
+
models: preset.defaultModels,
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
index.providers.push(provider)
|
| 160 |
+
index.activeId = provider.id
|
| 161 |
+
await this.writeIndex(index)
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
private async readIndex(): Promise<ProvidersIndex> {
|
| 165 |
await ensurePersistentStorageUpgraded()
|
| 166 |
return readRecoverableJsonFile({
|
|
|
|
| 204 |
// --- CRUD ---
|
| 205 |
|
| 206 |
async listProviders(): Promise<{ providers: SavedProvider[]; activeId: string | null }> {
|
| 207 |
+
// Auto-import TUI provider so desktop follows the TUI's provider choice
|
| 208 |
+
await this.autoImportTuiProvider()
|
| 209 |
const index = await this.readIndex()
|
| 210 |
return { providers: index.providers, activeId: index.activeId }
|
| 211 |
}
|
src/server/ws/handler.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
| 9 |
import type { ServerWebSocket } from 'bun'
|
| 10 |
import type { ClientMessage, ServerMessage } from './events.js'
|
| 11 |
import * as os from 'node:os'
|
|
|
|
|
|
|
| 12 |
import {
|
| 13 |
ConversationStartupError,
|
| 14 |
conversationService,
|
|
@@ -1743,7 +1745,36 @@ async function getRuntimeSettings(sessionId?: string): Promise<RuntimeSettings>
|
|
| 1743 |
}
|
| 1744 |
|
| 1745 |
async function getDefaultRuntimeSettings(): Promise<RuntimeSettings> {
|
| 1746 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1747 |
const { providers, activeId } = await providerService.listProviders()
|
| 1748 |
let resolvedActiveId = activeId
|
| 1749 |
if (activeId && !isKnownRuntimeProviderId(activeId, providers)) {
|
|
@@ -1803,6 +1834,30 @@ function resolveDesktopThinkingMode(
|
|
| 1803 |
return settings.alwaysThinkingEnabled === false ? 'disabled' : undefined
|
| 1804 |
}
|
| 1805 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1806 |
async function buildSessionStartupDiagnosticMessage(
|
| 1807 |
sessionId: string,
|
| 1808 |
cause: string,
|
|
|
|
| 9 |
import type { ServerWebSocket } from 'bun'
|
| 10 |
import type { ClientMessage, ServerMessage } from './events.js'
|
| 11 |
import * as os from 'node:os'
|
| 12 |
+
import * as fs from 'node:fs'
|
| 13 |
+
import * as path from 'node:path'
|
| 14 |
import {
|
| 15 |
ConversationStartupError,
|
| 16 |
conversationService,
|
|
|
|
| 1745 |
}
|
| 1746 |
|
| 1747 |
async function getDefaultRuntimeSettings(): Promise<RuntimeSettings> {
|
| 1748 |
+
// First check if CLI has an authProvider configured in ~/.claude.json
|
| 1749 |
+
// (opencode, nvidia, openrouter, local, etc.) - these take precedence
|
| 1750 |
+
// over cc-haha providers since they are managed by the CLI's /login command
|
| 1751 |
+
const cliConfig = await readCliGlobalConfig()
|
| 1752 |
+
if (cliConfig?.authProvider && cliConfig.authProvider !== 'anthropic' && cliConfig.authProvider !== 'openai') {
|
| 1753 |
+
const userSettings = await settingsService.getUserSettings()
|
| 1754 |
+
const baseModel =
|
| 1755 |
+
typeof userSettings.model === 'string' && userSettings.model.trim()
|
| 1756 |
+
? userSettings.model
|
| 1757 |
+
: undefined
|
| 1758 |
+
const modelContext =
|
| 1759 |
+
typeof userSettings.modelContext === 'string' && userSettings.modelContext.trim()
|
| 1760 |
+
? userSettings.modelContext
|
| 1761 |
+
: undefined
|
| 1762 |
+
const effort =
|
| 1763 |
+
typeof userSettings.effort === 'string' && userSettings.effort.trim()
|
| 1764 |
+
? userSettings.effort
|
| 1765 |
+
: undefined
|
| 1766 |
+
const thinking = resolveDesktopThinkingMode(userSettings)
|
| 1767 |
+
|
| 1768 |
+
return {
|
| 1769 |
+
permissionMode: await settingsService.getPermissionMode().catch(() => undefined),
|
| 1770 |
+
model: baseModel ? (modelContext ? `${baseModel}:${modelContext}` : baseModel) : undefined,
|
| 1771 |
+
effort,
|
| 1772 |
+
thinking,
|
| 1773 |
+
providerId: undefined, // CLI manages auth via ~/.claude.json - pass undefined so shouldMarkManagedOAuth checks cli config
|
| 1774 |
+
}
|
| 1775 |
+
}
|
| 1776 |
+
|
| 1777 |
+
// Check if a custom cc-haha provider is active
|
| 1778 |
const { providers, activeId } = await providerService.listProviders()
|
| 1779 |
let resolvedActiveId = activeId
|
| 1780 |
if (activeId && !isKnownRuntimeProviderId(activeId, providers)) {
|
|
|
|
| 1834 |
return settings.alwaysThinkingEnabled === false ? 'disabled' : undefined
|
| 1835 |
}
|
| 1836 |
|
| 1837 |
+
/**
|
| 1838 |
+
* Read CLI's ~/.claude.json directly to check authProvider.
|
| 1839 |
+
* Bypasses getGlobalConfig cache since this runs in the server process
|
| 1840 |
+
* and needs to reflect changes made by CLI's /login command.
|
| 1841 |
+
*/
|
| 1842 |
+
function readCliGlobalConfig(): {
|
| 1843 |
+
authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'
|
| 1844 |
+
openCodeApiKey?: string
|
| 1845 |
+
openCodeModelName?: string
|
| 1846 |
+
nvidiaApiKey?: string
|
| 1847 |
+
openRouterApiKey?: string
|
| 1848 |
+
localBaseUrl?: string
|
| 1849 |
+
localModelName?: string
|
| 1850 |
+
} | null {
|
| 1851 |
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
| 1852 |
+
const configPath = path.join(configDir, '.claude.json')
|
| 1853 |
+
try {
|
| 1854 |
+
const raw = fs.readFileSync(configPath, 'utf-8')
|
| 1855 |
+
return JSON.parse(raw)
|
| 1856 |
+
} catch {
|
| 1857 |
+
return null
|
| 1858 |
+
}
|
| 1859 |
+
}
|
| 1860 |
+
|
| 1861 |
async function buildSessionStartupDiagnosticMessage(
|
| 1862 |
sessionId: string,
|
| 1863 |
cause: string,
|