@bibobonking/opensol 3.1.1 → 3.1.3

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 (42) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/dist/assets/index-DaJcH9ts.css +1 -0
  3. package/dist/assets/index-hs0KWMP6.js +288 -0
  4. package/dist/favicon.svg +11 -5
  5. package/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/public/favicon.svg +11 -5
  8. package/server/daemon.mjs +44 -5
  9. package/server/lib/deploy.mjs +78 -7
  10. package/server/lib/skills.mjs +128 -0
  11. package/server/lib/tools.mjs +40 -0
  12. package/src/agent/adapters/openai-responses.ts +5 -1
  13. package/src/agent/adapters/openai.ts +15 -2
  14. package/src/agent/prompt.ts +4 -7
  15. package/src/agent/tools.ts +1 -0
  16. package/src/components/DeployModal.tsx +77 -15
  17. package/src/components/FileIcon.tsx +108 -0
  18. package/src/components/HomeDashboard.tsx +9 -2
  19. package/src/components/Logo.tsx +11 -5
  20. package/src/components/ModelDeck.tsx +12 -2
  21. package/src/components/SettingsModal.tsx +144 -199
  22. package/src/components/Sidebar.tsx +4 -5
  23. package/src/components/SystemPromptModal.tsx +168 -0
  24. package/src/components/TopBar.tsx +19 -0
  25. package/src/components/WorkspacePanel.tsx +10 -8
  26. package/src/core/kernel.ts +2 -0
  27. package/src/core/plugins/tools/skill.ts +101 -0
  28. package/src/core/services/agent-loop.ts +10 -22
  29. package/src/core/services/system-prompt.ts +15 -13
  30. package/src/core/services/tools.ts +1 -0
  31. package/src/lib/daemon.ts +19 -0
  32. package/src/lib/permissions.ts +1 -1
  33. package/src/lib/skills.test.mjs +22 -0
  34. package/src/lib/skills.ts +93 -0
  35. package/src/lib/usage.ts +12 -0
  36. package/src/store/chats.ts +1 -0
  37. package/src/store/settings.ts +21 -9
  38. package/src/types.ts +14 -1
  39. package/tsconfig.tsbuildinfo +1 -1
  40. package/dist/assets/index-CeN54A2N.css +0 -1
  41. package/dist/assets/index-fhIO9a7_.js +0 -318
  42. package/src/data/skills.ts +0 -68
@@ -9,12 +9,14 @@ import {
9
9
  Play,
10
10
  Rocket,
11
11
  ShieldCheck,
12
+ Sparkles,
12
13
  Square,
13
14
  Triangle,
14
15
  Terminal,
16
+ X,
15
17
  } from 'lucide-react'
16
18
  import type { DeployProviderId, DeployResult } from '../types'
17
- import { streamDeploy } from '../lib/daemon'
19
+ import { inspectDeploy, streamDeploy, type DeployInspection } from '../lib/daemon'
18
20
  import { cn } from '../lib/utils'
19
21
  import { useSettings } from '../store/settings'
20
22
  import { useUi } from '../store/ui'
@@ -74,9 +76,11 @@ export function DeployModal() {
74
76
 
75
77
  const [provider, setProvider] = useState<DeployProviderId>('cloudflare')
76
78
  const [path, setPath] = useState('')
77
- const [outputDir, setOutputDir] = useState('dist')
79
+ const [outputDir, setOutputDir] = useState('')
78
80
  const [buildCommand, setBuildCommand] = useState('')
79
81
  const [projectName, setProjectName] = useState('')
82
+ const [inspection, setInspection] = useState<DeployInspection | null>(null)
83
+ const [inspecting, setInspecting] = useState(false)
80
84
  const [running, setRunning] = useState(false)
81
85
  const [logs, setLogs] = useState<LogLine[]>([])
82
86
  const [result, setResult] = useState<DeployResult | null>(null)
@@ -89,10 +93,15 @@ export function DeployModal() {
89
93
  const meta = PROVIDERS.find((entry) => entry.id === provider) ?? PROVIDERS[0]
90
94
  const tool = deployTools?.[provider]
91
95
  const configured = Boolean(deploySecrets?.[provider]?.configured)
96
+ const lastUrl = deploySecrets?.[provider]?.fields?.lastUrl ?? ''
92
97
 
93
98
  useEffect(() => {
94
99
  if (!open) return
95
100
  setPath(workspaceRoot)
101
+ setOutputDir('')
102
+ setBuildCommand('')
103
+ setProjectName('')
104
+ setInspection(null)
96
105
  setResult(null)
97
106
  setError('')
98
107
  setLogs([])
@@ -100,6 +109,30 @@ export function DeployModal() {
100
109
  setCopied(false)
101
110
  }, [open, workspaceRoot])
102
111
 
112
+ const detect = async (target = path) => {
113
+ if (!target.trim()) return
114
+ setInspecting(true)
115
+ try {
116
+ const info = await inspectDeploy(target.trim())
117
+ setInspection(info)
118
+ if (info.buildCommand) setBuildCommand((value) => value || info.buildCommand)
119
+ if (info.outputDir) setOutputDir((value) => value || info.outputDir)
120
+ if (info.name) setProjectName((value) => value || info.name)
121
+ } catch (cause) {
122
+ setInspection(null)
123
+ setError(cause instanceof Error ? cause.message : 'Could not inspect that folder.')
124
+ } finally {
125
+ setInspecting(false)
126
+ }
127
+ }
128
+
129
+ // Fill the form from the real project once the path settles.
130
+ useEffect(() => {
131
+ if (!open || !path.trim()) return
132
+ const timer = window.setTimeout(() => void detect(path), 450)
133
+ return () => window.clearTimeout(timer)
134
+ }, [open, path])
135
+
103
136
  useEffect(() => {
104
137
  if (!open) return
105
138
  const node = logRef.current
@@ -201,7 +234,7 @@ export function DeployModal() {
201
234
  </div>
202
235
  </div>
203
236
  <button type="button" className="icon-button -mr-1 shrink-0" onClick={close} aria-label="Close deploy panel">
204
- <span className="text-[18px] leading-none">×</span>
237
+ <X size={16} />
205
238
  </button>
206
239
  </div>
207
240
 
@@ -221,16 +254,16 @@ export function DeployModal() {
221
254
  onClick={() => setProvider(entry.id)}
222
255
  disabled={running}
223
256
  className={cn(
224
- 'flex items-start gap-2.5 rounded-xl border px-3 py-2.5 text-left transition-colors disabled:opacity-60',
257
+ 'group/host flex items-start gap-2.5 rounded-xl border px-3 py-2.5 text-left transition-all duration-200 disabled:opacity-60',
225
258
  active
226
- ? 'border-accent/35 bg-accent-soft/45'
227
- : 'border-line bg-panel hover:bg-raised/50',
259
+ ? 'border-accent/40 bg-gradient-to-br from-accent-soft/80 to-panel shadow-pill'
260
+ : 'border-line bg-panel hover:border-line hover:bg-raised/50',
228
261
  )}
229
262
  >
230
263
  <span
231
264
  className={cn(
232
- 'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg',
233
- active ? 'bg-accent text-on-accent' : 'bg-raised text-muted',
265
+ 'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors',
266
+ active ? 'bg-accent text-on-accent shadow-pill' : 'bg-raised text-muted group-hover/host:text-ink',
234
267
  )}
235
268
  >
236
269
  <Icon size={13} />
@@ -254,6 +287,18 @@ export function DeployModal() {
254
287
  </div>
255
288
  </div>
256
289
 
290
+ {lastUrl && (
291
+ <a
292
+ href={lastUrl}
293
+ target="_blank"
294
+ rel="noreferrer"
295
+ className="flex items-center gap-1.5 rounded-lg border border-line bg-raised/30 px-2.5 py-1.5 text-[9.5px] text-muted hover:text-ink"
296
+ >
297
+ <ExternalLink size={10} className="shrink-0 text-faint" />
298
+ <span className="truncate">Last deploy: {lastUrl}</span>
299
+ </a>
300
+ )}
301
+
257
302
  {!configured && (
258
303
  <div className="rounded-xl border border-warning/25 bg-warning/[0.045] p-3">
259
304
  <p className="flex items-start gap-1.5 text-[10.5px] leading-relaxed text-muted">
@@ -300,10 +345,27 @@ export function DeployModal() {
300
345
  spellCheck={false}
301
346
  className="h-9 w-full rounded-lg border border-line bg-panel px-2.5 font-mono text-[10.5px] text-ink outline-none transition-colors placeholder:text-faint focus:border-accent/50 disabled:opacity-60"
302
347
  />
303
- <p className="mt-1 text-[9px] leading-relaxed text-faint">
304
- {workspaceAvailable ? `Open workspace: ${workspaceName}` : 'No workspace open.'} Another folder must
305
- already exist on this machine.
306
- </p>
348
+ <div className="mt-1.5 flex items-center gap-2">
349
+ <p className="min-w-0 flex-1 truncate text-[9px] leading-relaxed text-faint">
350
+ {workspaceAvailable ? `Open workspace: ${workspaceName}` : 'No workspace open.'} Any folder that already exists on this machine.
351
+ </p>
352
+ <button
353
+ type="button"
354
+ onClick={() => void detect()}
355
+ disabled={running || inspecting || !path.trim()}
356
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-line bg-panel px-1.5 text-[9px] font-medium text-muted hover:text-ink disabled:opacity-40"
357
+ >
358
+ {inspecting ? <Spinner size={9} /> : <Sparkles size={9} />}
359
+ Detect
360
+ </button>
361
+ </div>
362
+ {inspection && (
363
+ <div className="mt-2 flex flex-wrap items-center gap-1.5 rounded-lg border border-line bg-raised/40 px-2 py-1.5 text-[9px] text-muted">
364
+ <span className="rounded bg-panel px-1.5 py-0.5 font-medium text-ink">{inspection.framework}</span>
365
+ {inspection.existingOutputs.length > 0 && <span>build output: {inspection.existingOutputs.join(', ')}</span>}
366
+ {!inspection.hasPackageJson && <span>static folder, no package.json</span>}
367
+ </div>
368
+ )}
307
369
  </div>
308
370
 
309
371
  <div className="grid grid-cols-2 gap-2.5">
@@ -316,7 +378,7 @@ export function DeployModal() {
316
378
  value={outputDir}
317
379
  onChange={(event) => setOutputDir(event.target.value)}
318
380
  disabled={running}
319
- placeholder="dist"
381
+ placeholder="auto"
320
382
  spellCheck={false}
321
383
  className="h-8 w-full rounded-lg border border-line bg-panel px-2 font-mono text-[10.5px] text-ink outline-none focus:border-accent/50 disabled:opacity-60"
322
384
  />
@@ -338,13 +400,13 @@ export function DeployModal() {
338
400
  </div>
339
401
 
340
402
  <div className="flex flex-wrap gap-1">
341
- {['dist', 'build', 'out', '.'].map((value) => (
403
+ {['auto', 'dist', 'build', 'out', '.'].map((value) => (
342
404
  <button
343
405
  key={value}
344
406
  type="button"
345
407
  onClick={() => setOutputDir(value)}
346
408
  disabled={running}
347
- className={cn('chip h-7 px-2 text-[9.5px]', outputDir === value && 'chip-active')}
409
+ className={cn('chip h-7 px-2 text-[9.5px]', (outputDir || 'auto') === value && 'chip-active')}
348
410
  >
349
411
  {value}
350
412
  </button>
@@ -0,0 +1,108 @@
1
+ import type { LucideIcon } from 'lucide-react'
2
+ import {
3
+ Braces,
4
+ FileArchive,
5
+ FileAudio,
6
+ FileCode2,
7
+ FileCog,
8
+ FileImage,
9
+ FileKey2,
10
+ FileLock2,
11
+ FileText,
12
+ FileVideo,
13
+ Folder,
14
+ FolderOpen,
15
+ Palette,
16
+ Terminal,
17
+ } from 'lucide-react'
18
+ import { cn } from '../lib/utils'
19
+
20
+ const TYPES: Record<string, { icon: LucideIcon; tone: string }> = {
21
+ ts: { icon: FileCode2, tone: 'text-sky-500' },
22
+ tsx: { icon: FileCode2, tone: 'text-sky-500' },
23
+ js: { icon: FileCode2, tone: 'text-amber-400' },
24
+ jsx: { icon: FileCode2, tone: 'text-amber-400' },
25
+ mjs: { icon: FileCode2, tone: 'text-amber-400' },
26
+ cjs: { icon: FileCode2, tone: 'text-amber-400' },
27
+ rs: { icon: FileCode2, tone: 'text-orange-400' },
28
+ go: { icon: FileCode2, tone: 'text-cyan-400' },
29
+ py: { icon: FileCode2, tone: 'text-emerald-400' },
30
+ rb: { icon: FileCode2, tone: 'text-rose-400' },
31
+ java: { icon: FileCode2, tone: 'text-red-400' },
32
+ css: { icon: Palette, tone: 'text-fuchsia-400' },
33
+ scss: { icon: Palette, tone: 'text-pink-400' },
34
+ sass: { icon: Palette, tone: 'text-pink-400' },
35
+ less: { icon: Palette, tone: 'text-pink-400' },
36
+ html: { icon: FileCode2, tone: 'text-orange-500' },
37
+ vue: { icon: FileCode2, tone: 'text-emerald-500' },
38
+ svelte: { icon: FileCode2, tone: 'text-orange-500' },
39
+ json: { icon: Braces, tone: 'text-yellow-500' },
40
+ jsonc: { icon: Braces, tone: 'text-yellow-500' },
41
+ yaml: { icon: FileCog, tone: 'text-teal-400' },
42
+ yml: { icon: FileCog, tone: 'text-teal-400' },
43
+ toml: { icon: FileCog, tone: 'text-teal-400' },
44
+ ini: { icon: FileCog, tone: 'text-teal-400' },
45
+ env: { icon: FileKey2, tone: 'text-amber-500' },
46
+ md: { icon: FileText, tone: 'text-violet-400' },
47
+ mdx: { icon: FileText, tone: 'text-violet-400' },
48
+ txt: { icon: FileText, tone: 'text-slate-400' },
49
+ csv: { icon: FileText, tone: 'text-emerald-400' },
50
+ svg: { icon: FileImage, tone: 'text-purple-400' },
51
+ png: { icon: FileImage, tone: 'text-purple-400' },
52
+ jpg: { icon: FileImage, tone: 'text-purple-400' },
53
+ jpeg: { icon: FileImage, tone: 'text-purple-400' },
54
+ gif: { icon: FileImage, tone: 'text-purple-400' },
55
+ webp: { icon: FileImage, tone: 'text-purple-400' },
56
+ ico: { icon: FileImage, tone: 'text-purple-400' },
57
+ mp3: { icon: FileAudio, tone: 'text-rose-400' },
58
+ wav: { icon: FileAudio, tone: 'text-rose-400' },
59
+ mp4: { icon: FileVideo, tone: 'text-rose-400' },
60
+ mov: { icon: FileVideo, tone: 'text-rose-400' },
61
+ zip: { icon: FileArchive, tone: 'text-amber-500' },
62
+ gz: { icon: FileArchive, tone: 'text-amber-500' },
63
+ tar: { icon: FileArchive, tone: 'text-amber-500' },
64
+ lock: { icon: FileLock2, tone: 'text-slate-400' },
65
+ sh: { icon: Terminal, tone: 'text-emerald-400' },
66
+ bash: { icon: Terminal, tone: 'text-emerald-400' },
67
+ zsh: { icon: Terminal, tone: 'text-emerald-400' },
68
+ sql: { icon: FileText, tone: 'text-cyan-400' },
69
+ }
70
+
71
+ const SPECIAL: Array<{ match: RegExp; icon: LucideIcon; tone: string }> = [
72
+ { match: /^(dockerfile|makefile|justfile)$/i, icon: FileCog, tone: 'text-sky-400' },
73
+ { match: /^\.env/i, icon: FileKey2, tone: 'text-amber-500' },
74
+ { match: /^(\.gitignore|\.gitattributes|\.npmignore|\.dockerignore)$/i, icon: FileCog, tone: 'text-slate-400' },
75
+ { match: /^package(-lock)?\.json$/i, icon: Braces, tone: 'text-emerald-400' },
76
+ { match: /^(vite|webpack|rollup|tsconfig|tailwind|postcss)\./i, icon: FileCog, tone: 'text-sky-400' },
77
+ { match: /^readme/i, icon: FileText, tone: 'text-violet-400' },
78
+ { match: /^skill\.md$/i, icon: FileText, tone: 'text-accent' },
79
+ ]
80
+
81
+ export function fileTone(path: string): { icon: LucideIcon; tone: string } {
82
+ const name = path.split('/').pop() ?? path
83
+ const special = SPECIAL.find((entry) => entry.match.test(name))
84
+ if (special) return { icon: special.icon, tone: special.tone }
85
+ const extension = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase() ?? ''
86
+ return TYPES[extension] ?? { icon: FileText, tone: 'text-faint' }
87
+ }
88
+
89
+ export function FileIcon({
90
+ path,
91
+ type = 'file',
92
+ open = false,
93
+ size = 13,
94
+ className,
95
+ }: {
96
+ path: string
97
+ type?: 'file' | 'dir' | 'link'
98
+ open?: boolean
99
+ size?: number
100
+ className?: string
101
+ }) {
102
+ if (type === 'dir') {
103
+ const Icon = open ? FolderOpen : Folder
104
+ return <Icon size={size} className={cn('shrink-0 fill-amber-400/25 text-amber-500', className)} />
105
+ }
106
+ const { icon: Icon, tone } = fileTone(path)
107
+ return <Icon size={size} className={cn('shrink-0', tone, className)} />
108
+ }
@@ -1,10 +1,10 @@
1
1
  import { useMemo, useState } from 'react'
2
- import { Activity, CalendarDays, Clock3, Cpu, MessageSquare, Sparkles } from 'lucide-react'
2
+ import { Activity, CalendarDays, Clock3, Cpu, CreditCard, MessageSquare, Sparkles } from 'lucide-react'
3
3
  import { useChats } from '../store/chats'
4
4
  import { useSettings } from '../store/settings'
5
5
  import { useWorkspace } from '../store/workspace'
6
6
  import { cn } from '../lib/utils'
7
- import { computeUsage, formatTokens, shortModelName, type UsageRange } from '../lib/usage'
7
+ import { computeUsage, formatCredits, formatTokens, shortModelName, type UsageRange } from '../lib/usage'
8
8
  import { PixleSol } from './PixleSol'
9
9
 
10
10
  const RANGES: Array<{ value: UsageRange; label: string }> = [
@@ -118,6 +118,7 @@ export function HomeDashboard() {
118
118
  <Stat icon={Activity} label="Sessions" value={String(stats.sessions)} />
119
119
  <Stat icon={MessageSquare} label="Messages" value={String(stats.messages)} />
120
120
  <Stat icon={Sparkles} label="Total tokens" value={stats.tokens ? formatTokens(stats.tokens) : '0'} />
121
+ <Stat icon={CreditCard} label="Credits spent" value={formatCredits(stats.credits)} />
121
122
  <Stat icon={CalendarDays} label="Active days" value={String(stats.activeDays)} />
122
123
  <Stat icon={Clock3} label="Peak hour" value={stats.peakHour} />
123
124
  <Stat icon={Cpu} label="Favorite model" value={shortModelName(stats.favoriteModel)} />
@@ -143,6 +144,12 @@ export function HomeDashboard() {
143
144
  {stats.messages === 0
144
145
  ? 'No activity in this range yet. Send a message to start the graph.'
145
146
  : `You've used ~${formatTokens(stats.tokens)} tokens across ${stats.messages} messages.`}
147
+ {stats.credits > 0 && (
148
+ <>
149
+ <span className="px-1.5 text-line">·</span>
150
+ {formatCredits(stats.credits)} in provider credits
151
+ </>
152
+ )}
146
153
  {stats.favoriteModel !== '—' && (
147
154
  <>
148
155
  <span className="px-1.5 text-line">·</span>
@@ -1,6 +1,6 @@
1
1
  import { cn } from '../lib/utils'
2
2
 
3
- /** The OpenSol sun: a solid core with eight round-capped rays. Inherits `currentColor`. */
3
+ /** The OpenSol aperture: a broken solar ring around a solid core. Inherits `currentColor`. */
4
4
  export function LogoMark({ size = 28, className, spin = false }: { size?: number; className?: string; spin?: boolean }) {
5
5
  return (
6
6
  <span
@@ -9,10 +9,16 @@ export function LogoMark({ size = 28, className, spin = false }: { size?: number
9
9
  aria-hidden="true"
10
10
  >
11
11
  <svg viewBox="0 0 32 32" width={size} height={size} fill="none" className={cn(spin && 'animate-spin-slow')}>
12
- <circle cx="16" cy="16" r="6.2" fill="currentColor" />
13
- <g stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
14
- <path d="M16 3.6v3.2M16 25.2v3.2M3.6 16h3.2M25.2 16h3.2" />
15
- <path d="M7.2 7.2l2.2 2.2M22.6 22.6l2.2 2.2M7.2 24.8l2.2-2.2M22.6 9.4l2.2-2.2" opacity="0.55" />
12
+ <circle cx="16" cy="16" r="5.1" fill="currentColor" />
13
+ <circle cx="16" cy="16" r="8.4" stroke="currentColor" strokeWidth="0.9" opacity="0.22" />
14
+ <g stroke="currentColor" strokeWidth="2.15" strokeLinecap="round">
15
+ <path d="M16 4.4a11.6 11.6 0 0 1 9.7 5.5" />
16
+ <path d="M27.6 16a11.6 11.6 0 0 1-5.5 9.7" opacity="0.6" />
17
+ <path d="M16 27.6a11.6 11.6 0 0 1-9.7-5.5" opacity="0.32" />
18
+ <path d="M4.4 16a11.6 11.6 0 0 1 5.5-9.7" opacity="0.8" />
19
+ </g>
20
+ <g stroke="currentColor" strokeWidth="1.7" strokeLinecap="round">
21
+ <path d="M16 1.5v2.2M30.5 16h-2.2M16 30.5v-2.2M1.5 16h2.2" opacity="0.9" />
16
22
  </g>
17
23
  </svg>
18
24
  </span>
@@ -178,6 +178,8 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
178
178
  const [manual, setManual] = useState('')
179
179
  const [showManual, setShowManual] = useState(false)
180
180
  const [cursor, setCursor] = useState(0)
181
+ /** Only keyboard moves scroll the list; scrolling on hover fights the pointer and jitters. */
182
+ const keyboardCursor = useRef(true)
181
183
  const listRef = useRef<HTMLDivElement>(null)
182
184
  const searchRef = useRef<HTMLInputElement>(null)
183
185
 
@@ -226,11 +228,13 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
226
228
  }, [models, query])
227
229
 
228
230
  useLayoutEffect(() => {
231
+ keyboardCursor.current = true
229
232
  setCursor(0)
230
233
  }, [query, provider])
231
234
 
232
235
  useEffect(() => {
233
- listRef.current?.querySelector<HTMLElement>(`[data-row="${cursor}"]`)?.scrollIntoView({ block: 'center' })
236
+ if (!keyboardCursor.current) return
237
+ listRef.current?.querySelector<HTMLElement>(`[data-row="${cursor}"]`)?.scrollIntoView({ block: 'nearest' })
234
238
  }, [cursor])
235
239
 
236
240
  const choose = (model: ModelInfo) => {
@@ -258,9 +262,11 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
258
262
  const onSearchKey = (event: KeyboardEvent<HTMLInputElement>) => {
259
263
  if (event.key === 'ArrowDown') {
260
264
  event.preventDefault()
265
+ keyboardCursor.current = true
261
266
  setCursor((value) => Math.min(filtered.length - 1, value + 1))
262
267
  } else if (event.key === 'ArrowUp') {
263
268
  event.preventDefault()
269
+ keyboardCursor.current = true
264
270
  setCursor((value) => Math.max(0, value - 1))
265
271
  } else if (event.key === 'Enter') {
266
272
  event.preventDefault()
@@ -472,7 +478,11 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
472
478
  setCursor(i)
473
479
  choose(model)
474
480
  }}
475
- onMouseMove={() => highlighted || setCursor(i)}
481
+ onMouseEnter={() => {
482
+ if (highlighted) return
483
+ keyboardCursor.current = false
484
+ setCursor(i)
485
+ }}
476
486
  style={{ animationDelay: `${Math.min(i * 22, 260)}ms` }}
477
487
  className={cn(
478
488
  'deck-row group/row relative mb-1 flex w-full items-center gap-3 overflow-hidden rounded-2xl px-3 py-2.5 text-left transition-colors',