@bibobonking/opensol 3.0.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build.yml +60 -8
- package/CHANGELOG.md +18 -0
- package/desktop/README.md +39 -0
- package/desktop/loading/index.html +36 -0
- package/desktop/src-tauri/Cargo.lock +4301 -0
- package/desktop/src-tauri/Cargo.toml +23 -0
- package/desktop/src-tauri/binaries/.gitkeep +0 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/capabilities/default.json +7 -0
- package/desktop/src-tauri/icons/128x128.png +0 -0
- package/desktop/src-tauri/icons/128x128@2x.png +0 -0
- package/desktop/src-tauri/icons/32x32.png +0 -0
- package/desktop/src-tauri/icons/64x64.png +0 -0
- package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
- package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
- package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
- package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
- package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
- package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
- package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
- package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
- package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
- package/desktop/src-tauri/icons/StoreLogo.png +0 -0
- package/desktop/src-tauri/icons/icon.icns +0 -0
- package/desktop/src-tauri/icons/icon.ico +0 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/lib.rs +210 -0
- package/desktop/src-tauri/src/main.rs +6 -0
- package/desktop/src-tauri/tauri.conf.json +48 -0
- package/dist/assets/index-DdI1_iPD.css +1 -0
- package/dist/assets/index-egG5Bgfo.js +312 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/server/daemon.mjs +64 -2
- package/server/lib/mcp.mjs +532 -0
- package/server/lib/mcp.test.mjs +46 -0
- package/server/lib/store.mjs +22 -0
- package/server/lib/tools.mjs +59 -1
- package/src/agent/tools.ts +6 -5
- package/src/components/Composer.tsx +22 -38
- package/src/components/DiffView.tsx +98 -0
- package/src/components/McpTab.tsx +337 -0
- package/src/components/MessageItem.tsx +62 -13
- package/src/components/ModelDeck.tsx +652 -0
- package/src/components/SessionsTab.tsx +206 -0
- package/src/components/SettingsModal.tsx +106 -69
- package/src/components/Sidebar.tsx +78 -24
- package/src/components/ToolCallCard.tsx +29 -7
- package/src/components/WorkspacePanel.tsx +16 -1
- package/src/core/kernel.ts +2 -0
- package/src/core/mcp-tools.ts +63 -0
- package/src/core/plugins/tools/fs.ts +24 -3
- package/src/core/services/agent-loop.ts +3 -1
- package/src/core/services/approval.ts +2 -1
- package/src/core/services/titles.ts +135 -0
- package/src/core/services/tools.ts +11 -8
- package/src/core/types.ts +4 -0
- package/src/index.css +97 -1
- package/src/lib/daemon.ts +29 -0
- package/src/lib/diff.ts +62 -0
- package/src/lib/permissions.ts +6 -6
- package/src/store/chats.ts +9 -1
- package/src/store/mcp.ts +67 -0
- package/src/store/settings.ts +9 -6
- package/src/store/ui.ts +13 -0
- package/src/store/workspace.ts +26 -0
- package/src/types.ts +51 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-BmN7pBji.css +0 -1
- package/dist/assets/index-DgNDjkOz.js +0 -301
- package/src/components/ModelPicker.tsx +0 -487
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type FormEvent } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import {
|
|
4
|
-
AlertCircle,
|
|
5
|
-
Brain,
|
|
6
|
-
Check,
|
|
7
|
-
ChevronDown,
|
|
8
|
-
Cpu,
|
|
9
|
-
KeyRound,
|
|
10
|
-
RefreshCw,
|
|
11
|
-
Search,
|
|
12
|
-
Settings2,
|
|
13
|
-
SlidersHorizontal,
|
|
14
|
-
} from 'lucide-react'
|
|
15
|
-
import { PROVIDERS, PROVIDER_MAP, effortLabel } from '../data/providers'
|
|
16
|
-
import type { Effort, ModelInfo, ProviderId } from '../types'
|
|
17
|
-
import {
|
|
18
|
-
activeModelId,
|
|
19
|
-
providerHasCredentials,
|
|
20
|
-
useSettings,
|
|
21
|
-
} from '../store/settings'
|
|
22
|
-
import { useModelCatalog } from '../store/models'
|
|
23
|
-
import { useUi } from '../store/ui'
|
|
24
|
-
import { useClickOutside } from '../lib/hooks'
|
|
25
|
-
import { defaultEffort, effortOptions, reasoningSummary } from '../lib/reasoning'
|
|
26
|
-
import { cn } from '../lib/utils'
|
|
27
|
-
import { Spinner } from './ui'
|
|
28
|
-
|
|
29
|
-
const NO_MODELS: ModelInfo[] = []
|
|
30
|
-
|
|
31
|
-
interface PickerPosition {
|
|
32
|
-
placement: 'top' | 'bottom'
|
|
33
|
-
top?: number
|
|
34
|
-
bottom?: number
|
|
35
|
-
left: number
|
|
36
|
-
width: number
|
|
37
|
-
maxHeight: number
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function connected(providerId: ProviderId) {
|
|
41
|
-
return providerHasCredentials(useSettings.getState(), providerId)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function contextLabel(context?: number) {
|
|
45
|
-
if (!context) return ''
|
|
46
|
-
if (context >= 1_000_000) return `${(context / 1_000_000).toFixed(context % 1_000_000 ? 1 : 0)}M`
|
|
47
|
-
return `${Math.round(context / 1000)}k`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function ModelPicker({
|
|
51
|
-
open,
|
|
52
|
-
onClose,
|
|
53
|
-
triggerRef,
|
|
54
|
-
}: {
|
|
55
|
-
open: boolean
|
|
56
|
-
onClose: () => void
|
|
57
|
-
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
58
|
-
}) {
|
|
59
|
-
const settings = useSettings()
|
|
60
|
-
const selectModel = useSettings((state) => state.selectModel)
|
|
61
|
-
const update = useSettings((state) => state.update)
|
|
62
|
-
const openSettings = useUi((state) => state.openSettings)
|
|
63
|
-
const panelRef = useRef<HTMLDivElement>(null)
|
|
64
|
-
const [query, setQuery] = useState('')
|
|
65
|
-
const [manual, setManual] = useState('')
|
|
66
|
-
const [showManual, setShowManual] = useState(false)
|
|
67
|
-
const [providerOpen, setProviderOpen] = useState(false)
|
|
68
|
-
|
|
69
|
-
const provider = settings.provider
|
|
70
|
-
const models = useModelCatalog((state) => state.byProvider[provider]) ?? NO_MODELS
|
|
71
|
-
const loading = useModelCatalog((state) => Boolean(state.loading[provider]))
|
|
72
|
-
const error = useModelCatalog((state) => state.errors[provider] ?? '')
|
|
73
|
-
const note = useModelCatalog((state) => state.notes[provider] ?? '')
|
|
74
|
-
const source = useModelCatalog((state) => state.source[provider] ?? '')
|
|
75
|
-
const fetchModels = useModelCatalog((state) => state.fetchModels)
|
|
76
|
-
|
|
77
|
-
useClickOutside(panelRef, onClose, open, triggerRef)
|
|
78
|
-
|
|
79
|
-
const [position, setPosition] = useState<PickerPosition | null>(null)
|
|
80
|
-
|
|
81
|
-
const updatePosition = useCallback(() => {
|
|
82
|
-
const trigger = triggerRef.current
|
|
83
|
-
if (!trigger) return
|
|
84
|
-
|
|
85
|
-
const rect = trigger.getBoundingClientRect()
|
|
86
|
-
const viewportWidth = window.innerWidth
|
|
87
|
-
const viewportHeight = window.innerHeight
|
|
88
|
-
|
|
89
|
-
const panelWidth = Math.min(viewportWidth - 24, 440)
|
|
90
|
-
|
|
91
|
-
let left = rect.left
|
|
92
|
-
if (left + panelWidth > viewportWidth - 12) {
|
|
93
|
-
left = Math.max(12, viewportWidth - panelWidth - 12)
|
|
94
|
-
}
|
|
95
|
-
if (left < 12) left = 12
|
|
96
|
-
|
|
97
|
-
const spaceAbove = rect.top - 16
|
|
98
|
-
const spaceBelow = viewportHeight - rect.bottom - 16
|
|
99
|
-
|
|
100
|
-
const openAbove = spaceAbove >= 320 || spaceAbove >= spaceBelow
|
|
101
|
-
|
|
102
|
-
if (openAbove) {
|
|
103
|
-
const maxHeight = Math.min(560, Math.max(220, spaceAbove))
|
|
104
|
-
setPosition({
|
|
105
|
-
placement: 'top',
|
|
106
|
-
bottom: viewportHeight - rect.top + 8,
|
|
107
|
-
left,
|
|
108
|
-
width: panelWidth,
|
|
109
|
-
maxHeight,
|
|
110
|
-
})
|
|
111
|
-
} else {
|
|
112
|
-
const maxHeight = Math.min(560, Math.max(220, spaceBelow))
|
|
113
|
-
setPosition({
|
|
114
|
-
placement: 'bottom',
|
|
115
|
-
top: rect.bottom + 8,
|
|
116
|
-
left,
|
|
117
|
-
width: panelWidth,
|
|
118
|
-
maxHeight,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
}, [triggerRef])
|
|
122
|
-
|
|
123
|
-
useLayoutEffect(() => {
|
|
124
|
-
if (!open) {
|
|
125
|
-
setPosition(null)
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
updatePosition()
|
|
129
|
-
window.addEventListener('resize', updatePosition)
|
|
130
|
-
window.addEventListener('scroll', updatePosition, true)
|
|
131
|
-
return () => {
|
|
132
|
-
window.removeEventListener('resize', updatePosition)
|
|
133
|
-
window.removeEventListener('scroll', updatePosition, true)
|
|
134
|
-
}
|
|
135
|
-
}, [open, updatePosition])
|
|
136
|
-
|
|
137
|
-
const modelId = activeModelId(settings)
|
|
138
|
-
const active = models.find((model) => model.id === modelId) ?? null
|
|
139
|
-
const connectedNow = connected(provider)
|
|
140
|
-
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
if (!open) return
|
|
143
|
-
if (connectedNow || provider === 'custom' || PROVIDER_MAP[provider]?.keyless) {
|
|
144
|
-
void fetchModels(provider)
|
|
145
|
-
}
|
|
146
|
-
}, [open, provider, connectedNow, fetchModels])
|
|
147
|
-
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (!open) {
|
|
150
|
-
setProviderOpen(false)
|
|
151
|
-
setShowManual(false)
|
|
152
|
-
setQuery('')
|
|
153
|
-
}
|
|
154
|
-
}, [open])
|
|
155
|
-
|
|
156
|
-
const filtered = useMemo(() => {
|
|
157
|
-
const clean = query.trim().toLowerCase()
|
|
158
|
-
if (!clean) return models
|
|
159
|
-
return models.filter(
|
|
160
|
-
(model) => model.name.toLowerCase().includes(clean) || model.id.toLowerCase().includes(clean),
|
|
161
|
-
)
|
|
162
|
-
}, [models, query])
|
|
163
|
-
|
|
164
|
-
const efforts = effortOptions(active)
|
|
165
|
-
|
|
166
|
-
const choose = (model: ModelInfo) => {
|
|
167
|
-
const effort = efforts.length > 0 ? defaultEffort(model) : settings.effort
|
|
168
|
-
selectModel(provider, model.id, effort)
|
|
169
|
-
onClose()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const chooseManual = (event?: FormEvent) => {
|
|
173
|
-
event?.preventDefault()
|
|
174
|
-
const id = manual.trim()
|
|
175
|
-
if (!id) return
|
|
176
|
-
selectModel(provider, id, settings.effort)
|
|
177
|
-
setManual('')
|
|
178
|
-
setShowManual(false)
|
|
179
|
-
onClose()
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const chooseProvider = (next: ProviderId) => {
|
|
183
|
-
update({ provider: next, model: '' })
|
|
184
|
-
setProviderOpen(false)
|
|
185
|
-
setQuery('')
|
|
186
|
-
void fetchModels(next)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!open || !position) return null
|
|
190
|
-
|
|
191
|
-
return createPortal(
|
|
192
|
-
<div
|
|
193
|
-
ref={panelRef}
|
|
194
|
-
style={{
|
|
195
|
-
position: 'fixed',
|
|
196
|
-
left: `${position.left}px`,
|
|
197
|
-
width: `${position.width}px`,
|
|
198
|
-
maxHeight: `${position.maxHeight}px`,
|
|
199
|
-
...(position.placement === 'top'
|
|
200
|
-
? { bottom: `${position.bottom}px` }
|
|
201
|
-
: { top: `${position.top}px` }),
|
|
202
|
-
}}
|
|
203
|
-
className="z-50 flex flex-col overflow-hidden rounded-2xl border border-line surface-lift shadow-float"
|
|
204
|
-
role="dialog"
|
|
205
|
-
aria-label="Choose a model"
|
|
206
|
-
>
|
|
207
|
-
<div className="flex shrink-0 items-center gap-2 border-b border-line px-3 py-2.5">
|
|
208
|
-
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-accent-soft text-accent">
|
|
209
|
-
<Cpu size={14} />
|
|
210
|
-
</span>
|
|
211
|
-
<div className="min-w-0 flex-1">
|
|
212
|
-
<p className="text-[12px] font-semibold text-ink">Model</p>
|
|
213
|
-
<p className="text-[9.5px] text-faint">
|
|
214
|
-
{models.length > 0 ? `${models.length} live models` : 'Load models from this provider'}
|
|
215
|
-
{source === 'catalog' ? ' · catalog fallback' : ''}
|
|
216
|
-
</p>
|
|
217
|
-
</div>
|
|
218
|
-
<button
|
|
219
|
-
type="button"
|
|
220
|
-
className="icon-button h-7 w-7"
|
|
221
|
-
onClick={() => void fetchModels(provider, true)}
|
|
222
|
-
disabled={loading}
|
|
223
|
-
title="Refresh live models"
|
|
224
|
-
aria-label="Refresh live models"
|
|
225
|
-
>
|
|
226
|
-
<RefreshCw size={12} className={cn(loading && 'animate-spin')} />
|
|
227
|
-
</button>
|
|
228
|
-
<button
|
|
229
|
-
type="button"
|
|
230
|
-
className="icon-button h-7 w-7"
|
|
231
|
-
onClick={() => {
|
|
232
|
-
onClose()
|
|
233
|
-
openSettings('models')
|
|
234
|
-
}}
|
|
235
|
-
title="Provider settings"
|
|
236
|
-
aria-label="Provider settings"
|
|
237
|
-
>
|
|
238
|
-
<Settings2 size={13} />
|
|
239
|
-
</button>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
<div className="shrink-0 border-b border-line">
|
|
243
|
-
<button
|
|
244
|
-
type="button"
|
|
245
|
-
onClick={() => setProviderOpen((value) => !value)}
|
|
246
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-left"
|
|
247
|
-
aria-expanded={providerOpen}
|
|
248
|
-
>
|
|
249
|
-
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-faint">Provider</span>
|
|
250
|
-
<span className="text-[11.5px] font-medium text-ink">{PROVIDER_MAP[provider]?.name ?? provider}</span>
|
|
251
|
-
{connectedNow ? (
|
|
252
|
-
<span className="flex items-center gap-1 text-[9.5px] text-success">
|
|
253
|
-
<span className="h-1.5 w-1.5 rounded-full bg-success" /> connected
|
|
254
|
-
</span>
|
|
255
|
-
) : (
|
|
256
|
-
<span className="flex items-center gap-1 text-[9.5px] text-warning">
|
|
257
|
-
<KeyRound size={9} /> key needed
|
|
258
|
-
</span>
|
|
259
|
-
)}
|
|
260
|
-
<ChevronDown
|
|
261
|
-
size={12}
|
|
262
|
-
className={cn('ml-auto text-faint transition-transform', providerOpen && 'rotate-180')}
|
|
263
|
-
/>
|
|
264
|
-
</button>
|
|
265
|
-
{providerOpen && (
|
|
266
|
-
<div className="thin-scroll max-h-44 overflow-y-auto border-t border-line/70 px-1.5 py-1.5">
|
|
267
|
-
{PROVIDERS.map((entry) => {
|
|
268
|
-
const ready = connected(entry.id) || entry.keyless || entry.id === 'custom'
|
|
269
|
-
return (
|
|
270
|
-
<button
|
|
271
|
-
key={entry.id}
|
|
272
|
-
type="button"
|
|
273
|
-
onClick={() => chooseProvider(entry.id)}
|
|
274
|
-
className={cn(
|
|
275
|
-
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[11px] transition-colors',
|
|
276
|
-
entry.id === provider ? 'bg-accent-soft text-accent' : 'text-muted hover:bg-raised hover:text-ink',
|
|
277
|
-
)}
|
|
278
|
-
>
|
|
279
|
-
<span className="min-w-0 flex-1 truncate">{entry.name}</span>
|
|
280
|
-
{!ready && <span className="text-[9px] text-warning">key</span>}
|
|
281
|
-
{entry.id === provider && <Check size={11} />}
|
|
282
|
-
</button>
|
|
283
|
-
)
|
|
284
|
-
})}
|
|
285
|
-
</div>
|
|
286
|
-
)}
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
{!connectedNow && provider !== 'custom' && !PROVIDER_MAP[provider]?.keyless ? (
|
|
290
|
-
<div className="flex flex-col items-center gap-2 px-6 py-8 text-center">
|
|
291
|
-
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-warning/10 text-warning">
|
|
292
|
-
<KeyRound size={16} />
|
|
293
|
-
</span>
|
|
294
|
-
<p className="text-[12px] font-medium text-ink">Connect {PROVIDER_MAP[provider]?.name ?? provider}</p>
|
|
295
|
-
<p className="max-w-[280px] text-[10.5px] leading-relaxed text-muted">
|
|
296
|
-
Keys are encrypted by the local daemon before they are written to disk. They never enter this page after
|
|
297
|
-
saving.
|
|
298
|
-
</p>
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
onClick={() => {
|
|
302
|
-
onClose()
|
|
303
|
-
openSettings('models')
|
|
304
|
-
}}
|
|
305
|
-
className="mt-1 inline-flex h-7 items-center gap-1.5 rounded-lg bg-ink px-3 text-[11px] font-medium text-panel"
|
|
306
|
-
>
|
|
307
|
-
Add API key
|
|
308
|
-
</button>
|
|
309
|
-
</div>
|
|
310
|
-
) : (
|
|
311
|
-
<>
|
|
312
|
-
<div className="shrink-0 px-3 py-2">
|
|
313
|
-
<div className="relative">
|
|
314
|
-
<Search size={11} className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-faint" />
|
|
315
|
-
<input
|
|
316
|
-
value={query}
|
|
317
|
-
onChange={(event) => setQuery(event.target.value)}
|
|
318
|
-
placeholder="Search models"
|
|
319
|
-
className="h-7 w-full rounded-md border border-transparent bg-raised/60 pl-7 pr-2 text-[10.5px] text-ink outline-none focus:border-line focus:bg-panel"
|
|
320
|
-
aria-label="Search models"
|
|
321
|
-
/>
|
|
322
|
-
</div>
|
|
323
|
-
{loading && models.length === 0 && (
|
|
324
|
-
<p className="flex items-center gap-1.5 px-1 pt-2 text-[10.5px] text-faint">
|
|
325
|
-
<Spinner size={11} /> Loading live models…
|
|
326
|
-
</p>
|
|
327
|
-
)}
|
|
328
|
-
{error && (
|
|
329
|
-
<div className="mt-2 flex items-start gap-1.5 rounded-md border border-danger/20 bg-danger/[0.04] px-2 py-1.5">
|
|
330
|
-
<AlertCircle size={11} className="mt-0.5 shrink-0 text-danger" />
|
|
331
|
-
<p className="min-w-0 flex-1 text-[10px] leading-relaxed text-danger">{error}</p>
|
|
332
|
-
<button type="button" onClick={() => void fetchModels(provider, true)} className="text-[10px] font-medium text-danger">
|
|
333
|
-
Retry
|
|
334
|
-
</button>
|
|
335
|
-
</div>
|
|
336
|
-
)}
|
|
337
|
-
{note && !error && <p className="mt-1.5 px-1 text-[9.5px] leading-relaxed text-faint">{note}</p>}
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<div className="thin-scroll min-h-0 flex-1 overflow-y-auto px-1.5 pb-2">
|
|
341
|
-
{filtered.map((model) => {
|
|
342
|
-
const selected = model.id === modelId
|
|
343
|
-
return (
|
|
344
|
-
<button
|
|
345
|
-
key={model.id}
|
|
346
|
-
type="button"
|
|
347
|
-
onClick={() => choose(model)}
|
|
348
|
-
className={cn(
|
|
349
|
-
'flex w-full items-start gap-2 rounded-lg px-2 py-2 text-left transition-colors',
|
|
350
|
-
selected ? 'bg-accent-soft' : 'hover:bg-raised',
|
|
351
|
-
)}
|
|
352
|
-
>
|
|
353
|
-
<span
|
|
354
|
-
className={cn(
|
|
355
|
-
'mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md',
|
|
356
|
-
selected ? 'bg-accent text-on-accent' : 'bg-raised text-faint',
|
|
357
|
-
)}
|
|
358
|
-
>
|
|
359
|
-
{selected ? <Check size={11} /> : <Cpu size={10} />}
|
|
360
|
-
</span>
|
|
361
|
-
<span className="min-w-0 flex-1">
|
|
362
|
-
<span className="flex items-center gap-1.5">
|
|
363
|
-
<span
|
|
364
|
-
className={cn(
|
|
365
|
-
'truncate text-[11.5px] font-medium',
|
|
366
|
-
selected ? 'text-accent' : 'text-ink',
|
|
367
|
-
)}
|
|
368
|
-
>
|
|
369
|
-
{model.name || model.id}
|
|
370
|
-
</span>
|
|
371
|
-
{model.free && (
|
|
372
|
-
<span className="rounded-full border border-success/25 px-1.5 py-px text-[8.5px] font-medium text-success">
|
|
373
|
-
free
|
|
374
|
-
</span>
|
|
375
|
-
)}
|
|
376
|
-
</span>
|
|
377
|
-
<span className="mt-0.5 flex flex-wrap items-center gap-1.5 text-[9px] text-faint">
|
|
378
|
-
<span className="truncate font-mono">{model.id}</span>
|
|
379
|
-
{model.context ? <span>{contextLabel(model.context)} ctx</span> : null}
|
|
380
|
-
{model.reasoningOptions.length > 0 ? (
|
|
381
|
-
<span className="inline-flex items-center gap-0.5 text-accent/80">
|
|
382
|
-
<Brain size={8} /> reasoning
|
|
383
|
-
</span>
|
|
384
|
-
) : null}
|
|
385
|
-
{model.vision ? <span>vision</span> : null}
|
|
386
|
-
{!model.toolCall ? <span className="text-warning">no tools</span> : null}
|
|
387
|
-
</span>
|
|
388
|
-
</span>
|
|
389
|
-
</button>
|
|
390
|
-
)
|
|
391
|
-
})}
|
|
392
|
-
{!loading && filtered.length === 0 && models.length > 0 && (
|
|
393
|
-
<p className="px-2 py-4 text-center text-[10.5px] text-faint">No model matches “{query}”.</p>
|
|
394
|
-
)}
|
|
395
|
-
{!loading && models.length === 0 && !error && (
|
|
396
|
-
<div className="px-2 py-4 text-center">
|
|
397
|
-
<p className="text-[10.5px] text-faint">This provider returned no usable chat models.</p>
|
|
398
|
-
<button
|
|
399
|
-
type="button"
|
|
400
|
-
onClick={() => setShowManual(true)}
|
|
401
|
-
className="mt-1.5 text-[10.5px] font-medium text-accent hover:underline"
|
|
402
|
-
>
|
|
403
|
-
Enter a model ID manually
|
|
404
|
-
</button>
|
|
405
|
-
</div>
|
|
406
|
-
)}
|
|
407
|
-
</div>
|
|
408
|
-
|
|
409
|
-
<div className="shrink-0 border-t border-line px-3 py-2.5">
|
|
410
|
-
{active ? (
|
|
411
|
-
<>
|
|
412
|
-
<div className="flex items-center justify-between gap-2">
|
|
413
|
-
<span className="flex items-center gap-1.5 text-[10.5px] font-medium text-muted">
|
|
414
|
-
<Brain size={11} />
|
|
415
|
-
Effort
|
|
416
|
-
</span>
|
|
417
|
-
{efforts.length > 0 ? (
|
|
418
|
-
<select
|
|
419
|
-
value={settings.effort}
|
|
420
|
-
onChange={(event) => {
|
|
421
|
-
selectModel(provider, active.id, event.target.value as Effort)
|
|
422
|
-
}}
|
|
423
|
-
className="h-6 rounded-md border border-line bg-panel px-1.5 text-[10px] text-ink outline-none"
|
|
424
|
-
aria-label="Reasoning effort"
|
|
425
|
-
>
|
|
426
|
-
{efforts.map((effort) => (
|
|
427
|
-
<option key={effort} value={effort}>
|
|
428
|
-
{effortLabel(effort)}
|
|
429
|
-
</option>
|
|
430
|
-
))}
|
|
431
|
-
</select>
|
|
432
|
-
) : null}
|
|
433
|
-
</div>
|
|
434
|
-
<p className="mt-1 text-[9.5px] leading-relaxed text-faint">{reasoningSummary(active)}</p>
|
|
435
|
-
<div className="mt-2 flex flex-wrap items-center gap-2 text-[9px] text-faint">
|
|
436
|
-
{active.maxOutput ? <span>max output {contextLabel(active.maxOutput)}</span> : null}
|
|
437
|
-
{active.family ? <span>{active.family}</span> : null}
|
|
438
|
-
<span className="rounded border border-line px-1 py-px font-mono">{active.apiKind}</span>
|
|
439
|
-
</div>
|
|
440
|
-
</>
|
|
441
|
-
) : (
|
|
442
|
-
<div className="flex items-center justify-between gap-2">
|
|
443
|
-
<p className="text-[10px] text-faint">Select a model to see live reasoning controls.</p>
|
|
444
|
-
<button
|
|
445
|
-
type="button"
|
|
446
|
-
onClick={() => setShowManual((value) => !value)}
|
|
447
|
-
className="inline-flex items-center gap-1 text-[10px] font-medium text-accent"
|
|
448
|
-
>
|
|
449
|
-
<SlidersHorizontal size={10} /> ID
|
|
450
|
-
</button>
|
|
451
|
-
</div>
|
|
452
|
-
)}
|
|
453
|
-
|
|
454
|
-
{(showManual || (active && modelId !== active.id)) && (
|
|
455
|
-
<form onSubmit={chooseManual} className="mt-2 flex items-center gap-1.5">
|
|
456
|
-
<input
|
|
457
|
-
value={manual}
|
|
458
|
-
onChange={(event) => setManual(event.target.value)}
|
|
459
|
-
placeholder="model-id"
|
|
460
|
-
className="h-7 min-w-0 flex-1 rounded-md border border-line bg-panel px-2 font-mono text-[10.5px] outline-none"
|
|
461
|
-
aria-label="Model ID"
|
|
462
|
-
/>
|
|
463
|
-
<button
|
|
464
|
-
type="submit"
|
|
465
|
-
disabled={!manual.trim()}
|
|
466
|
-
className="inline-flex h-7 items-center rounded-md bg-ink px-2.5 text-[10px] font-medium text-panel disabled:opacity-40"
|
|
467
|
-
>
|
|
468
|
-
Use
|
|
469
|
-
</button>
|
|
470
|
-
</form>
|
|
471
|
-
)}
|
|
472
|
-
{active && (
|
|
473
|
-
<button
|
|
474
|
-
type="button"
|
|
475
|
-
onClick={() => setShowManual((value) => !value)}
|
|
476
|
-
className="mt-1.5 text-[9.5px] font-medium text-faint hover:text-ink"
|
|
477
|
-
>
|
|
478
|
-
{showManual ? 'Hide manual ID' : 'Use a model ID that is not listed'}
|
|
479
|
-
</button>
|
|
480
|
-
)}
|
|
481
|
-
</div>
|
|
482
|
-
</>
|
|
483
|
-
)}
|
|
484
|
-
</div>,
|
|
485
|
-
document.body,
|
|
486
|
-
)
|
|
487
|
-
}
|