@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.
- package/.github/workflows/build.yml +25 -0
- package/README.md +74 -2
- package/dist/assets/index-BmN7pBji.css +1 -0
- package/dist/assets/index-DgNDjkOz.js +301 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +55 -0
- package/dist/preview-host.html +33 -0
- package/index.html +54 -0
- package/package.json +43 -4
- package/postcss.config.js +6 -0
- package/public/favicon.svg +15 -0
- package/public/preview-host.html +33 -0
- package/server/README.md +75 -0
- package/server/daemon.mjs +1082 -0
- package/server/lib/checkpoints.mjs +124 -0
- package/server/lib/checkpoints.test.mjs +110 -0
- package/server/lib/deploy.mjs +436 -0
- package/server/lib/http.mjs +237 -0
- package/server/lib/plugins.mjs +321 -0
- package/server/lib/plugins.test.mjs +140 -0
- package/server/lib/providers.mjs +594 -0
- package/server/lib/store.mjs +491 -0
- package/server/lib/tools.mjs +990 -0
- package/server/opensol.mjs +202 -0
- package/server/package.json +42 -0
- package/shared/plugin-builder.test.mjs +43 -0
- package/shared/plugin-package.d.mts +34 -0
- package/shared/plugin-package.mjs +197 -0
- package/shared/plugin-package.test.mjs +94 -0
- package/src/App.tsx +152 -0
- package/src/agent/adapters/anthropic.ts +233 -0
- package/src/agent/adapters/gemini.ts +225 -0
- package/src/agent/adapters/openai-responses.ts +215 -0
- package/src/agent/adapters/openai.ts +200 -0
- package/src/agent/adapters/shared.ts +96 -0
- package/src/agent/approval.ts +20 -0
- package/src/agent/compact.ts +234 -0
- package/src/agent/index.ts +36 -0
- package/src/agent/model.ts +20 -0
- package/src/agent/prompt.ts +107 -0
- package/src/agent/queue.test.mjs +111 -0
- package/src/agent/queue.ts +84 -0
- package/src/agent/subagent.ts +109 -0
- package/src/agent/tools.ts +541 -0
- package/src/components/ChatView.tsx +403 -0
- package/src/components/CommandPalette.tsx +416 -0
- package/src/components/Composer.tsx +678 -0
- package/src/components/ContextBar.tsx +58 -0
- package/src/components/DeployModal.tsx +512 -0
- package/src/components/ErrorBoundary.tsx +75 -0
- package/src/components/HomeDashboard.tsx +189 -0
- package/src/components/Logo.tsx +41 -0
- package/src/components/Markdown.tsx +232 -0
- package/src/components/MessageItem.tsx +655 -0
- package/src/components/ModelPicker.tsx +487 -0
- package/src/components/PixleSol.tsx +243 -0
- package/src/components/PluginBuilder.tsx +285 -0
- package/src/components/PluginManager.tsx +341 -0
- package/src/components/PreviewPane.tsx +166 -0
- package/src/components/SettingsModal.tsx +2052 -0
- package/src/components/Sidebar.tsx +504 -0
- package/src/components/Splash.tsx +104 -0
- package/src/components/ToolCallCard.tsx +177 -0
- package/src/components/TopBar.tsx +182 -0
- package/src/components/TrajectoryView.tsx +178 -0
- package/src/components/WorkspacePanel.tsx +710 -0
- package/src/components/ui.tsx +311 -0
- package/src/core/kernel.ts +45 -0
- package/src/core/plugins/tools/fs.ts +219 -0
- package/src/core/plugins/tools/git.ts +83 -0
- package/src/core/plugins/tools/grep.ts +98 -0
- package/src/core/plugins/tools/shell.ts +88 -0
- package/src/core/plugins/tools/subagent.ts +36 -0
- package/src/core/plugins/tools/web.ts +97 -0
- package/src/core/services/agent-loop.ts +417 -0
- package/src/core/services/approval.ts +66 -0
- package/src/core/services/compaction.ts +45 -0
- package/src/core/services/llm.ts +32 -0
- package/src/core/services/sessions.ts +47 -0
- package/src/core/services/subagents.ts +15 -0
- package/src/core/services/system-prompt.ts +177 -0
- package/src/core/services/tools.ts +135 -0
- package/src/core/services/ui.ts +109 -0
- package/src/core/types.ts +139 -0
- package/src/data/providers.ts +125 -0
- package/src/data/skills.ts +68 -0
- package/src/index.css +776 -0
- package/src/lib/boot.ts +94 -0
- package/src/lib/context.ts +75 -0
- package/src/lib/conversation.test.mjs +64 -0
- package/src/lib/conversation.ts +59 -0
- package/src/lib/daemon.ts +475 -0
- package/src/lib/hooks.ts +33 -0
- package/src/lib/permissions.ts +60 -0
- package/src/lib/pixlesol.test.mjs +51 -0
- package/src/lib/pixlesol.ts +55 -0
- package/src/lib/plugin-folder.test.mjs +145 -0
- package/src/lib/plugin-folder.ts +96 -0
- package/src/lib/reasoning.ts +87 -0
- package/src/lib/sse.ts +75 -0
- package/src/lib/themes.test.mjs +72 -0
- package/src/lib/themes.ts +52 -0
- package/src/lib/usage.test.mjs +72 -0
- package/src/lib/usage.ts +155 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +16 -0
- package/src/plugins/api.test.mjs +77 -0
- package/src/plugins/api.ts +113 -0
- package/src/plugins/bridge.d.mts +1 -0
- package/src/plugins/bridge.mjs +26 -0
- package/src/plugins/bridge.test.mjs +17 -0
- package/src/plugins/index.ts +52 -0
- package/src/plugins/preview.test.mjs +18 -0
- package/src/plugins/preview.ts +34 -0
- package/src/plugins/runtime.ts +194 -0
- package/src/plugins/types.ts +40 -0
- package/src/store/chats.ts +278 -0
- package/src/store/models.ts +120 -0
- package/src/store/settings.ts +445 -0
- package/src/store/ui.ts +132 -0
- package/src/store/workspace.ts +299 -0
- package/src/types.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +83 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
}
|