@bibobonking/opensol 0.0.0-stage → 3.0.2

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 (127) 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/server/README.md +75 -0
  14. package/server/daemon.mjs +1082 -0
  15. package/server/lib/checkpoints.mjs +124 -0
  16. package/server/lib/checkpoints.test.mjs +110 -0
  17. package/server/lib/deploy.mjs +436 -0
  18. package/server/lib/http.mjs +237 -0
  19. package/server/lib/plugins.mjs +321 -0
  20. package/server/lib/plugins.test.mjs +140 -0
  21. package/server/lib/providers.mjs +594 -0
  22. package/server/lib/store.mjs +491 -0
  23. package/server/lib/tools.mjs +990 -0
  24. package/server/opensol.mjs +202 -0
  25. package/server/package.json +42 -0
  26. package/shared/plugin-builder.test.mjs +43 -0
  27. package/shared/plugin-package.d.mts +34 -0
  28. package/shared/plugin-package.mjs +197 -0
  29. package/shared/plugin-package.test.mjs +94 -0
  30. package/src/App.tsx +152 -0
  31. package/src/agent/adapters/anthropic.ts +233 -0
  32. package/src/agent/adapters/gemini.ts +225 -0
  33. package/src/agent/adapters/openai-responses.ts +215 -0
  34. package/src/agent/adapters/openai.ts +200 -0
  35. package/src/agent/adapters/shared.ts +96 -0
  36. package/src/agent/approval.ts +20 -0
  37. package/src/agent/compact.ts +234 -0
  38. package/src/agent/index.ts +36 -0
  39. package/src/agent/model.ts +20 -0
  40. package/src/agent/prompt.ts +107 -0
  41. package/src/agent/queue.test.mjs +111 -0
  42. package/src/agent/queue.ts +84 -0
  43. package/src/agent/subagent.ts +109 -0
  44. package/src/agent/tools.ts +541 -0
  45. package/src/components/ChatView.tsx +403 -0
  46. package/src/components/CommandPalette.tsx +416 -0
  47. package/src/components/Composer.tsx +678 -0
  48. package/src/components/ContextBar.tsx +58 -0
  49. package/src/components/DeployModal.tsx +512 -0
  50. package/src/components/ErrorBoundary.tsx +75 -0
  51. package/src/components/HomeDashboard.tsx +189 -0
  52. package/src/components/Logo.tsx +41 -0
  53. package/src/components/Markdown.tsx +232 -0
  54. package/src/components/MessageItem.tsx +655 -0
  55. package/src/components/ModelPicker.tsx +487 -0
  56. package/src/components/PixleSol.tsx +243 -0
  57. package/src/components/PluginBuilder.tsx +285 -0
  58. package/src/components/PluginManager.tsx +341 -0
  59. package/src/components/PreviewPane.tsx +166 -0
  60. package/src/components/SettingsModal.tsx +2052 -0
  61. package/src/components/Sidebar.tsx +504 -0
  62. package/src/components/Splash.tsx +104 -0
  63. package/src/components/ToolCallCard.tsx +177 -0
  64. package/src/components/TopBar.tsx +182 -0
  65. package/src/components/TrajectoryView.tsx +178 -0
  66. package/src/components/WorkspacePanel.tsx +710 -0
  67. package/src/components/ui.tsx +311 -0
  68. package/src/core/kernel.ts +45 -0
  69. package/src/core/plugins/tools/fs.ts +219 -0
  70. package/src/core/plugins/tools/git.ts +83 -0
  71. package/src/core/plugins/tools/grep.ts +98 -0
  72. package/src/core/plugins/tools/shell.ts +88 -0
  73. package/src/core/plugins/tools/subagent.ts +36 -0
  74. package/src/core/plugins/tools/web.ts +97 -0
  75. package/src/core/services/agent-loop.ts +417 -0
  76. package/src/core/services/approval.ts +66 -0
  77. package/src/core/services/compaction.ts +45 -0
  78. package/src/core/services/llm.ts +32 -0
  79. package/src/core/services/sessions.ts +47 -0
  80. package/src/core/services/subagents.ts +15 -0
  81. package/src/core/services/system-prompt.ts +177 -0
  82. package/src/core/services/tools.ts +135 -0
  83. package/src/core/services/ui.ts +109 -0
  84. package/src/core/types.ts +139 -0
  85. package/src/data/providers.ts +125 -0
  86. package/src/data/skills.ts +68 -0
  87. package/src/index.css +776 -0
  88. package/src/lib/boot.ts +94 -0
  89. package/src/lib/context.ts +75 -0
  90. package/src/lib/conversation.test.mjs +64 -0
  91. package/src/lib/conversation.ts +59 -0
  92. package/src/lib/daemon.ts +475 -0
  93. package/src/lib/hooks.ts +33 -0
  94. package/src/lib/permissions.ts +60 -0
  95. package/src/lib/pixlesol.test.mjs +51 -0
  96. package/src/lib/pixlesol.ts +55 -0
  97. package/src/lib/plugin-folder.test.mjs +145 -0
  98. package/src/lib/plugin-folder.ts +96 -0
  99. package/src/lib/reasoning.ts +87 -0
  100. package/src/lib/sse.ts +75 -0
  101. package/src/lib/themes.test.mjs +72 -0
  102. package/src/lib/themes.ts +52 -0
  103. package/src/lib/usage.test.mjs +72 -0
  104. package/src/lib/usage.ts +155 -0
  105. package/src/lib/utils.ts +73 -0
  106. package/src/main.tsx +16 -0
  107. package/src/plugins/api.test.mjs +77 -0
  108. package/src/plugins/api.ts +113 -0
  109. package/src/plugins/bridge.d.mts +1 -0
  110. package/src/plugins/bridge.mjs +26 -0
  111. package/src/plugins/bridge.test.mjs +17 -0
  112. package/src/plugins/index.ts +52 -0
  113. package/src/plugins/preview.test.mjs +18 -0
  114. package/src/plugins/preview.ts +34 -0
  115. package/src/plugins/runtime.ts +194 -0
  116. package/src/plugins/types.ts +40 -0
  117. package/src/store/chats.ts +278 -0
  118. package/src/store/models.ts +120 -0
  119. package/src/store/settings.ts +445 -0
  120. package/src/store/ui.ts +132 -0
  121. package/src/store/workspace.ts +299 -0
  122. package/src/types.ts +337 -0
  123. package/src/vite-env.d.ts +1 -0
  124. package/tailwind.config.js +83 -0
  125. package/tsconfig.json +22 -0
  126. package/tsconfig.tsbuildinfo +1 -0
  127. package/vite.config.ts +19 -0
@@ -0,0 +1,403 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { ArrowUp, Bug, FlaskConical, FolderSearch, Gamepad2, Globe2, KeyRound, LayoutTemplate, ListChecks, Sparkles } from 'lucide-react'
3
+ import type { Attachment } from '../types'
4
+ import { createId, useChats } from '../store/chats'
5
+ import { activeModelId, hasCredentials, useSettings } from '../store/settings'
6
+ import { useActiveModel } from '../store/models'
7
+ import { useRuntime, useUi } from '../store/ui'
8
+ import { useWorkspace } from '../store/workspace'
9
+ import { getProviderName } from '../data/providers'
10
+ import { stopRun, runAgent } from '../agent'
11
+ import { getContextUsage } from '../lib/context'
12
+ import { pixleMood } from '../lib/pixlesol'
13
+ import { formatTokens } from '../lib/usage'
14
+ import { groupConversationTurns } from '../lib/conversation'
15
+ import { TopBar, type ChatViewMode } from './TopBar'
16
+ import { Composer } from './Composer'
17
+ import { TurnBlock } from './MessageItem'
18
+ import { HomeDashboard, HomeGreeting } from './HomeDashboard'
19
+ import { TrajectoryView } from './TrajectoryView'
20
+
21
+ const STARTERS = [
22
+ {
23
+ icon: Gamepad2,
24
+ label: 'Build a game',
25
+ prompt:
26
+ 'Build a small browser game as one self-contained index.html (canvas, no build step). Make it playable with keyboard and touch, with a score and a restart button. Put the whole file in a single html code block so I can preview it.',
27
+ },
28
+ {
29
+ icon: LayoutTemplate,
30
+ label: 'Design a landing page',
31
+ prompt:
32
+ 'Design a polished, responsive landing page for a small indie game studio. Choose a distinctive typographic style, keep it to one file, and put everything in a single html code block so I can preview it.',
33
+ },
34
+ {
35
+ icon: Bug,
36
+ label: 'Trace a bug',
37
+ prompt:
38
+ 'Inspect this workspace for a real bug. Explain the root cause, make the smallest correct fix, and run the project checks.',
39
+ },
40
+ {
41
+ icon: FolderSearch,
42
+ label: 'Audit this folder',
43
+ prompt:
44
+ 'Audit this workspace. Read the key files first, then list the three most important improvements with file references and a short plan.',
45
+ },
46
+ {
47
+ icon: FlaskConical,
48
+ label: 'Write tests',
49
+ prompt:
50
+ 'Find the least-tested, highest-risk module in this workspace, write focused tests for it, run them, and fix anything they reveal.',
51
+ },
52
+ {
53
+ icon: Globe2,
54
+ label: 'Research with sources',
55
+ prompt:
56
+ 'Search the web for the current release notes of the main framework in this project, then summarise what matters and cite the pages you read.',
57
+ },
58
+ ]
59
+
60
+ export function ChatView() {
61
+ const sessions = useChats((state) => state.sessions)
62
+ const activeId = useChats((state) => state.activeId)
63
+ const session = sessions.find((item) => item.id === activeId) ?? null
64
+ const streaming = useRuntime((state) => (activeId ? Boolean(state.streaming[activeId]) : false))
65
+ const settings = useSettings()
66
+ const setMode = useSettings((state) => state.setMode)
67
+ const update = useSettings((state) => state.update)
68
+ const notify = useUi((state) => state.notify)
69
+ const openSettings = useUi((state) => state.openSettings)
70
+ const focusComposer = useUi((state) => state.focusComposer)
71
+ const setWorkspaceOpen = useUi((state) => state.setWorkspaceOpen)
72
+ const setDeployOpen = useUi((state) => state.setDeployOpen)
73
+ const askConfirm = useUi((state) => state.askConfirm)
74
+ const workspace = useWorkspace((state) => state.info)
75
+ const git = useWorkspace((state) => state.git)
76
+ const activeModel = useActiveModel()
77
+ const [draft, setDraft] = useState('')
78
+ const [view, setView] = useState<ChatViewMode>('chat')
79
+ const scrollRef = useRef<HTMLDivElement>(null)
80
+ const [atBottom, setAtBottom] = useState(true)
81
+ const lastSessionRef = useRef<string | null>(activeId)
82
+ const lastMessageIdRef = useRef<string | null>(null)
83
+
84
+ const messages = session?.messages ?? []
85
+ const lastMessage = messages[messages.length - 1]
86
+ const lastAssistantId = [...messages].reverse().find((message) => message.role === 'assistant')?.id
87
+ const connected = hasCredentials(settings)
88
+ const selectedModel = activeModelId(settings)
89
+ const activeSkills = settings.skills.filter((skill) => skill.enabled).length
90
+ const turns = groupConversationTurns(messages)
91
+ const context = getContextUsage(messages, activeModel)
92
+ const mood = pixleMood(messages, streaming)
93
+
94
+ const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
95
+ const node = scrollRef.current
96
+ if (!node) return
97
+ node.scrollTo({ top: node.scrollHeight, behavior })
98
+ }
99
+
100
+ useEffect(() => {
101
+ if (lastSessionRef.current !== activeId) {
102
+ lastSessionRef.current = activeId
103
+ setAtBottom(true)
104
+ setView('chat')
105
+ requestAnimationFrame(() => scrollToBottom('auto'))
106
+ const current = useChats.getState().sessions.find((item) => item.id === activeId)
107
+ if (current?.mode) setMode(current.mode)
108
+ }
109
+ }, [activeId, setMode])
110
+
111
+ useEffect(() => {
112
+ if (!lastMessage) return
113
+ if (lastMessage.id !== lastMessageIdRef.current) {
114
+ lastMessageIdRef.current = lastMessage.id
115
+ if (lastMessage.role === 'user') {
116
+ setAtBottom(true)
117
+ requestAnimationFrame(() => scrollToBottom('auto'))
118
+ } else if (atBottom) {
119
+ requestAnimationFrame(() => scrollToBottom('auto'))
120
+ }
121
+ } else if (atBottom) {
122
+ scrollToBottom('auto')
123
+ }
124
+ }, [lastMessage, atBottom])
125
+
126
+ useEffect(() => {
127
+ if (!atBottom) return
128
+ const timer = window.setTimeout(() => scrollToBottom('auto'), 40)
129
+ return () => window.clearTimeout(timer)
130
+ }, [messages, atBottom])
131
+
132
+ const send = async (text: string, attachments: Attachment[]): Promise<boolean> => {
133
+ const current = useSettings.getState()
134
+ if (!hasCredentials(current)) {
135
+ notify(
136
+ current.provider === 'custom'
137
+ ? 'Add a base URL for your custom endpoint in Settings → Models.'
138
+ : `Add an API key for ${getProviderName(current.provider)} in Settings → Models.`,
139
+ 'error',
140
+ )
141
+ openSettings('models')
142
+ return false
143
+ }
144
+ const modelId = activeModelId(current)
145
+ if (!modelId) {
146
+ notify('Choose a model before sending a message.', 'error')
147
+ openSettings('models')
148
+ return false
149
+ }
150
+
151
+ let sessionId = activeId
152
+ if (!sessionId || !useChats.getState().sessions.some((item) => item.id === sessionId)) {
153
+ sessionId = useChats.getState().createSession(current.provider, modelId, current.mode)
154
+ }
155
+ const content = text.trim()
156
+ const userMessageId = createId('msg')
157
+ useChats.getState().addMessage(sessionId, {
158
+ id: userMessageId,
159
+ role: 'user',
160
+ content,
161
+ attachments: attachments.length > 0 ? attachments : undefined,
162
+ createdAt: Date.now(),
163
+ })
164
+ useChats.getState().updateSession(sessionId, { mode: current.mode })
165
+ setDraft('')
166
+ setAtBottom(true)
167
+ runAgent(sessionId, userMessageId)
168
+ return true
169
+ }
170
+
171
+ const useStarter = (prompt: string) => {
172
+ setDraft(prompt)
173
+ focusComposer()
174
+ }
175
+
176
+ const handleCommand = async (command: string, args: string): Promise<boolean> => {
177
+ switch (command) {
178
+ case 'search': {
179
+ if (!args.trim()) {
180
+ setDraft('/search ')
181
+ notify('Type what you want OpenSol to research.', 'info')
182
+ focusComposer()
183
+ return false
184
+ }
185
+ update({ webSearchEnabled: true })
186
+ return send(
187
+ `Use the web to research this: ${args.trim()}\n\nRead the most useful sources, cite the URLs you actually opened, and answer for this workspace.`,
188
+ [],
189
+ )
190
+ }
191
+ case 'plan': {
192
+ setMode('plan')
193
+ if (!args.trim()) {
194
+ notify('Plan mode is on: investigation only until you switch to Build.', 'info')
195
+ focusComposer()
196
+ return true
197
+ }
198
+ return send(args.trim(), [])
199
+ }
200
+ case 'build': {
201
+ setMode('build')
202
+ if (!args.trim()) {
203
+ notify('Build mode is on: OpenSol can edit files and run commands.', 'info')
204
+ focusComposer()
205
+ return true
206
+ }
207
+ return send(args.trim(), [])
208
+ }
209
+ case 'new': {
210
+ useChats.getState().newChat()
211
+ focusComposer()
212
+ return true
213
+ }
214
+ case 'workspace': {
215
+ setWorkspaceOpen(true)
216
+ return true
217
+ }
218
+ case 'deploy': {
219
+ setDeployOpen(true)
220
+ return true
221
+ }
222
+ case 'settings': {
223
+ openSettings('models')
224
+ return true
225
+ }
226
+ case 'clear': {
227
+ const current = useChats.getState().sessions.find((item) => item.id === activeId)
228
+ if (!current || current.messages.length === 0) return true
229
+ const confirmed = await askConfirm({
230
+ title: 'Clear this conversation?',
231
+ body: 'The messages in this conversation will be removed from your local history. The workspace files are not touched.',
232
+ confirmLabel: 'Clear conversation',
233
+ tone: 'danger',
234
+ })
235
+ if (!confirmed) return true
236
+ stopRun(current.id)
237
+ useChats.getState().updateSession(current.id, { messages: [] })
238
+ useChats.getState().updateSession(current.id, { title: 'New conversation' })
239
+ notify('Conversation cleared.', 'success')
240
+ return true
241
+ }
242
+ default:
243
+ return false
244
+ }
245
+ }
246
+
247
+ return (
248
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-panel">
249
+ <TopBar session={session} view={view} onViewChange={setView} />
250
+
251
+ {!session || messages.length === 0 ? (
252
+ <div className="thin-scroll relative min-h-0 flex-1 overflow-y-auto">
253
+ <div className="hero-wash pointer-events-none absolute inset-0" aria-hidden="true" />
254
+ <div className="bg-dots pointer-events-none absolute inset-0 opacity-70" aria-hidden="true" />
255
+ <div className="relative mx-auto flex min-h-full w-full max-w-[720px] flex-col justify-center px-4 py-10 sm:px-6">
256
+ <div className="animate-blurin">
257
+ <HomeGreeting />
258
+ </div>
259
+
260
+ <div className="mt-8 w-full animate-rise [animation-delay:120ms]">
261
+ {(!connected || !selectedModel) && (
262
+ <button
263
+ type="button"
264
+ onClick={() => openSettings('models')}
265
+ className="mb-3 flex w-full items-center gap-2 rounded-xl border border-accent/30 bg-accent-soft px-4 py-2.5 text-left text-[12.5px] font-medium text-accent transition-all hover:border-accent/50"
266
+ >
267
+ <KeyRound size={14} className="shrink-0" />
268
+ <span className="flex-1">{connected ? 'Choose a model to start' : 'Connect a provider to start'}</span>
269
+ <span className="text-[11px] font-normal opacity-70">Open settings →</span>
270
+ </button>
271
+ )}
272
+ <Composer
273
+ value={draft}
274
+ onChange={setDraft}
275
+ onSend={send}
276
+ onStop={() => undefined}
277
+ onCommand={handleCommand}
278
+ streaming={false}
279
+ hints
280
+ autoFocus
281
+ placeholder={connected ? 'How can I help you today?' : 'Add a provider key to start'}
282
+ pixleMood={connected ? 'idle' : 'sleepy'}
283
+ />
284
+ <div className="stagger mt-5 grid grid-cols-2 gap-2 sm:grid-cols-3">
285
+ {STARTERS.slice(0, 6).map((starter) => (
286
+ <button
287
+ key={starter.label}
288
+ type="button"
289
+ onClick={() => useStarter(starter.prompt)}
290
+ className="card-lift group/starter flex items-center gap-2.5 rounded-xl border border-line bg-panel px-3 py-2.5 text-left text-[12.5px] font-medium text-muted hover:text-ink"
291
+ >
292
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-raised text-faint transition-colors duration-200 group-hover/starter:bg-accent-soft group-hover/starter:text-accent">
293
+ <starter.icon size={14} strokeWidth={1.8} />
294
+ </span>
295
+ <span className="min-w-0 truncate">{starter.label}</span>
296
+ </button>
297
+ ))}
298
+ </div>
299
+ <div className="mt-2.5 flex items-center justify-center gap-3 px-1 text-[11px] text-faint">
300
+ <span className="truncate">
301
+ {workspace.available ? workspace.name : 'No folder open'}
302
+ {workspace.available && git.available && git.branch ? ` · ${git.branch}` : ''}
303
+ </span>
304
+ <span aria-hidden="true">·</span>
305
+ <span className="shrink-0">
306
+ {activeSkills} skill{activeSkills === 1 ? '' : 's'} ·{' '}
307
+ {!connected ? 'Provider needed' : !selectedModel ? 'Model needed' : 'Ready'}
308
+ </span>
309
+ </div>
310
+ </div>
311
+
312
+ <div className="mt-10 animate-rise [animation-delay:420ms]">
313
+ <HomeDashboard />
314
+ </div>
315
+ </div>
316
+ </div>
317
+ ) : (
318
+ <>
319
+ <div className="relative min-h-0 flex-1">
320
+ {view === 'trajectory' ? (
321
+ <TrajectoryView session={session} />
322
+ ) : (
323
+ <>
324
+ <div
325
+ ref={scrollRef}
326
+ onScroll={(event) => {
327
+ const node = event.currentTarget
328
+ const distance = node.scrollHeight - node.scrollTop - node.clientHeight
329
+ setAtBottom(distance < 90)
330
+ }}
331
+ className="thin-scroll h-full overflow-y-auto"
332
+ >
333
+ <div className="mx-auto w-full max-w-[760px] px-4 pb-12 pt-8 sm:px-6 sm:pt-10">
334
+ {turns.map((turn, index) => (
335
+ <div key={turn.id} className={index === 0 ? 'animate-rise' : 'mt-10 animate-rise'}>
336
+ <TurnBlock
337
+ turn={turn}
338
+ sessionId={session.id}
339
+ streaming={streaming}
340
+ lastAssistantId={lastAssistantId}
341
+ activeTurn={streaming && index === turns.length - 1}
342
+ />
343
+ </div>
344
+ ))}
345
+ </div>
346
+ </div>
347
+
348
+ {!atBottom && (
349
+ <button
350
+ type="button"
351
+ onClick={() => {
352
+ setAtBottom(true)
353
+ scrollToBottom('smooth')
354
+ }}
355
+ className="surface-lift absolute bottom-4 left-1/2 -translate-x-1/2 animate-scalein rounded-full border border-line px-3.5 py-1.5 text-[11.5px] font-medium text-muted shadow-composer transition-colors hover:text-ink"
356
+ >
357
+ <span className="inline-flex items-center gap-1.5">
358
+ <ArrowUp size={11} />
359
+ Jump to latest
360
+ </span>
361
+ </button>
362
+ )}
363
+ </>
364
+ )}
365
+ </div>
366
+
367
+ <div className="shrink-0 bg-panel px-3 pb-3 pt-1 sm:px-6 sm:pb-4">
368
+ <div className="mx-auto w-full max-w-[760px]">
369
+ <Composer
370
+ value={draft}
371
+ onChange={setDraft}
372
+ onSend={send}
373
+ onStop={() => session && stopRun(session.id)}
374
+ onCommand={handleCommand}
375
+ streaming={streaming}
376
+ placeholder={connected ? 'How can I help you today?' : 'Add a provider key to start'}
377
+ contextPct={context.pct}
378
+ contextTitle={`Context: ~${formatTokens(context.used)} / ${formatTokens(context.limit)} tokens`}
379
+ pixleMood={mood}
380
+ />
381
+ <div className="mt-2 flex items-center justify-between gap-3 px-2 text-[11px] text-faint">
382
+ <span className="flex items-center gap-1">
383
+ {settings.mode === 'plan' ? <ListChecks size={10} /> : <Sparkles size={10} />}
384
+ {settings.mode === 'plan'
385
+ ? 'Plan mode · read-only tools'
386
+ : activeSkills > 0
387
+ ? `${activeSkills} skill${activeSkills === 1 ? '' : 's'} loaded`
388
+ : 'Build mode · full tools'}
389
+ </span>
390
+ <span className="hidden sm:inline">
391
+ {workspace.available ? `Working in ${workspace.name}` : 'No workspace open'}
392
+ </span>
393
+ <span className="sm:hidden">
394
+ {settings.tools.shell.approval === 'ask' ? 'Shell asks first' : 'Shell auto-runs'}
395
+ </span>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </>
400
+ )}
401
+ </div>
402
+ )
403
+ }