@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,58 @@
1
+ /**
2
+ * ContextBar — compact context-usage indicator shown in the TopBar.
3
+ *
4
+ * Displays used / total context tokens and the percentage as plain text. When the session is
5
+ * being compacted it shows a pulsing "Compacting…" state. Colour grades from
6
+ * neutral → warning → danger as the context fills up.
7
+ */
8
+
9
+ import { useMemo } from 'react'
10
+ import type { ModelInfo, Message } from '../types'
11
+ import { getContextUsage, AUTO_COMPACT_THRESHOLD } from '../lib/context'
12
+ import { isCompacting } from '../agent/compact'
13
+ import { cn } from '../lib/utils'
14
+
15
+ interface ContextBarProps {
16
+ messages: Message[]
17
+ model: ModelInfo | null
18
+ sessionId: string | null
19
+ }
20
+
21
+ function formatTokens(n: number): string {
22
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
23
+ if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`
24
+ return String(n)
25
+ }
26
+
27
+ export function ContextBar({ messages, model, sessionId }: ContextBarProps) {
28
+ const usage = useMemo(() => getContextUsage(messages, model), [messages, model])
29
+ const compacting = sessionId ? isCompacting(sessionId) : false
30
+
31
+ if (messages.length === 0) return null
32
+
33
+ const { pct, used, limit } = usage
34
+ const isDanger = pct >= AUTO_COMPACT_THRESHOLD * 100
35
+ const isWarning = pct >= 70 && !isDanger
36
+
37
+ return (
38
+ <div
39
+ className={cn(
40
+ 'hidden items-baseline gap-1.5 text-[11.5px] tabular-nums text-muted sm:flex',
41
+ isWarning && 'text-warning',
42
+ isDanger && 'text-danger',
43
+ )}
44
+ title={`Context window: ~${used.toLocaleString()} of ${limit.toLocaleString()} tokens used (${pct}%)`}
45
+ >
46
+ {compacting ? (
47
+ <span className="text-shimmer">Compacting…</span>
48
+ ) : (
49
+ <>
50
+ <span className="text-ink/80">{formatTokens(used)}</span>
51
+ <span className="text-faint">/ {formatTokens(limit)} tokens</span>
52
+ <span className="text-faint">·</span>
53
+ <span>{pct}%</span>
54
+ </>
55
+ )}
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,512 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import {
3
+ Check,
4
+ Cloud,
5
+ Copy,
6
+ ExternalLink,
7
+ FolderOpen,
8
+ Globe2,
9
+ Play,
10
+ Rocket,
11
+ ShieldCheck,
12
+ Square,
13
+ Triangle,
14
+ Terminal,
15
+ } from 'lucide-react'
16
+ import type { DeployProviderId, DeployResult } from '../types'
17
+ import { streamDeploy } from '../lib/daemon'
18
+ import { cn } from '../lib/utils'
19
+ import { useSettings } from '../store/settings'
20
+ import { useUi } from '../store/ui'
21
+ import { useWorkspace } from '../store/workspace'
22
+ import { Modal, Spinner } from './ui'
23
+
24
+ const PROVIDERS: Array<{
25
+ id: DeployProviderId
26
+ label: string
27
+ short: string
28
+ icon: typeof Cloud
29
+ blurb: string
30
+ needs: string
31
+ }> = [
32
+ {
33
+ id: 'cloudflare',
34
+ label: 'Cloudflare Pages',
35
+ short: 'Pages',
36
+ icon: Cloud,
37
+ blurb: 'Fast global hosting with a generous free tier.',
38
+ needs: 'API token + account id',
39
+ },
40
+ {
41
+ id: 'netlify',
42
+ label: 'Netlify',
43
+ short: 'Netlify',
44
+ icon: Globe2,
45
+ blurb: 'Create a site and publish a folder in one step.',
46
+ needs: 'Personal access token',
47
+ },
48
+ {
49
+ id: 'vercel',
50
+ label: 'Vercel',
51
+ short: 'Vercel',
52
+ icon: Triangle,
53
+ blurb: 'Production deploys through the Vercel CLI.',
54
+ needs: 'Access token',
55
+ },
56
+ ]
57
+
58
+ interface LogLine {
59
+ id: number
60
+ stream: 'stdout' | 'stderr' | 'meta'
61
+ text: string
62
+ }
63
+
64
+ export function DeployModal() {
65
+ const open = useUi((state) => state.deployOpen)
66
+ const setOpen = useUi((state) => state.setDeployOpen)
67
+ const openSettings = useUi((state) => state.openSettings)
68
+ const notify = useUi((state) => state.notify)
69
+ const workspaceRoot = useWorkspace((state) => state.root)
70
+ const workspaceName = useWorkspace((state) => state.info.name)
71
+ const workspaceAvailable = useWorkspace((state) => state.info.available)
72
+ const deployTools = useSettings((state) => state.deployTools)
73
+ const deploySecrets = useSettings((state) => state.deploySecrets)
74
+
75
+ const [provider, setProvider] = useState<DeployProviderId>('cloudflare')
76
+ const [path, setPath] = useState('')
77
+ const [outputDir, setOutputDir] = useState('dist')
78
+ const [buildCommand, setBuildCommand] = useState('')
79
+ const [projectName, setProjectName] = useState('')
80
+ const [running, setRunning] = useState(false)
81
+ const [logs, setLogs] = useState<LogLine[]>([])
82
+ const [result, setResult] = useState<DeployResult | null>(null)
83
+ const [error, setError] = useState('')
84
+ const [copied, setCopied] = useState(false)
85
+ const abortRef = useRef<AbortController | null>(null)
86
+ const logRef = useRef<HTMLDivElement>(null)
87
+ const logId = useRef(0)
88
+
89
+ const meta = PROVIDERS.find((entry) => entry.id === provider) ?? PROVIDERS[0]
90
+ const tool = deployTools?.[provider]
91
+ const configured = Boolean(deploySecrets?.[provider]?.configured)
92
+
93
+ useEffect(() => {
94
+ if (!open) return
95
+ setPath(workspaceRoot)
96
+ setResult(null)
97
+ setError('')
98
+ setLogs([])
99
+ setRunning(false)
100
+ setCopied(false)
101
+ }, [open, workspaceRoot])
102
+
103
+ useEffect(() => {
104
+ if (!open) return
105
+ const node = logRef.current
106
+ if (node) node.scrollTop = node.scrollHeight
107
+ }, [logs, open])
108
+
109
+ useEffect(() => {
110
+ return () => abortRef.current?.abort()
111
+ }, [])
112
+
113
+ const append = (stream: LogLine['stream'], text: string) => {
114
+ const clean = text.trimEnd()
115
+ if (!clean) return
116
+ logId.current += 1
117
+ const id = logId.current
118
+ setLogs((current) => [...current.slice(-500), { id, stream, text: clean }])
119
+ }
120
+
121
+ const close = () => {
122
+ abortRef.current?.abort()
123
+ setOpen(false)
124
+ }
125
+
126
+ const deploy = async () => {
127
+ if (running) return
128
+ if (!path.trim()) {
129
+ setError('Choose the project folder to deploy.')
130
+ return
131
+ }
132
+ setRunning(true)
133
+ setResult(null)
134
+ setError('')
135
+ setLogs([])
136
+ const controller = new AbortController()
137
+ abortRef.current = controller
138
+ append('meta', `Preparing ${meta.label} deploy from ${path.trim()}`)
139
+ try {
140
+ for await (const event of streamDeploy(
141
+ {
142
+ provider,
143
+ path: path.trim(),
144
+ outputDir: outputDir.trim() || undefined,
145
+ buildCommand: buildCommand.trim() || undefined,
146
+ projectName: projectName.trim() || undefined,
147
+ },
148
+ controller.signal,
149
+ )) {
150
+ if (event.type === 'log') append(event.stream, event.text)
151
+ else if (event.type === 'start') append('meta', `Deploying ${event.provider}${event.path ? ` · ${event.path}` : ''}`)
152
+ else if (event.type === 'done') {
153
+ setResult(event.result)
154
+ append('meta', `Published ${event.result.url}`)
155
+ break
156
+ } else if (event.type === 'error') {
157
+ setError(event.message)
158
+ append('stderr', event.message)
159
+ break
160
+ }
161
+ }
162
+ } catch (cause) {
163
+ if (cause instanceof DOMException && cause.name === 'AbortError') {
164
+ append('meta', 'Deployment stopped.')
165
+ } else {
166
+ const message = cause instanceof Error ? cause.message : 'The deployment failed.'
167
+ setError(message)
168
+ append('stderr', message)
169
+ }
170
+ } finally {
171
+ abortRef.current = null
172
+ setRunning(false)
173
+ }
174
+ }
175
+
176
+ const copyUrl = async () => {
177
+ if (!result?.url) return
178
+ try {
179
+ await navigator.clipboard.writeText(result.url)
180
+ setCopied(true)
181
+ window.setTimeout(() => setCopied(false), 1600)
182
+ } catch {
183
+ notify('Clipboard access was blocked by the browser.', 'error')
184
+ }
185
+ }
186
+
187
+ return (
188
+ <Modal open={open} onClose={close} labelledBy="deploy-title" wide className="h-[92dvh] p-0 sm:h-[min(90dvh,760px)]">
189
+ <div className="flex shrink-0 items-start justify-between gap-4 border-b border-line px-5 py-4 sm:px-6">
190
+ <div className="flex min-w-0 items-start gap-3">
191
+ <span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-accent-soft text-accent">
192
+ <Rocket size={17} />
193
+ </span>
194
+ <div className="min-w-0">
195
+ <h2 id="deploy-title" className="font-display text-[20px] font-semibold tracking-[-0.02em] text-ink">
196
+ Deploy a site
197
+ </h2>
198
+ <p className="mt-0.5 text-[11.5px] leading-relaxed text-muted">
199
+ Publish the open workspace or another project folder. Tokens stay encrypted on this machine.
200
+ </p>
201
+ </div>
202
+ </div>
203
+ <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>
205
+ </button>
206
+ </div>
207
+
208
+ <div className="grid min-h-0 flex-1 lg:grid-cols-[minmax(0,380px)_minmax(0,1fr)]">
209
+ <div className="thin-scroll min-h-0 overflow-y-auto border-b border-line lg:border-b-0 lg:border-r">
210
+ <div className="space-y-5 p-5">
211
+ <div>
212
+ <p className="mb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">Host</p>
213
+ <div className="grid gap-1.5">
214
+ {PROVIDERS.map((entry) => {
215
+ const Icon = entry.icon
216
+ const active = provider === entry.id
217
+ return (
218
+ <button
219
+ key={entry.id}
220
+ type="button"
221
+ onClick={() => setProvider(entry.id)}
222
+ disabled={running}
223
+ className={cn(
224
+ 'flex items-start gap-2.5 rounded-xl border px-3 py-2.5 text-left transition-colors disabled:opacity-60',
225
+ active
226
+ ? 'border-accent/35 bg-accent-soft/45'
227
+ : 'border-line bg-panel hover:bg-raised/50',
228
+ )}
229
+ >
230
+ <span
231
+ 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',
234
+ )}
235
+ >
236
+ <Icon size={13} />
237
+ </span>
238
+ <span className="min-w-0 flex-1">
239
+ <span className="flex items-center gap-2">
240
+ <span className={cn('text-[12px] font-medium', active ? 'text-accent' : 'text-ink')}>
241
+ {entry.label}
242
+ </span>
243
+ {deploySecrets?.[entry.id]?.configured ? (
244
+ <Check size={11} className="text-success" />
245
+ ) : (
246
+ <span className="text-[9px] text-warning">needs token</span>
247
+ )}
248
+ </span>
249
+ <span className="mt-0.5 block text-[10px] leading-relaxed text-faint">{entry.blurb}</span>
250
+ </span>
251
+ </button>
252
+ )
253
+ })}
254
+ </div>
255
+ </div>
256
+
257
+ {!configured && (
258
+ <div className="rounded-xl border border-warning/25 bg-warning/[0.045] p-3">
259
+ <p className="flex items-start gap-1.5 text-[10.5px] leading-relaxed text-muted">
260
+ <ShieldCheck size={12} className="mt-0.5 shrink-0 text-warning" />
261
+ {meta.label} needs {meta.needs}. Add it once in Settings → Deploy; it is encrypted with AES-256-GCM
262
+ and never appears in the page again.
263
+ </p>
264
+ <button
265
+ type="button"
266
+ onClick={() => {
267
+ setOpen(false)
268
+ openSettings('deploy')
269
+ }}
270
+ className="mt-2 inline-flex h-7 items-center rounded-lg bg-ink px-2.5 text-[10.5px] font-medium text-panel"
271
+ >
272
+ Add {meta.label} credentials
273
+ </button>
274
+ </div>
275
+ )}
276
+
277
+ <div>
278
+ <div className="mb-1.5 flex items-center justify-between gap-2">
279
+ <label htmlFor="deploy-path" className="text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
280
+ Project folder
281
+ </label>
282
+ {workspaceAvailable && (
283
+ <button
284
+ type="button"
285
+ onClick={() => setPath(workspaceRoot)}
286
+ disabled={running}
287
+ className="inline-flex items-center gap-1 text-[9.5px] font-medium text-accent disabled:opacity-40"
288
+ >
289
+ <FolderOpen size={10} />
290
+ Use open workspace
291
+ </button>
292
+ )}
293
+ </div>
294
+ <input
295
+ id="deploy-path"
296
+ value={path}
297
+ onChange={(event) => setPath(event.target.value)}
298
+ disabled={running}
299
+ placeholder="/absolute/path/to/project or ~/Projects/site"
300
+ spellCheck={false}
301
+ 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
+ />
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>
307
+ </div>
308
+
309
+ <div className="grid grid-cols-2 gap-2.5">
310
+ <div>
311
+ <label htmlFor="deploy-output" className="mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
312
+ Output folder
313
+ </label>
314
+ <input
315
+ id="deploy-output"
316
+ value={outputDir}
317
+ onChange={(event) => setOutputDir(event.target.value)}
318
+ disabled={running}
319
+ placeholder="dist"
320
+ spellCheck={false}
321
+ 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
+ />
323
+ </div>
324
+ <div>
325
+ <label htmlFor="deploy-name" className="mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
326
+ Project name
327
+ </label>
328
+ <input
329
+ id="deploy-name"
330
+ value={projectName}
331
+ onChange={(event) => setProjectName(event.target.value)}
332
+ disabled={running}
333
+ placeholder="my-site"
334
+ spellCheck={false}
335
+ 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"
336
+ />
337
+ </div>
338
+ </div>
339
+
340
+ <div className="flex flex-wrap gap-1">
341
+ {['dist', 'build', 'out', '.'].map((value) => (
342
+ <button
343
+ key={value}
344
+ type="button"
345
+ onClick={() => setOutputDir(value)}
346
+ disabled={running}
347
+ className={cn('chip h-7 px-2 text-[9.5px]', outputDir === value && 'chip-active')}
348
+ >
349
+ {value}
350
+ </button>
351
+ ))}
352
+ </div>
353
+
354
+ <div>
355
+ <label htmlFor="deploy-build" className="mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
356
+ Build command <span className="font-normal normal-case tracking-normal text-faint">optional</span>
357
+ </label>
358
+ <input
359
+ id="deploy-build"
360
+ value={buildCommand}
361
+ onChange={(event) => setBuildCommand(event.target.value)}
362
+ disabled={running}
363
+ placeholder="npm run build"
364
+ spellCheck={false}
365
+ 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"
366
+ />
367
+ <div className="mt-1.5 flex flex-wrap gap-1">
368
+ {['npm run build', 'pnpm build', 'yarn build'].map((value) => (
369
+ <button
370
+ key={value}
371
+ type="button"
372
+ onClick={() => setBuildCommand(value)}
373
+ disabled={running}
374
+ className="rounded-md border border-line px-1.5 py-0.5 text-[9px] text-faint transition-colors hover:text-ink"
375
+ >
376
+ {value}
377
+ </button>
378
+ ))}
379
+ {buildCommand && (
380
+ <button
381
+ type="button"
382
+ onClick={() => setBuildCommand('')}
383
+ disabled={running}
384
+ className="px-1.5 py-0.5 text-[9px] text-faint hover:text-danger"
385
+ >
386
+ clear
387
+ </button>
388
+ )}
389
+ </div>
390
+ </div>
391
+
392
+ <div className="flex items-start gap-1.5 rounded-lg border border-line bg-raised/40 px-2.5 py-2 text-[9.5px] leading-relaxed text-faint">
393
+ <Terminal size={11} className="mt-0.5 shrink-0" />
394
+ {tool?.installed
395
+ ? `${tool.label} ${tool.version ? `(${tool.version})` : ''} is installed locally.`
396
+ : `${tool?.label ?? meta.short} CLI is not installed; OpenSol will use npx for this deploy.`}
397
+ </div>
398
+ </div>
399
+ </div>
400
+
401
+ <div className="flex min-h-0 flex-col">
402
+ <div className="flex shrink-0 items-center gap-2 border-b border-line px-4 py-2.5">
403
+ <span className={cn('h-1.5 w-1.5 rounded-full', running ? 'animate-breathe bg-accent' : result ? 'bg-success' : 'bg-faint')} />
404
+ <p className="min-w-0 flex-1 truncate text-[11px] font-medium text-muted">
405
+ {running ? `Deploying with ${meta.label}…` : result ? 'Deployment complete' : 'Deploy output'}
406
+ </p>
407
+ {running && (
408
+ <button
409
+ type="button"
410
+ onClick={() => abortRef.current?.abort()}
411
+ className="inline-flex h-7 items-center gap-1 rounded-lg border border-line px-2 text-[10px] font-medium text-muted hover:text-ink"
412
+ >
413
+ <Square size={9} fill="currentColor" /> Stop
414
+ </button>
415
+ )}
416
+ {!running && (
417
+ <button
418
+ type="button"
419
+ onClick={() => void deploy()}
420
+ disabled={!configured || !path.trim()}
421
+ className="inline-flex h-7 items-center gap-1.5 rounded-lg bg-accent px-2.5 text-[10.5px] font-medium text-on-accent transition-colors hover:bg-accent/90 disabled:cursor-not-allowed disabled:opacity-40"
422
+ >
423
+ <Play size={10} fill="currentColor" />
424
+ Deploy
425
+ </button>
426
+ )}
427
+ </div>
428
+
429
+ {result && (
430
+ <div className="shrink-0 border-b border-line bg-success/[0.045] px-4 py-3">
431
+ <div className="flex items-start gap-2">
432
+ <Check size={14} className="mt-0.5 shrink-0 text-success" />
433
+ <div className="min-w-0 flex-1">
434
+ <p className="text-[11.5px] font-medium text-ink">Published successfully</p>
435
+ <a
436
+ href={result.url}
437
+ target="_blank"
438
+ rel="noreferrer"
439
+ className="mt-0.5 block truncate text-[11px] text-accent hover:underline"
440
+ >
441
+ {result.url}
442
+ </a>
443
+ </div>
444
+ <button type="button" onClick={copyUrl} className="icon-button h-7 w-7" title="Copy URL" aria-label="Copy URL">
445
+ {copied ? <Check size={12} className="text-success" /> : <Copy size={12} />}
446
+ </button>
447
+ <a
448
+ href={result.url}
449
+ target="_blank"
450
+ rel="noreferrer"
451
+ className="icon-button h-7 w-7"
452
+ title="Open site"
453
+ aria-label="Open site"
454
+ >
455
+ <ExternalLink size={12} />
456
+ </a>
457
+ </div>
458
+ </div>
459
+ )}
460
+
461
+ {error && (
462
+ <div className="shrink-0 border-b border-danger/20 bg-danger/[0.045] px-4 py-2.5 text-[10.5px] leading-relaxed text-danger">
463
+ {error}
464
+ </div>
465
+ )}
466
+
467
+ <div ref={logRef} className="thin-scroll min-h-0 flex-1 overflow-y-auto bg-canvas/40 px-4 py-3">
468
+ {logs.length === 0 && !running ? (
469
+ <div className="flex h-full min-h-[180px] flex-col items-center justify-center gap-2 px-6 text-center">
470
+ <span className="flex h-10 w-10 items-center justify-center rounded-xl bg-raised text-faint">
471
+ <Rocket size={17} />
472
+ </span>
473
+ <p className="text-[11.5px] font-medium text-muted">Ready when you are</p>
474
+ <p className="max-w-[260px] text-[10px] leading-relaxed text-faint">
475
+ The build and deploy output appears here. Credentials are passed to the CLI through the environment,
476
+ never printed in the log.
477
+ </p>
478
+ </div>
479
+ ) : (
480
+ <pre className="whitespace-pre-wrap break-words font-mono text-[10px] leading-[1.7] text-muted">
481
+ {logs.map((line) => (
482
+ <span
483
+ key={line.id}
484
+ className={cn(
485
+ 'block',
486
+ line.stream === 'stderr' && 'text-danger',
487
+ line.stream === 'meta' && 'text-accent',
488
+ )}
489
+ >
490
+ {line.stream === 'meta' ? '› ' : ''}
491
+ {line.text}
492
+ </span>
493
+ ))}
494
+ {running && (
495
+ <span className="mt-1 flex items-center gap-1.5 text-accent">
496
+ <Spinner size={10} /> working…
497
+ </span>
498
+ )}
499
+ </pre>
500
+ )}
501
+ </div>
502
+
503
+ <div className="flex h-9 shrink-0 items-center gap-2 border-t border-line px-4 text-[9px] text-faint">
504
+ <ShieldCheck size={10} />
505
+ Tokens encrypted at rest
506
+ <span className="ml-auto">{path.trim() ? path.trim().split(/[\\/]/).filter(Boolean).pop() : 'No folder'}</span>
507
+ </div>
508
+ </div>
509
+ </div>
510
+ </Modal>
511
+ )
512
+ }
@@ -0,0 +1,75 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react'
2
+ import { LogoMark } from './Logo'
3
+
4
+ interface State {
5
+ error: Error | null
6
+ copied: boolean
7
+ }
8
+
9
+ /** Keeps a render error from blanking the whole app; chats are already saved by the daemon. */
10
+ export class ErrorBoundary extends Component<{ children: ReactNode }, State> {
11
+ state: State = { error: null, copied: false }
12
+
13
+ static getDerivedStateFromError(error: Error): State {
14
+ return { error, copied: false }
15
+ }
16
+
17
+ componentDidCatch(error: Error, info: ErrorInfo) {
18
+ console.error('[maxcode] render error', error, info.componentStack)
19
+ }
20
+
21
+ private details() {
22
+ const { error } = this.state
23
+ return `${error?.name}: ${error?.message}\n\n${error?.stack ?? ''}`
24
+ }
25
+
26
+ private copy = async () => {
27
+ try {
28
+ await navigator.clipboard.writeText(this.details())
29
+ this.setState({ copied: true })
30
+ } catch {
31
+ // Clipboard can be blocked; the details stay visible below.
32
+ }
33
+ }
34
+
35
+ render() {
36
+ if (!this.state.error) return this.props.children
37
+ return (
38
+ <div className="flex h-dvh items-center justify-center bg-panel px-5 text-ink">
39
+ <div className="w-full max-w-[460px]">
40
+ <LogoMark size={34} />
41
+ <h1 className="mt-4 font-display text-[28px] font-normal tracking-[-0.02em]">Something broke on this screen</h1>
42
+ <p className="mt-2 text-[14px] leading-relaxed text-muted">
43
+ Your conversations are saved on this computer. Try the screen again, or reload the app.
44
+ </p>
45
+ <pre className="thin-scroll mt-4 max-h-40 overflow-auto rounded-xl border border-line bg-canvas p-3 font-mono text-[11.5px] leading-relaxed text-muted">
46
+ {this.state.error.message}
47
+ </pre>
48
+ <div className="mt-5 flex flex-wrap items-center gap-2">
49
+ <button
50
+ type="button"
51
+ onClick={() => this.setState({ error: null })}
52
+ className="inline-flex h-9 items-center rounded-full bg-accent px-4 text-[13px] font-medium text-on-accent transition hover:brightness-110"
53
+ >
54
+ Try again
55
+ </button>
56
+ <button
57
+ type="button"
58
+ onClick={() => window.location.reload()}
59
+ className="inline-flex h-9 items-center rounded-full border border-line px-4 text-[13px] font-medium text-ink transition-colors hover:bg-raised"
60
+ >
61
+ Reload app
62
+ </button>
63
+ <button
64
+ type="button"
65
+ onClick={() => void this.copy()}
66
+ className="inline-flex h-9 items-center rounded-full px-3 text-[13px] text-muted transition-colors hover:text-ink"
67
+ >
68
+ {this.state.copied ? 'Copied' : 'Copy details'}
69
+ </button>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+ }