diff --git a/packages/desktop/src/components/chat/ProviderSettingsModal.tsx b/packages/desktop/src/components/chat/ProviderSettingsModal.tsx index d13cfa5..52f1631 100644 --- a/packages/desktop/src/components/chat/ProviderSettingsModal.tsx +++ b/packages/desktop/src/components/chat/ProviderSettingsModal.tsx @@ -1,4 +1,4 @@ -import { Check, Plus, RotateCcw, Trash2 } from 'lucide-react' +import { ArrowRight, Check, Plus, RotateCcw, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import type { ProviderInfo } from '../../lib/store.js' import { sessionStore } from '../../lib/store/sessionStore.js' @@ -15,15 +15,13 @@ export function ProviderSettingsModal({ provider, onClose }: Props) { const [keySaved, setKeySaved] = useState(false) const [models, setModels] = useState([]) const [newModel, setNewModel] = useState('') - const [modelsSaved, setModelsSaved] = useState(false) - const modelInputRef = useRef(null) + const addInputRef = useRef(null) useEffect(() => { if (provider) { setModels([...provider.models]) setApiKey('') setKeySaved(false) - setModelsSaved(false) setNewModel('') } }, [provider]) @@ -31,182 +29,124 @@ export function ProviderSettingsModal({ provider, onClose }: Props) { if (!provider) return null const icon = providerIcons[provider.name] - const providerLabel = provider.name.charAt(0).toUpperCase() + provider.name.slice(1) + const label = provider.name.charAt(0).toUpperCase() + provider.name.slice(1) + const connected = provider.hasApiKey || keySaved - const handleSaveKey = () => { + const commitModels = (next: string[]) => { + setModels(next) + sessionStore.getState().sendProviderSetModels(provider.name, next) + setTimeout(() => sessionStore.getState().sendProvidersList(), 300) + } + + const saveKey = (e: React.FormEvent) => { + e.preventDefault() const trimmed = apiKey.trim() if (!trimmed) return sessionStore.getState().sendProviderSetKey(provider.name, trimmed) setApiKey('') setKeySaved(true) - // Refresh providers to pick up new key status setTimeout(() => sessionStore.getState().sendProvidersList(), 300) - setTimeout(() => setKeySaved(false), 2000) + setTimeout(() => setKeySaved(false), 1800) } - const handleKeySubmit = (e: React.FormEvent) => { + const addModel = (e: React.FormEvent) => { e.preventDefault() - handleSaveKey() - } - - const handleRemoveModel = (index: number) => { - setModels((prev) => prev.filter((_, i) => i !== index)) - setModelsSaved(false) - } - - const handleAddModel = () => { const trimmed = newModel.trim() if (!trimmed || models.includes(trimmed)) return - setModels((prev) => [...prev, trimmed]) + commitModels([...models, trimmed]) setNewModel('') - setModelsSaved(false) - modelInputRef.current?.focus() + addInputRef.current?.focus() } - const handleAddModelSubmit = (e: React.FormEvent) => { - e.preventDefault() - handleAddModel() + const removeModel = (id: string) => { + commitModels(models.filter((m) => m !== id)) } - const handleResetModels = () => { + const resetDefaults = () => { const defaults = provider.defaultModels - if (defaults && defaults.length > 0) { - setModels([...defaults]) - setModelsSaved(false) - } - } - - const handleSaveModels = () => { - sessionStore.getState().sendProviderSetModels(provider.name, models) - setModelsSaved(true) - // Refresh providers so the dropdown picks up the new models - setTimeout(() => sessionStore.getState().sendProvidersList(), 300) - setTimeout(() => setModelsSaved(false), 2000) + if (defaults?.length) commitModels([...defaults]) } - const modelsChanged = - models.length !== provider.models.length || models.some((m, i) => m !== provider.models[i]) + const defaultsAvailable = (provider.defaultModels?.length ?? 0) > 0 + const isAtDefaults = + defaultsAvailable && + provider.defaultModels!.length === models.length && + provider.defaultModels!.every((m, i) => m === models[i]) return ( -
- {/* Title bar with icon */} -
-
- {icon ? ( - - ) : ( - - {provider.name.charAt(0).toUpperCase()} - - )} - {providerLabel} - {provider.baseUrl && ( - {provider.baseUrl} - )} -
- {(provider.hasApiKey || keySaved) && ( - - - Connected +
+
+ {icon ? ( + + ) : ( + + {provider.name.charAt(0).toUpperCase()} )} -
- - {/* API Key */} -
- -
- setApiKey(e.target.value)} - autoComplete="off" - spellCheck={false} - /> - -
-
- - {/* Models */} -
-
- Models - {provider.defaultModels && provider.defaultModels.length > 0 && ( - - )} -
- -
- {models.length === 0 && ( -
- No models. Add one below or click Defaults. -
+ {label} + {connected && } + + +
+ setApiKey(e.target.value)} + autoComplete="off" + spellCheck={false} + /> + -
- ))} - - + + + +
    + {models.map((m) => ( +
  • + {m} + +
  • + ))} +
  • + +
    setNewModel(e.target.value)} spellCheck={false} />
    -
-
- - {/* Footer */} - {modelsChanged && ( -
- -
+ + + + {defaultsAvailable && !isAtDefaults && ( + )}
diff --git a/packages/desktop/src/components/settings/SettingsModal.tsx b/packages/desktop/src/components/settings/SettingsModal.tsx index d66dda8..adfa765 100644 --- a/packages/desktop/src/components/settings/SettingsModal.tsx +++ b/packages/desktop/src/components/settings/SettingsModal.tsx @@ -767,16 +767,19 @@ function ProviderRow({ onOpen: (p: ProviderInfo) => void }) { const isHarness = provider.type === 'harness' + const comingSoon = provider.name === 'claude-code' const connected = provider.hasApiKey || provider.installed === true - const meta = isHarness - ? connected - ? 'CLI installed' - : 'Install to connect' - : connected - ? 'API key configured' - : 'Not connected' + const meta = comingSoon + ? 'Subscription support coming soon' + : isHarness + ? connected + ? 'CLI installed' + : 'Install to connect' + : connected + ? 'API key configured' + : 'Not connected' return ( -
+
@@ -784,7 +787,9 @@ function ProviderRow({
{provider.name}
{meta}
- {connected ? ( + {comingSoon ? ( + Coming soon + ) : connected ? ( Connected @@ -793,14 +798,16 @@ function ProviderRow({ Connect )} - + {!comingSoon && ( + + )}
) } diff --git a/packages/desktop/src/index.css b/packages/desktop/src/index.css index 0cf55bd..1efd850 100644 --- a/packages/desktop/src/index.css +++ b/packages/desktop/src/index.css @@ -6672,17 +6672,16 @@ button { } /* ═══════════════════════════════════════════════════════════════════ - PROVIDER SETTINGS MODAL + PROVIDER SETTINGS FORM ═══════════════════════════════════════════════════════════════════ */ +/* Shared title bar / key input (also used by HarnessSetupModal) */ + .prov-modal { display: flex; flex-direction: column; gap: 0; } - -/* ── Title bar ── */ - .prov-modal__titlebar { display: flex; align-items: center; @@ -6691,21 +6690,18 @@ button { margin-bottom: 16px; border-bottom: 1px solid var(--border); } - .prov-modal__titlebar-left { display: flex; align-items: center; gap: 8px; min-width: 0; } - .prov-modal__provider-icon { width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; } - .prov-modal__provider-icon-fallback { width: 20px; height: 20px; @@ -6719,13 +6715,11 @@ button { color: var(--text-subtle); flex-shrink: 0; } - .prov-modal__provider-name { font-size: 13px; font-weight: 600; color: var(--text); } - .prov-modal__provider-url { font-size: 11px; font-family: var(--font-mono); @@ -6734,7 +6728,6 @@ button { text-overflow: ellipsis; white-space: nowrap; } - .prov-modal__connected-badge { display: inline-flex; align-items: center; @@ -6744,48 +6737,12 @@ button { color: var(--success); flex-shrink: 0; } - .prov-modal__connected-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--success); } - -/* ── Sections ── */ - -.prov-modal__section { - margin-bottom: 16px; -} - -.prov-modal__field-label { - display: block; - font-size: 11px; - font-weight: 600; - color: var(--text-subtle); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 8px; -} - -.prov-modal__field-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -.prov-modal__field-header .prov-modal__field-label { - margin-bottom: 0; -} - -/* ── API Key ── */ - -.prov-modal__key-row { - display: flex; - gap: 8px; -} - .prov-modal__key-input { flex: 1; min-width: 0; @@ -6799,15 +6756,12 @@ button { outline: none; transition: border-color 0.15s ease; } - .prov-modal__key-input:focus { border-color: var(--text-subtle); } - .prov-modal__key-input::placeholder { color: var(--text-subtle); } - .prov-modal__key-btn { padding: 9px 14px; border-radius: 8px; @@ -6820,139 +6774,205 @@ button { white-space: nowrap; transition: all 0.12s ease; } - .prov-modal__key-btn:hover:not(:disabled) { background: rgba(var(--overlay), 0.1); color: var(--text); border-color: var(--border-strong); } - .prov-modal__key-btn:disabled { opacity: 0.35; cursor: default; } -.prov-modal__key-btn--saved { - display: inline-flex; - align-items: center; - gap: 4px; - color: var(--success); - border-color: rgba(34, 197, 94, 0.25); - background: var(--connection-bg); +.pform { + display: flex; + flex-direction: column; + gap: 18px; + min-width: 360px; } -/* ── Reset button ── */ - -.prov-modal__reset { +.pform__head { + display: flex; + align-items: center; + gap: 10px; +} +.pform__icon { + width: 22px; + height: 22px; + border-radius: 5px; + flex-shrink: 0; +} +.pform__icon--fallback { display: inline-flex; align-items: center; - gap: 4px; + justify-content: center; + background: rgba(var(--overlay), 0.08); font-size: 11px; + font-weight: 700; color: var(--text-subtle); - cursor: pointer; - transition: color 0.12s ease; } - -.prov-modal__reset:hover { +.pform__title { + font-size: 15px; + font-weight: 600; color: var(--text); + letter-spacing: -0.01em; +} +.pform__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--success) 18%, transparent); } -/* ── Models list ── */ +/* ── API key row ── */ -.prov-modal__models { +.pform__key { + position: relative; + display: flex; + align-items: center; +} +.pform__key-input { + flex: 1; + min-width: 0; + padding: 10px 40px 10px 12px; + border-radius: 9px; border: 1px solid var(--border); - border-radius: 10px; - overflow: hidden; background: var(--bg); + color: var(--text); + font-size: 13px; + font-family: var(--font-mono); + outline: none; + transition: border-color 0.15s ease; } - -.prov-modal__models-empty { - padding: 20px 12px; - text-align: center; - font-size: 12px; +.pform__key-input:focus { + border-color: var(--text-subtle); +} +.pform__key-input::placeholder { color: var(--text-subtle); - line-height: 1.5; } +.pform__key-btn { + position: absolute; + right: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 6px; + color: var(--text-subtle); + background: transparent; + cursor: pointer; + transition: all 0.12s ease; +} +.pform__key-btn:hover:not(:disabled) { + color: var(--text); + background: rgba(var(--overlay), 0.08); +} +.pform__key-btn:disabled { + opacity: 0.3; + cursor: default; +} + +/* ── Model list ── */ -.prov-modal__model-row { +.pform__list { + list-style: none; + margin: 0; + padding: 0; + border-radius: 10px; + background: rgba(var(--overlay), 0.025); + border: 1px solid var(--border); + overflow: hidden; +} +.pform__row { display: flex; align-items: center; - padding: 0 12px; height: 34px; - border-bottom: 1px solid rgba(var(--overlay), 0.04); - transition: background 0.08s ease; + padding: 0 8px 0 12px; + transition: background 0.1s ease; } - -.prov-modal__model-row:hover { - background: rgba(var(--overlay), 0.025); +.pform__row + .pform__row { + border-top: 1px solid rgba(var(--overlay), 0.05); } - -.prov-modal__model-id { +.pform__row:hover { + background: rgba(var(--overlay), 0.05); +} +.pform__row-id { flex: 1; min-width: 0; - font-size: 12px; + font-size: 12.5px; font-family: var(--font-mono); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - background: none; } - -.prov-modal__model-delete { - opacity: 0; +.pform__row-x { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 5px; color: var(--text-subtle); + background: transparent; cursor: pointer; - padding: 3px; - border-radius: 4px; + opacity: 0; transition: all 0.1s ease; - flex-shrink: 0; } - -.prov-modal__model-row:hover .prov-modal__model-delete { +.pform__row:hover .pform__row-x { opacity: 1; } - -.prov-modal__model-delete:hover { +.pform__row-x:hover { color: var(--danger); - background: rgba(239, 68, 68, 0.1); + background: rgba(239, 68, 68, 0.12); } - -/* ── Add model row ── */ - -.prov-modal__model-add-row { - display: flex; - align-items: center; - gap: 6px; - height: 34px; - padding: 0 12px; - border-top: 1px solid var(--border); +.pform__row--add { + color: var(--text-subtle); } - -.prov-modal__model-add-plus { +.pform__row--add:hover { + background: transparent; +} +.pform__row-plus { color: var(--text-subtle); + margin-right: 6px; flex-shrink: 0; } - -.prov-modal__model-add-input { +.pform__row-form { + flex: 1; + display: flex; +} +.pform__row-input { flex: 1; - min-width: 0; background: transparent; border: none; outline: none; - font-size: 12px; + font-size: 12.5px; font-family: var(--font-mono); - color: var(--text-muted); + color: var(--text); + padding: 0; } - -.prov-modal__model-add-input::placeholder { +.pform__row-input::placeholder { color: var(--text-subtle); } -/* ── Footer / save ── */ +/* ── Reset link ── */ -.prov-modal__footer { - padding-top: 12px; +.pform__reset { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-subtle); + cursor: pointer; + background: transparent; + transition: color 0.12s ease; + padding: 2px 0; +} +.pform__reset:hover { + color: var(--text); } /* ── Harness setup: steps list ── */ @@ -25649,6 +25669,13 @@ button { .sprov + .sprov { margin-top: 4px; } +.sprov[aria-disabled="true"] { + opacity: 0.55; + pointer-events: none; +} +.sprov[aria-disabled="true"] .stag { + pointer-events: auto; +} .sprov__av { width: 32px; height: 32px;