@bibobonking/opensol 0.0.0-stage → 3.0.1

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.
Files changed (114) hide show
  1. package/.github/workflows/build.yml +25 -0
  2. package/README.md +74 -2
  3. package/dist/assets/index-BmN7pBji.css +1 -0
  4. package/dist/assets/index-DgNDjkOz.js +301 -0
  5. package/dist/favicon.svg +15 -0
  6. package/dist/index.html +55 -0
  7. package/dist/preview-host.html +33 -0
  8. package/index.html +54 -0
  9. package/package.json +43 -4
  10. package/postcss.config.js +6 -0
  11. package/public/favicon.svg +15 -0
  12. package/public/preview-host.html +33 -0
  13. package/shared/plugin-builder.test.mjs +43 -0
  14. package/shared/plugin-package.d.mts +34 -0
  15. package/shared/plugin-package.mjs +197 -0
  16. package/shared/plugin-package.test.mjs +94 -0
  17. package/src/App.tsx +152 -0
  18. package/src/agent/adapters/anthropic.ts +233 -0
  19. package/src/agent/adapters/gemini.ts +225 -0
  20. package/src/agent/adapters/openai-responses.ts +215 -0
  21. package/src/agent/adapters/openai.ts +200 -0
  22. package/src/agent/adapters/shared.ts +96 -0
  23. package/src/agent/approval.ts +20 -0
  24. package/src/agent/compact.ts +234 -0
  25. package/src/agent/index.ts +36 -0
  26. package/src/agent/model.ts +20 -0
  27. package/src/agent/prompt.ts +107 -0
  28. package/src/agent/queue.test.mjs +111 -0
  29. package/src/agent/queue.ts +84 -0
  30. package/src/agent/subagent.ts +109 -0
  31. package/src/agent/tools.ts +541 -0
  32. package/src/components/ChatView.tsx +403 -0
  33. package/src/components/CommandPalette.tsx +416 -0
  34. package/src/components/Composer.tsx +678 -0
  35. package/src/components/ContextBar.tsx +58 -0
  36. package/src/components/DeployModal.tsx +512 -0
  37. package/src/components/ErrorBoundary.tsx +75 -0
  38. package/src/components/HomeDashboard.tsx +189 -0
  39. package/src/components/Logo.tsx +41 -0
  40. package/src/components/Markdown.tsx +232 -0
  41. package/src/components/MessageItem.tsx +655 -0
  42. package/src/components/ModelPicker.tsx +487 -0
  43. package/src/components/PixleSol.tsx +243 -0
  44. package/src/components/PluginBuilder.tsx +285 -0
  45. package/src/components/PluginManager.tsx +341 -0
  46. package/src/components/PreviewPane.tsx +166 -0
  47. package/src/components/SettingsModal.tsx +2052 -0
  48. package/src/components/Sidebar.tsx +504 -0
  49. package/src/components/Splash.tsx +104 -0
  50. package/src/components/ToolCallCard.tsx +177 -0
  51. package/src/components/TopBar.tsx +182 -0
  52. package/src/components/TrajectoryView.tsx +178 -0
  53. package/src/components/WorkspacePanel.tsx +710 -0
  54. package/src/components/ui.tsx +311 -0
  55. package/src/core/kernel.ts +45 -0
  56. package/src/core/plugins/tools/fs.ts +219 -0
  57. package/src/core/plugins/tools/git.ts +83 -0
  58. package/src/core/plugins/tools/grep.ts +98 -0
  59. package/src/core/plugins/tools/shell.ts +88 -0
  60. package/src/core/plugins/tools/subagent.ts +36 -0
  61. package/src/core/plugins/tools/web.ts +97 -0
  62. package/src/core/services/agent-loop.ts +417 -0
  63. package/src/core/services/approval.ts +66 -0
  64. package/src/core/services/compaction.ts +45 -0
  65. package/src/core/services/llm.ts +32 -0
  66. package/src/core/services/sessions.ts +47 -0
  67. package/src/core/services/subagents.ts +15 -0
  68. package/src/core/services/system-prompt.ts +177 -0
  69. package/src/core/services/tools.ts +135 -0
  70. package/src/core/services/ui.ts +109 -0
  71. package/src/core/types.ts +139 -0
  72. package/src/data/providers.ts +125 -0
  73. package/src/data/skills.ts +68 -0
  74. package/src/index.css +776 -0
  75. package/src/lib/boot.ts +94 -0
  76. package/src/lib/context.ts +75 -0
  77. package/src/lib/conversation.test.mjs +64 -0
  78. package/src/lib/conversation.ts +59 -0
  79. package/src/lib/daemon.ts +475 -0
  80. package/src/lib/hooks.ts +33 -0
  81. package/src/lib/permissions.ts +60 -0
  82. package/src/lib/pixlesol.test.mjs +51 -0
  83. package/src/lib/pixlesol.ts +55 -0
  84. package/src/lib/plugin-folder.test.mjs +145 -0
  85. package/src/lib/plugin-folder.ts +96 -0
  86. package/src/lib/reasoning.ts +87 -0
  87. package/src/lib/sse.ts +75 -0
  88. package/src/lib/themes.test.mjs +72 -0
  89. package/src/lib/themes.ts +52 -0
  90. package/src/lib/usage.test.mjs +72 -0
  91. package/src/lib/usage.ts +155 -0
  92. package/src/lib/utils.ts +73 -0
  93. package/src/main.tsx +16 -0
  94. package/src/plugins/api.test.mjs +77 -0
  95. package/src/plugins/api.ts +113 -0
  96. package/src/plugins/bridge.d.mts +1 -0
  97. package/src/plugins/bridge.mjs +26 -0
  98. package/src/plugins/bridge.test.mjs +17 -0
  99. package/src/plugins/index.ts +52 -0
  100. package/src/plugins/preview.test.mjs +18 -0
  101. package/src/plugins/preview.ts +34 -0
  102. package/src/plugins/runtime.ts +194 -0
  103. package/src/plugins/types.ts +40 -0
  104. package/src/store/chats.ts +278 -0
  105. package/src/store/models.ts +120 -0
  106. package/src/store/settings.ts +445 -0
  107. package/src/store/ui.ts +132 -0
  108. package/src/store/workspace.ts +299 -0
  109. package/src/types.ts +337 -0
  110. package/src/vite-env.d.ts +1 -0
  111. package/tailwind.config.js +83 -0
  112. package/tsconfig.json +22 -0
  113. package/tsconfig.tsbuildinfo +1 -0
  114. package/vite.config.ts +19 -0
@@ -0,0 +1,487 @@
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
+ }