@bibobonking/opensol 3.1.4 → 3.1.5

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 (72) hide show
  1. package/CHANGELOG.md +10 -12
  2. package/desktop/src-tauri/icons/128x128.png +0 -0
  3. package/desktop/src-tauri/icons/128x128@2x.png +0 -0
  4. package/desktop/src-tauri/icons/32x32.png +0 -0
  5. package/desktop/src-tauri/icons/64x64.png +0 -0
  6. package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
  7. package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
  8. package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
  9. package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
  10. package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
  11. package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
  12. package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
  13. package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
  14. package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
  15. package/desktop/src-tauri/icons/StoreLogo.png +0 -0
  16. package/desktop/src-tauri/icons/icon.icns +0 -0
  17. package/desktop/src-tauri/icons/icon.ico +0 -0
  18. package/desktop/src-tauri/icons/icon.png +0 -0
  19. package/dist/assets/index-C0yvs6Vl.js +281 -0
  20. package/dist/assets/index-CxNPLBg7.css +1 -0
  21. package/dist/favicon.svg +18 -15
  22. package/dist/index.html +2 -2
  23. package/package.json +1 -1
  24. package/public/favicon.svg +18 -15
  25. package/server/daemon.mjs +66 -49
  26. package/server/lib/deploy.mjs +222 -121
  27. package/server/lib/providers.mjs +23 -1
  28. package/server/lib/skills.mjs +385 -100
  29. package/server/lib/tools.mjs +0 -40
  30. package/src/agent/adapters/openai-responses.ts +1 -5
  31. package/src/agent/adapters/openai.ts +4 -11
  32. package/src/agent/prompt.ts +7 -14
  33. package/src/agent/subagent.ts +3 -14
  34. package/src/agent/tools.ts +2 -0
  35. package/src/components/ChatView.tsx +2 -1
  36. package/src/components/Composer.tsx +2 -1
  37. package/src/components/ContextBar.tsx +10 -0
  38. package/src/components/DeployModal.tsx +266 -252
  39. package/src/components/FileIcon.tsx +391 -99
  40. package/src/components/HomeDashboard.tsx +5 -10
  41. package/src/components/Logo.tsx +9 -11
  42. package/src/components/ModelDeck.tsx +8 -11
  43. package/src/components/SettingsModal.tsx +160 -122
  44. package/src/components/Sidebar.tsx +4 -3
  45. package/src/components/SystemPromptButton.tsx +198 -0
  46. package/src/components/ToolCallCard.tsx +2 -0
  47. package/src/components/TopBar.tsx +2 -19
  48. package/src/components/WorkspacePanel.tsx +76 -93
  49. package/src/core/plugins/tools/skill.ts +19 -76
  50. package/src/core/services/agent-loop.ts +16 -31
  51. package/src/core/services/subagents.ts +2 -2
  52. package/src/core/services/system-prompt.ts +16 -23
  53. package/src/core/services/tools.ts +0 -1
  54. package/src/lib/boot.ts +2 -0
  55. package/src/lib/credits.ts +33 -0
  56. package/src/lib/daemon.ts +40 -19
  57. package/src/lib/permissions.ts +1 -1
  58. package/src/lib/retry.test.mjs +40 -12
  59. package/src/lib/retry.ts +41 -8
  60. package/src/lib/system-prompt.ts +23 -0
  61. package/src/lib/usage.ts +8 -13
  62. package/src/store/chats.ts +0 -1
  63. package/src/store/settings.ts +12 -44
  64. package/src/store/skills.ts +79 -0
  65. package/src/store/ui.ts +3 -1
  66. package/src/types.ts +30 -15
  67. package/tsconfig.tsbuildinfo +1 -1
  68. package/dist/assets/index-B1pQC9Fe.js +0 -288
  69. package/dist/assets/index-qLr4aZVC.css +0 -1
  70. package/src/components/SystemPromptModal.tsx +0 -168
  71. package/src/lib/skills.test.mjs +0 -22
  72. package/src/lib/skills.ts +0 -93
@@ -1,108 +1,400 @@
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'
1
+ import { useId } from 'react'
18
2
  import { cn } from '../lib/utils'
19
3
 
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' },
4
+ /**
5
+ * Coloured file and folder icons. Colours are fixed per file type (like an editor's icon theme),
6
+ * so they read the same in every app theme.
7
+ */
8
+
9
+ interface FileKind {
10
+ color: string
11
+ /** Short mark printed on the badge. */
12
+ label?: string
13
+ /** A drawn glyph instead of a text label. */
14
+ glyph?: 'image' | 'video' | 'audio' | 'archive' | 'lock' | 'gear' | 'terminal' | 'font' | 'db' | 'text' | 'braces' | 'tag'
15
+ }
16
+
17
+ const EXT: Record<string, FileKind> = {
18
+ ts: { color: '#3178c6', label: 'TS' },
19
+ tsx: { color: '#2d9fd8', label: 'TSX' },
20
+ js: { color: '#f0c808', label: 'JS' },
21
+ mjs: { color: '#f0c808', label: 'MJS' },
22
+ cjs: { color: '#f0c808', label: 'CJS' },
23
+ jsx: { color: '#e6b800', label: 'JSX' },
24
+ json: { color: '#f59e0b', glyph: 'braces' },
25
+ jsonc: { color: '#f59e0b', glyph: 'braces' },
26
+ json5: { color: '#f59e0b', glyph: 'braces' },
27
+ html: { color: '#e5532d', label: 'HTML' },
28
+ htm: { color: '#e5532d', label: 'HTML' },
29
+ css: { color: '#3b82f6', label: 'CSS' },
30
+ scss: { color: '#cf649a', label: 'SCSS' },
31
+ sass: { color: '#cf649a', label: 'SASS' },
32
+ less: { color: '#2b4c80', label: 'LESS' },
33
+ md: { color: '#4f8fd8', label: 'MD' },
34
+ mdx: { color: '#f9ac00', label: 'MDX' },
35
+ py: { color: '#3b78b0', label: 'PY' },
36
+ rb: { color: '#cc342d', label: 'RB' },
37
+ go: { color: '#00add8', label: 'GO' },
38
+ rs: { color: '#d36f3b', label: 'RS' },
39
+ java: { color: '#e76f00', label: 'JAVA' },
40
+ kt: { color: '#a97bff', label: 'KT' },
41
+ swift: { color: '#f05138', label: 'SWIFT' },
42
+ c: { color: '#5c8dbc', label: 'C' },
43
+ h: { color: '#8a63d2', label: 'H' },
44
+ cpp: { color: '#3d7fc0', label: 'C++' },
45
+ cs: { color: '#7a4fd0', label: 'C#' },
46
+ php: { color: '#777bb3', label: 'PHP' },
47
+ lua: { color: '#2c2d72', label: 'LUA' },
48
+ dart: { color: '#2bb7f6', label: 'DART' },
49
+ vue: { color: '#41b883', label: 'VUE' },
50
+ svelte: { color: '#ff3e00', label: 'SVLT' },
51
+ astro: { color: '#ff5d01', label: 'ASTRO' },
52
+ sql: { color: '#d4a017', label: 'SQL' },
53
+ graphql: { color: '#e535ab', label: 'GQL' },
54
+ gql: { color: '#e535ab', label: 'GQL' },
55
+ sh: { color: '#4caf50', glyph: 'terminal' },
56
+ bash: { color: '#4caf50', glyph: 'terminal' },
57
+ zsh: { color: '#4caf50', glyph: 'terminal' },
58
+ ps1: { color: '#4a7bd0', glyph: 'terminal' },
59
+ bat: { color: '#4caf50', glyph: 'terminal' },
60
+ yml: { color: '#cb4b4b', label: 'YML' },
61
+ yaml: { color: '#cb4b4b', label: 'YML' },
62
+ toml: { color: '#9c4221', label: 'TOML' },
63
+ xml: { color: '#e37933', label: 'XML' },
64
+ svg: { color: '#ffb13b', glyph: 'image' },
65
+ png: { color: '#26a69a', glyph: 'image' },
66
+ jpg: { color: '#26a69a', glyph: 'image' },
67
+ jpeg: { color: '#26a69a', glyph: 'image' },
68
+ gif: { color: '#26a69a', glyph: 'image' },
69
+ webp: { color: '#26a69a', glyph: 'image' },
70
+ avif: { color: '#26a69a', glyph: 'image' },
71
+ ico: { color: '#26a69a', glyph: 'image' },
72
+ bmp: { color: '#26a69a', glyph: 'image' },
73
+ mp4: { color: '#e05d8c', glyph: 'video' },
74
+ mov: { color: '#e05d8c', glyph: 'video' },
75
+ webm: { color: '#e05d8c', glyph: 'video' },
76
+ mkv: { color: '#e05d8c', glyph: 'video' },
77
+ mp3: { color: '#a855f7', glyph: 'audio' },
78
+ wav: { color: '#a855f7', glyph: 'audio' },
79
+ ogg: { color: '#a855f7', glyph: 'audio' },
80
+ flac: { color: '#a855f7', glyph: 'audio' },
81
+ zip: { color: '#b8860b', glyph: 'archive' },
82
+ tar: { color: '#b8860b', glyph: 'archive' },
83
+ gz: { color: '#b8860b', glyph: 'archive' },
84
+ tgz: { color: '#b8860b', glyph: 'archive' },
85
+ rar: { color: '#b8860b', glyph: 'archive' },
86
+ '7z': { color: '#b8860b', glyph: 'archive' },
87
+ pdf: { color: '#e5484d', label: 'PDF' },
88
+ doc: { color: '#2b6cb0', label: 'DOC' },
89
+ docx: { color: '#2b6cb0', label: 'DOC' },
90
+ xls: { color: '#1f8a4c', label: 'XLS' },
91
+ xlsx: { color: '#1f8a4c', label: 'XLS' },
92
+ csv: { color: '#1f8a4c', label: 'CSV' },
93
+ ppt: { color: '#d35230', label: 'PPT' },
94
+ pptx: { color: '#d35230', label: 'PPT' },
95
+ txt: { color: '#8b95a5', glyph: 'text' },
96
+ log: { color: '#8b95a5', glyph: 'text' },
97
+ env: { color: '#d4b106', glyph: 'gear' },
98
+ ini: { color: '#8b95a5', glyph: 'gear' },
99
+ conf: { color: '#8b95a5', glyph: 'gear' },
100
+ lock: { color: '#8b95a5', glyph: 'lock' },
101
+ ttf: { color: '#c2410c', glyph: 'font' },
102
+ otf: { color: '#c2410c', glyph: 'font' },
103
+ woff: { color: '#c2410c', glyph: 'font' },
104
+ woff2: { color: '#c2410c', glyph: 'font' },
105
+ db: { color: '#6b7fd7', glyph: 'db' },
106
+ sqlite: { color: '#6b7fd7', glyph: 'db' },
107
+ wasm: { color: '#7c4dff', label: 'WASM' },
108
+ ipynb: { color: '#f37726', label: 'IPYNB' },
109
+ }
110
+
111
+ const NAMES: Record<string, FileKind> = {
112
+ 'package.json': { color: '#8bc34a', glyph: 'braces' },
113
+ 'package-lock.json': { color: '#8b95a5', glyph: 'lock' },
114
+ 'tsconfig.json': { color: '#3178c6', glyph: 'gear' },
115
+ 'jsconfig.json': { color: '#f0c808', glyph: 'gear' },
116
+ dockerfile: { color: '#2496ed', label: 'DKR' },
117
+ 'docker-compose.yml': { color: '#2496ed', label: 'DKR' },
118
+ 'docker-compose.yaml': { color: '#2496ed', label: 'DKR' },
119
+ makefile: { color: '#6d8086', glyph: 'terminal' },
120
+ '.gitignore': { color: '#f05133', glyph: 'tag' },
121
+ '.gitattributes': { color: '#f05133', glyph: 'tag' },
122
+ '.npmignore': { color: '#cb3837', glyph: 'tag' },
123
+ '.npmrc': { color: '#cb3837', glyph: 'gear' },
124
+ '.env': { color: '#d4b106', glyph: 'gear' },
125
+ '.editorconfig': { color: '#8b95a5', glyph: 'gear' },
126
+ '.prettierrc': { color: '#56b3b4', glyph: 'gear' },
127
+ '.eslintrc': { color: '#7c7ce0', glyph: 'gear' },
128
+ 'vite.config.ts': { color: '#a259ff', glyph: 'gear' },
129
+ 'vite.config.js': { color: '#a259ff', glyph: 'gear' },
130
+ 'tailwind.config.js': { color: '#38bdf8', glyph: 'gear' },
131
+ 'tailwind.config.ts': { color: '#38bdf8', glyph: 'gear' },
132
+ 'readme.md': { color: '#42a5f5', label: 'MD' },
133
+ 'license': { color: '#d4b106', glyph: 'text' },
134
+ 'changelog.md': { color: '#42a5f5', label: 'MD' },
135
+ 'claude.md': { color: '#d97757', label: 'MD' },
136
+ 'agents.md': { color: '#d97757', label: 'MD' },
137
+ }
138
+
139
+ const DEFAULT_FILE: FileKind = { color: '#8b95a5', glyph: 'text' }
140
+
141
+ export function fileKind(name: string): FileKind {
142
+ const lower = name.toLowerCase()
143
+ if (NAMES[lower]) return NAMES[lower]
144
+ if (lower.startsWith('.env')) return NAMES['.env']
145
+ if (/\.(test|spec)\.[a-z]+$/.test(lower)) {
146
+ const base = EXT[lower.split('.').pop() ?? '']
147
+ return { color: '#26b36b', label: base?.label ? `${base.label.slice(0, 2)}·T` : 'TEST' }
148
+ }
149
+ if (/\.d\.m?ts$/.test(lower)) return { color: '#3178c6', label: 'D.TS' }
150
+ const dot = lower.lastIndexOf('.')
151
+ if (dot > 0) return EXT[lower.slice(dot + 1)] ?? DEFAULT_FILE
152
+ return DEFAULT_FILE
69
153
  }
70
154
 
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
- ]
155
+ function Glyph({ kind, color }: { kind: NonNullable<FileKind['glyph']>; color: string }) {
156
+ const common = { stroke: color, strokeWidth: 1.25, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' }
157
+ switch (kind) {
158
+ case 'image':
159
+ return (
160
+ <g {...common}>
161
+ <circle cx="6.3" cy="8.3" r="0.9" fill={color} stroke="none" />
162
+ <path d="M3.8 12.4l2.7-2.7 1.6 1.6 1.8-2.3 2.3 3.4z" fill={color} fillOpacity="0.35" />
163
+ </g>
164
+ )
165
+ case 'video':
166
+ return <path d="M6 7.4v4l3.6-2z" fill={color} stroke={color} strokeWidth="0.8" strokeLinejoin="round" />
167
+ case 'audio':
168
+ return (
169
+ <g {...common}>
170
+ <path d="M7.6 12V7.2l3-.8v4.6" />
171
+ <circle cx="6.5" cy="12" r="1.1" fill={color} />
172
+ <circle cx="9.5" cy="11" r="1.1" fill={color} />
173
+ </g>
174
+ )
175
+ case 'archive':
176
+ return (
177
+ <g {...common}>
178
+ <path d="M8 3.5v9" strokeDasharray="1.2 1.2" />
179
+ <rect x="6.8" y="9.6" width="2.4" height="2.4" rx="0.5" fill={color} fillOpacity="0.4" />
180
+ </g>
181
+ )
182
+ case 'lock':
183
+ return (
184
+ <g {...common}>
185
+ <rect x="5.4" y="8.6" width="5.2" height="3.8" rx="0.9" fill={color} fillOpacity="0.3" />
186
+ <path d="M6.4 8.6V7.4a1.6 1.6 0 013.2 0v1.2" />
187
+ </g>
188
+ )
189
+ case 'gear':
190
+ return (
191
+ <g {...common}>
192
+ <circle cx="8" cy="9.6" r="1.4" />
193
+ <path d="M8 6.6v.9M8 11.7v.9M5 9.6h.9M10.1 9.6H11M5.9 7.5l.6.6M9.5 11.1l.6.6M5.9 11.7l.6-.6M9.5 8.1l.6-.6" />
194
+ </g>
195
+ )
196
+ case 'terminal':
197
+ return (
198
+ <g {...common}>
199
+ <path d="M5.2 7.6l2 1.8-2 1.8" />
200
+ <path d="M8.6 12h2.4" />
201
+ </g>
202
+ )
203
+ case 'font':
204
+ return <path d="M5.2 12.4L8 6.4l2.8 6M6.1 10.6h3.8" {...common} />
205
+ case 'db':
206
+ return (
207
+ <g {...common}>
208
+ <ellipse cx="8" cy="7.2" rx="2.7" ry="1.1" />
209
+ <path d="M5.3 7.2v4.4c0 .6 1.2 1.1 2.7 1.1s2.7-.5 2.7-1.1V7.2M5.3 9.4c0 .6 1.2 1.1 2.7 1.1s2.7-.5 2.7-1.1" />
210
+ </g>
211
+ )
212
+ case 'braces':
213
+ return (
214
+ <g {...common}>
215
+ <path d="M6.8 6.6c-1 0-1.3.5-1.3 1.2v.8c0 .6-.4.9-.9 1 .5.1.9.4.9 1v.8c0 .7.3 1.2 1.3 1.2" />
216
+ <path d="M9.2 6.6c1 0 1.3.5 1.3 1.2v.8c0 .6.4.9.9 1-.5.1-.9.4-.9 1v.8c0 .7-.3 1.2-1.3 1.2" />
217
+ </g>
218
+ )
219
+ case 'tag':
220
+ return (
221
+ <g {...common}>
222
+ <path d="M5.6 9.2l2.4-2.4 2.9 2.9L8.5 12z" fill={color} fillOpacity="0.3" />
223
+ <circle cx="8" cy="8.9" r="0.55" fill={color} stroke="none" />
224
+ </g>
225
+ )
226
+ default:
227
+ return (
228
+ <g {...common}>
229
+ <path d="M5.4 7.2h5.2M5.4 9.3h5.2M5.4 11.4h3.2" />
230
+ </g>
231
+ )
232
+ }
233
+ }
80
234
 
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' }
235
+ export function FileIcon({ name, size = 15, className }: { name: string; size?: number; className?: string }) {
236
+ const kind = fileKind(name)
237
+ const gradient = useId()
238
+ const label = kind.label
239
+ const small = label && label.length > 3
240
+ return (
241
+ <svg viewBox="0 0 16 16" width={size} height={size} className={cn('shrink-0', className)} aria-hidden="true">
242
+ <defs>
243
+ <linearGradient id={gradient} x1="0" y1="0" x2="0" y2="1">
244
+ <stop offset="0" stopColor={kind.color} stopOpacity="0.2" />
245
+ <stop offset="1" stopColor={kind.color} stopOpacity="0.07" />
246
+ </linearGradient>
247
+ </defs>
248
+ <path
249
+ d="M3.6 1.4h5.6l3.4 3.4v9a1.2 1.2 0 01-1.2 1.2H3.6a1.2 1.2 0 01-1.2-1.2V2.6a1.2 1.2 0 011.2-1.2z"
250
+ fill={`url(#${gradient})`}
251
+ stroke={kind.color}
252
+ strokeOpacity="0.85"
253
+ strokeWidth="1"
254
+ />
255
+ <path d="M9.2 1.4v2.2a1.2 1.2 0 001.2 1.2h2.2" fill={kind.color} fillOpacity="0.25" stroke={kind.color} strokeOpacity="0.85" strokeWidth="1" strokeLinejoin="round" />
256
+ {label ? (
257
+ <>
258
+ <rect x="2.4" y="9" width="11.2" height="4.4" rx="0.9" fill={kind.color} />
259
+ <text
260
+ x="8"
261
+ y="12.35"
262
+ textAnchor="middle"
263
+ fontFamily="ui-sans-serif, system-ui, sans-serif"
264
+ fontWeight="800"
265
+ fontSize={small ? 3.3 : 3.9}
266
+ letterSpacing={small ? 0.1 : 0.2}
267
+ fill="#fff"
268
+ >
269
+ {label}
270
+ </text>
271
+ </>
272
+ ) : (
273
+ <Glyph kind={kind.glyph ?? 'text'} color={kind.color} />
274
+ )}
275
+ </svg>
276
+ )
87
277
  }
88
278
 
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)} />
279
+ interface FolderKind {
280
+ color: string
281
+ glyph?: 'src' | 'git' | 'node' | 'dist' | 'public' | 'test' | 'docs' | 'config' | 'server' | 'components' | 'style' | 'github'
282
+ }
283
+
284
+ const FOLDERS: Record<string, FolderKind> = {
285
+ src: { color: '#3b82f6', glyph: 'src' },
286
+ source: { color: '#3b82f6', glyph: 'src' },
287
+ app: { color: '#3b82f6', glyph: 'src' },
288
+ lib: { color: '#6366f1', glyph: 'src' },
289
+ core: { color: '#6366f1', glyph: 'src' },
290
+ components: { color: '#a855f7', glyph: 'components' },
291
+ component: { color: '#a855f7', glyph: 'components' },
292
+ ui: { color: '#a855f7', glyph: 'components' },
293
+ pages: { color: '#a855f7', glyph: 'components' },
294
+ views: { color: '#a855f7', glyph: 'components' },
295
+ styles: { color: '#ec4899', glyph: 'style' },
296
+ css: { color: '#ec4899', glyph: 'style' },
297
+ node_modules: { color: '#6aa84f', glyph: 'node' },
298
+ '.git': { color: '#f05133', glyph: 'git' },
299
+ '.github': { color: '#8b95a5', glyph: 'github' },
300
+ dist: { color: '#8b95a5', glyph: 'dist' },
301
+ build: { color: '#8b95a5', glyph: 'dist' },
302
+ out: { color: '#8b95a5', glyph: 'dist' },
303
+ '.next': { color: '#8b95a5', glyph: 'dist' },
304
+ public: { color: '#14b8a6', glyph: 'public' },
305
+ static: { color: '#14b8a6', glyph: 'public' },
306
+ assets: { color: '#14b8a6', glyph: 'public' },
307
+ images: { color: '#14b8a6', glyph: 'public' },
308
+ test: { color: '#ef4444', glyph: 'test' },
309
+ tests: { color: '#ef4444', glyph: 'test' },
310
+ __tests__: { color: '#ef4444', glyph: 'test' },
311
+ spec: { color: '#ef4444', glyph: 'test' },
312
+ docs: { color: '#0ea5e9', glyph: 'docs' },
313
+ doc: { color: '#0ea5e9', glyph: 'docs' },
314
+ server: { color: '#6366f1', glyph: 'server' },
315
+ api: { color: '#6366f1', glyph: 'server' },
316
+ scripts: { color: '#22c55e', glyph: 'config' },
317
+ config: { color: '#f59e0b', glyph: 'config' },
318
+ '.claude': { color: '#d97757', glyph: 'config' },
319
+ '.vscode': { color: '#2b8ae8', glyph: 'config' },
320
+ }
321
+
322
+ const DEFAULT_FOLDER: FolderKind = { color: '#e0a526' }
323
+
324
+ function FolderGlyph({ kind, color }: { kind: NonNullable<FolderKind['glyph']>; color: string }) {
325
+ const p = { stroke: '#fff', strokeWidth: 1.15, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none', opacity: 0.95 }
326
+ void color
327
+ switch (kind) {
328
+ case 'src':
329
+ return <path {...p} d="M6.6 8.4L5.2 9.8l1.4 1.4M9.4 8.4l1.4 1.4-1.4 1.4" />
330
+ case 'git':
331
+ return (
332
+ <g {...p}>
333
+ <circle cx="6.2" cy="11" r="0.8" />
334
+ <circle cx="6.2" cy="8.6" r="0.8" />
335
+ <circle cx="9.8" cy="9.4" r="0.8" />
336
+ <path d="M6.2 9.4v.8M7 8.7c1.6 0 2.1.2 2.1.9" />
337
+ </g>
338
+ )
339
+ case 'node':
340
+ return <path {...p} d="M8 7.6l2.3 1.3v2.4L8 12.6l-2.3-1.3V8.9z" />
341
+ case 'dist':
342
+ return <path {...p} d="M8 7.6v3.6M6.4 9.8L8 11.4l1.6-1.6" />
343
+ case 'public':
344
+ return (
345
+ <g {...p}>
346
+ <circle cx="8" cy="10" r="2.1" />
347
+ <path d="M5.9 10h4.2M8 7.9c.8.6.8 3.6 0 4.2M8 7.9c-.8.6-.8 3.6 0 4.2" />
348
+ </g>
349
+ )
350
+ case 'test':
351
+ return <path {...p} d="M7.1 7.6h1.8M7.4 7.6v1.9l-1.4 2.4h4l-1.4-2.4V7.6" />
352
+ case 'docs':
353
+ return <path {...p} d="M6.2 8h3.6M6.2 9.8h3.6M6.2 11.6h2.2" />
354
+ case 'config':
355
+ return (
356
+ <g {...p}>
357
+ <circle cx="8" cy="9.9" r="1.2" />
358
+ <path d="M8 7.6v.7M8 11.5v.7M5.7 9.9h.7M9.6 9.9h.7" />
359
+ </g>
360
+ )
361
+ case 'server':
362
+ return (
363
+ <g {...p}>
364
+ <rect x="5.7" y="7.6" width="4.6" height="1.7" rx="0.5" />
365
+ <rect x="5.7" y="10.1" width="4.6" height="1.7" rx="0.5" />
366
+ </g>
367
+ )
368
+ case 'components':
369
+ return <path {...p} d="M6 8.2h2v2H6zM8.4 10.6h2v2h-2zM8.4 8.2h2v1.6h-2z" />
370
+ case 'style':
371
+ return <path {...p} d="M8 7.6c1.6 1.4 2.3 2.4 2.3 3.3a2.3 2.3 0 01-4.6 0c0-.9.7-1.9 2.3-3.3z" />
372
+ case 'github':
373
+ return <circle {...p} cx="8" cy="9.9" r="2.1" />
374
+ default:
375
+ return null
105
376
  }
106
- const { icon: Icon, tone } = fileTone(path)
107
- return <Icon size={size} className={cn('shrink-0', tone, className)} />
377
+ }
378
+
379
+ export function FolderIcon({ name, open = false, size = 16, className }: { name: string; open?: boolean; size?: number; className?: string }) {
380
+ const kind = FOLDERS[name.toLowerCase()] ?? DEFAULT_FOLDER
381
+ return (
382
+ <svg viewBox="0 0 16 16" width={size} height={size} className={cn('shrink-0', className)} aria-hidden="true">
383
+ {/* Back panel with tab */}
384
+ <path d="M1.8 3.6A1.2 1.2 0 013 2.4h3.1c.4 0 .7.1.95.4l.75.8H13a1.2 1.2 0 011.2 1.2v7.2A1.2 1.2 0 0113 13.2H3a1.2 1.2 0 01-1.2-1.2z" fill={kind.color} fillOpacity="0.55" />
385
+ {/* Front panel: tilted when open */}
386
+ {open ? (
387
+ <path d="M3.2 6.2a1 1 0 01.96-.7h10.2a.8.8 0 01.78 1l-1.4 5.7a1.2 1.2 0 01-1.17.9H3a1.2 1.2 0 01-1.2-1.2z" fill={kind.color} />
388
+ ) : (
389
+ <path d="M1.8 5.6A1.2 1.2 0 013 4.4h10A1.2 1.2 0 0114.2 5.6V12A1.2 1.2 0 0113 13.2H3A1.2 1.2 0 011.8 12z" fill={kind.color} />
390
+ )}
391
+ <path d={open ? 'M4.3 5.9h9.6' : 'M2.4 5.1h11.2'} stroke="#fff" strokeOpacity="0.35" strokeWidth="0.7" strokeLinecap="round" />
392
+ {kind.glyph && !open && <FolderGlyph kind={kind.glyph} color={kind.color} />}
393
+ {kind.glyph && open && (
394
+ <g transform="translate(0.6 0)">
395
+ <FolderGlyph kind={kind.glyph} color={kind.color} />
396
+ </g>
397
+ )}
398
+ </svg>
399
+ )
108
400
  }
@@ -1,10 +1,11 @@
1
1
  import { useMemo, useState } from 'react'
2
- import { Activity, CalendarDays, Clock3, Cpu, CreditCard, MessageSquare, Sparkles } from 'lucide-react'
2
+ import { Activity, CalendarDays, Clock3, Coins, Cpu, 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, formatCredits, formatTokens, shortModelName, type UsageRange } from '../lib/usage'
7
+ import { formatCredits } from '../lib/credits'
8
+ import { computeUsage, formatTokens, shortModelName, type UsageRange } from '../lib/usage'
8
9
  import { PixleSol } from './PixleSol'
9
10
 
10
11
  const RANGES: Array<{ value: UsageRange; label: string }> = [
@@ -118,7 +119,7 @@ export function HomeDashboard() {
118
119
  <Stat icon={Activity} label="Sessions" value={String(stats.sessions)} />
119
120
  <Stat icon={MessageSquare} label="Messages" value={String(stats.messages)} />
120
121
  <Stat icon={Sparkles} label="Total tokens" value={stats.tokens ? formatTokens(stats.tokens) : '0'} />
121
- <Stat icon={CreditCard} label="Credits spent" value={formatCredits(stats.credits)} />
122
+ <Stat icon={Coins} label="Credits spent" value={stats.cost > 0 ? formatCredits(stats.cost) : '—'} />
122
123
  <Stat icon={CalendarDays} label="Active days" value={String(stats.activeDays)} />
123
124
  <Stat icon={Clock3} label="Peak hour" value={stats.peakHour} />
124
125
  <Stat icon={Cpu} label="Favorite model" value={shortModelName(stats.favoriteModel)} />
@@ -144,12 +145,6 @@ export function HomeDashboard() {
144
145
  {stats.messages === 0
145
146
  ? 'No activity in this range yet. Send a message to start the graph.'
146
147
  : `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
- )}
153
148
  {stats.favoriteModel !== '—' && (
154
149
  <>
155
150
  <span className="px-1.5 text-line">·</span>
@@ -175,7 +170,7 @@ export function HomeDashboard() {
175
170
  {shortModelName(model.model)}
176
171
  </span>
177
172
  <span className="shrink-0 text-[10.5px] tabular-nums text-faint">
178
- {formatTokens(model.tokens)} · {model.messages} msg
173
+ {formatTokens(model.tokens)} · {model.messages} msg{model.cost > 0 ? ` · ${formatCredits(model.cost)}` : ''}
179
174
  </span>
180
175
  </div>
181
176
  <div className="mt-1 h-1.5 overflow-hidden rounded-full bg-raised">
@@ -1,6 +1,9 @@
1
1
  import { cn } from '../lib/utils'
2
2
 
3
- /** The OpenSol aperture: a broken solar ring around a solid core. Inherits `currentColor`. */
3
+ /**
4
+ * The OpenSol mark: an "O" ring left open, a small sun resting in the opening, and a command prompt
5
+ * inside. Open + Sol + a place to type. Inherits `currentColor`.
6
+ */
4
7
  export function LogoMark({ size = 28, className, spin = false }: { size?: number; className?: string; spin?: boolean }) {
5
8
  return (
6
9
  <span
@@ -9,16 +12,11 @@ export function LogoMark({ size = 28, className, spin = false }: { size?: number
9
12
  aria-hidden="true"
10
13
  >
11
14
  <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="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" />
15
+ <path d="M25.66 13.41A10 10 0 1 1 18.59 6.34" stroke="currentColor" strokeWidth="2.8" strokeLinecap="round" />
16
+ <circle cx="23.07" cy="8.93" r="2.5" fill="currentColor" />
17
+ <g stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
18
+ <path d="M12.6 12.8L17 16l-4.4 3.2" />
19
+ <path d="M18.8 19.6h3" opacity="0.6" />
22
20
  </g>
23
21
  </svg>
24
22
  </span>
@@ -178,9 +178,10 @@ 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)
183
181
  const listRef = useRef<HTMLDivElement>(null)
182
+ // Only keyboard navigation may scroll the list. Hover moves the cursor too, and scrolling
183
+ // under a moving pointer makes the rows jump up and down.
184
+ const keyboardNav = useRef(false)
184
185
  const searchRef = useRef<HTMLInputElement>(null)
185
186
 
186
187
  const railRef = useRef<HTMLElement>(null)
@@ -228,12 +229,12 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
228
229
  }, [models, query])
229
230
 
230
231
  useLayoutEffect(() => {
231
- keyboardCursor.current = true
232
232
  setCursor(0)
233
233
  }, [query, provider])
234
234
 
235
235
  useEffect(() => {
236
- if (!keyboardCursor.current) return
236
+ if (!keyboardNav.current) return
237
+ keyboardNav.current = false
237
238
  listRef.current?.querySelector<HTMLElement>(`[data-row="${cursor}"]`)?.scrollIntoView({ block: 'nearest' })
238
239
  }, [cursor])
239
240
 
@@ -262,11 +263,11 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
262
263
  const onSearchKey = (event: KeyboardEvent<HTMLInputElement>) => {
263
264
  if (event.key === 'ArrowDown') {
264
265
  event.preventDefault()
265
- keyboardCursor.current = true
266
+ keyboardNav.current = true
266
267
  setCursor((value) => Math.min(filtered.length - 1, value + 1))
267
268
  } else if (event.key === 'ArrowUp') {
268
269
  event.preventDefault()
269
- keyboardCursor.current = true
270
+ keyboardNav.current = true
270
271
  setCursor((value) => Math.max(0, value - 1))
271
272
  } else if (event.key === 'Enter') {
272
273
  event.preventDefault()
@@ -478,11 +479,7 @@ export function ModelDeck({ open, onClose }: { open: boolean; onClose: () => voi
478
479
  setCursor(i)
479
480
  choose(model)
480
481
  }}
481
- onMouseEnter={() => {
482
- if (highlighted) return
483
- keyboardCursor.current = false
484
- setCursor(i)
485
- }}
482
+ onMouseEnter={() => setCursor(i)}
486
483
  style={{ animationDelay: `${Math.min(i * 22, 260)}ms` }}
487
484
  className={cn(
488
485
  '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',