@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,243 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { PIXLE_LABELS, type PixleMood } from '../lib/pixlesol'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PixleSol is OpenSol's pixel sun. Everything is drawn on one 16×16 crisp-edges
|
|
7
|
+
* grid so the sprite stays sharp at any size. Rays are split into two sets
|
|
8
|
+
* (plus / diagonal) that swap in "busy" moods to give a pixel-art twinkle
|
|
9
|
+
* without ever rotating (rotation would blur the pixels).
|
|
10
|
+
*/
|
|
11
|
+
const PALETTE = {
|
|
12
|
+
outline: '#6b3410',
|
|
13
|
+
body: '#fbbf24',
|
|
14
|
+
shade: '#f59e0b',
|
|
15
|
+
highlight: '#fef3c7',
|
|
16
|
+
ray: '#f59e0b',
|
|
17
|
+
eye: '#1a0f06',
|
|
18
|
+
mark: '#4a2508',
|
|
19
|
+
blush: '#fb7185',
|
|
20
|
+
sweat: '#38bdf8',
|
|
21
|
+
mint: '#34d399',
|
|
22
|
+
sky: '#60a5fa',
|
|
23
|
+
rose: '#fb7185',
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
type Px = readonly [x: number, y: number, w: number, h: number, color: keyof typeof PALETTE]
|
|
27
|
+
|
|
28
|
+
const MOOD_ANIMATION: Record<PixleMood, string> = {
|
|
29
|
+
wave: 'animate-bob',
|
|
30
|
+
idle: 'animate-float-slow',
|
|
31
|
+
thinking: 'animate-float',
|
|
32
|
+
working: 'animate-bob-fast',
|
|
33
|
+
typing: 'animate-bob',
|
|
34
|
+
happy: 'animate-pop',
|
|
35
|
+
alert: 'animate-bounce',
|
|
36
|
+
error: 'animate-shake',
|
|
37
|
+
sleepy: 'animate-float-slow',
|
|
38
|
+
party: 'animate-pop',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TWINKLE = new Set<PixleMood>(['thinking', 'working', 'typing', 'party', 'wave'])
|
|
42
|
+
|
|
43
|
+
// Body mask: a rounded 8×8 disc centred on (8, 8).
|
|
44
|
+
const BODY_ROWS: Array<[y: number, x0: number, x1: number]> = [
|
|
45
|
+
[4, 6, 9],
|
|
46
|
+
[5, 5, 10],
|
|
47
|
+
[6, 4, 11],
|
|
48
|
+
[7, 4, 11],
|
|
49
|
+
[8, 4, 11],
|
|
50
|
+
[9, 4, 11],
|
|
51
|
+
[10, 5, 10],
|
|
52
|
+
[11, 6, 9],
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const BODY = new Set<string>()
|
|
56
|
+
for (const [y, x0, x1] of BODY_ROWS) for (let x = x0; x <= x1; x++) BODY.add(`${x},${y}`)
|
|
57
|
+
|
|
58
|
+
const OUTLINE: Array<[number, number]> = []
|
|
59
|
+
for (let y = 2; y <= 13; y++) {
|
|
60
|
+
for (let x = 2; x <= 13; x++) {
|
|
61
|
+
if (BODY.has(`${x},${y}`)) continue
|
|
62
|
+
const touches =
|
|
63
|
+
BODY.has(`${x - 1},${y}`) || BODY.has(`${x + 1},${y}`) || BODY.has(`${x},${y - 1}`) || BODY.has(`${x},${y + 1}`)
|
|
64
|
+
if (touches) OUTLINE.push([x, y])
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RAYS_PLUS: Px[] = [
|
|
69
|
+
[7, 0, 2, 2, 'ray'],
|
|
70
|
+
[7, 14, 2, 2, 'ray'],
|
|
71
|
+
[0, 7, 2, 2, 'ray'],
|
|
72
|
+
[14, 7, 2, 2, 'ray'],
|
|
73
|
+
]
|
|
74
|
+
const RAYS_DIAG: Px[] = [
|
|
75
|
+
[2, 2, 1, 1, 'ray'],
|
|
76
|
+
[13, 2, 1, 1, 'ray'],
|
|
77
|
+
[2, 13, 1, 1, 'ray'],
|
|
78
|
+
[13, 13, 1, 1, 'ray'],
|
|
79
|
+
[3, 3, 1, 1, 'ray'],
|
|
80
|
+
[12, 3, 1, 1, 'ray'],
|
|
81
|
+
[3, 12, 1, 1, 'ray'],
|
|
82
|
+
[12, 12, 1, 1, 'ray'],
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* PixleSol has no mouth. Its eyes are always plain black dots; mood is carried by
|
|
87
|
+
* where the dots look, how big they are, small marks beside the sun (?, !, z,
|
|
88
|
+
* a sweat drop, confetti) and how the rays behave.
|
|
89
|
+
*/
|
|
90
|
+
type EyeSize = 'sm' | 'md' | 'lg' | 'flat'
|
|
91
|
+
|
|
92
|
+
function eyes(size: EyeSize, dx = 0, dy = 0): Px[] {
|
|
93
|
+
const [w, h] = size === 'sm' ? [1, 1] : size === 'md' ? [1, 2] : size === 'lg' ? [2, 2] : [2, 1]
|
|
94
|
+
const y = (size === 'sm' ? 8 : size === 'flat' ? 8 : 7) + dy
|
|
95
|
+
const left = size === 'lg' || size === 'flat' ? 5 : 6
|
|
96
|
+
const right = size === 'lg' || size === 'flat' ? 9 : 9
|
|
97
|
+
return [
|
|
98
|
+
[left + dx, y, w, h, 'eye'],
|
|
99
|
+
[right + dx, y, w, h, 'eye'],
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const BLUSH: Px[] = [
|
|
104
|
+
[4, 9, 1, 1, 'blush'],
|
|
105
|
+
[11, 9, 1, 1, 'blush'],
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
interface Look {
|
|
109
|
+
eyes: Px[]
|
|
110
|
+
extras: Px[]
|
|
111
|
+
/** Hide the diagonal rays so marks beside the sun stay readable. */
|
|
112
|
+
clearCorner?: boolean
|
|
113
|
+
/** Extra class on the eye group (blink / scan animation). */
|
|
114
|
+
eyeClass?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function look(mood: PixleMood): Look {
|
|
118
|
+
switch (mood) {
|
|
119
|
+
case 'wave':
|
|
120
|
+
return { eyes: eyes('md'), extras: BLUSH, eyeClass: 'pxs-blink' }
|
|
121
|
+
case 'happy':
|
|
122
|
+
return {
|
|
123
|
+
eyes: eyes('lg'),
|
|
124
|
+
extras: [...BLUSH, [13, 1, 1, 3, 'ray'], [12, 2, 3, 1, 'ray']],
|
|
125
|
+
clearCorner: true,
|
|
126
|
+
}
|
|
127
|
+
case 'party':
|
|
128
|
+
return {
|
|
129
|
+
eyes: eyes('lg'),
|
|
130
|
+
extras: [
|
|
131
|
+
[1, 3, 1, 1, 'rose'],
|
|
132
|
+
[14, 2, 1, 1, 'mint'],
|
|
133
|
+
[0, 11, 1, 1, 'sky'],
|
|
134
|
+
[15, 12, 1, 1, 'rose'],
|
|
135
|
+
[4, 0, 1, 1, 'mint'],
|
|
136
|
+
[11, 0, 1, 1, 'sky'],
|
|
137
|
+
[2, 14, 1, 1, 'mint'],
|
|
138
|
+
[13, 14, 1, 1, 'rose'],
|
|
139
|
+
],
|
|
140
|
+
clearCorner: true,
|
|
141
|
+
}
|
|
142
|
+
case 'thinking':
|
|
143
|
+
return {
|
|
144
|
+
eyes: eyes('md', 1, -1),
|
|
145
|
+
extras: [
|
|
146
|
+
[10, 1, 1, 1, 'mark'],
|
|
147
|
+
[12, 1, 1, 1, 'mark'],
|
|
148
|
+
[14, 1, 1, 1, 'mark'],
|
|
149
|
+
],
|
|
150
|
+
clearCorner: true,
|
|
151
|
+
}
|
|
152
|
+
case 'working':
|
|
153
|
+
return { eyes: eyes('sm', 0, 1), extras: [], eyeClass: 'pxs-scan' }
|
|
154
|
+
case 'typing':
|
|
155
|
+
return { eyes: eyes('md', 0, 1), extras: [], eyeClass: 'pxs-scan' }
|
|
156
|
+
case 'alert':
|
|
157
|
+
return {
|
|
158
|
+
eyes: eyes('lg'),
|
|
159
|
+
extras: [
|
|
160
|
+
[13, 0, 1, 3, 'mark'],
|
|
161
|
+
[13, 4, 1, 1, 'mark'],
|
|
162
|
+
],
|
|
163
|
+
clearCorner: true,
|
|
164
|
+
}
|
|
165
|
+
case 'error':
|
|
166
|
+
return {
|
|
167
|
+
eyes: eyes('sm', 0, 1),
|
|
168
|
+
extras: [
|
|
169
|
+
[12, 3, 1, 1, 'sweat'],
|
|
170
|
+
[12, 4, 1, 2, 'sweat'],
|
|
171
|
+
],
|
|
172
|
+
clearCorner: true,
|
|
173
|
+
}
|
|
174
|
+
case 'sleepy':
|
|
175
|
+
return {
|
|
176
|
+
eyes: eyes('flat'),
|
|
177
|
+
extras: [
|
|
178
|
+
[12, 1, 4, 1, 'mark'],
|
|
179
|
+
[14, 2, 1, 1, 'mark'],
|
|
180
|
+
[13, 3, 1, 1, 'mark'],
|
|
181
|
+
[12, 4, 4, 1, 'mark'],
|
|
182
|
+
],
|
|
183
|
+
clearCorner: true,
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
return { eyes: eyes('md'), extras: [], eyeClass: 'pxs-blink' }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function PixleSol({
|
|
191
|
+
mood = 'idle',
|
|
192
|
+
size = 40,
|
|
193
|
+
className,
|
|
194
|
+
animate = true,
|
|
195
|
+
}: {
|
|
196
|
+
mood?: PixleMood
|
|
197
|
+
size?: number
|
|
198
|
+
className?: string
|
|
199
|
+
animate?: boolean
|
|
200
|
+
}): ReactNode {
|
|
201
|
+
const label = PIXLE_LABELS[mood]
|
|
202
|
+
const twinkle = animate && TWINKLE.has(mood)
|
|
203
|
+
const face = look(mood)
|
|
204
|
+
return (
|
|
205
|
+
<span
|
|
206
|
+
className={cn('inline-flex shrink-0 select-none', animate && MOOD_ANIMATION[mood], className)}
|
|
207
|
+
role="img"
|
|
208
|
+
aria-label={`PixleSol — ${label}`}
|
|
209
|
+
title={`PixleSol — ${label}`}
|
|
210
|
+
>
|
|
211
|
+
<svg viewBox="0 0 16 16" width={size} height={size} shapeRendering="crispEdges" aria-hidden="true">
|
|
212
|
+
<g className={twinkle ? 'pxs-a' : undefined}>
|
|
213
|
+
{RAYS_PLUS.map(([x, y, w, h, c], i) => (
|
|
214
|
+
<rect key={i} x={x} y={y} width={w} height={h} fill={PALETTE[c]} />
|
|
215
|
+
))}
|
|
216
|
+
</g>
|
|
217
|
+
<g className={twinkle ? 'pxs-b' : undefined} opacity={face.clearCorner ? 0.35 : undefined}>
|
|
218
|
+
{RAYS_DIAG.map(([x, y, w, h, c], i) => (
|
|
219
|
+
<rect key={i} x={x} y={y} width={w} height={h} fill={PALETTE[c]} />
|
|
220
|
+
))}
|
|
221
|
+
</g>
|
|
222
|
+
{OUTLINE.map(([x, y]) => (
|
|
223
|
+
<rect key={`o${x},${y}`} x={x} y={y} width={1} height={1} fill={PALETTE.outline} />
|
|
224
|
+
))}
|
|
225
|
+
{BODY_ROWS.map(([y, x0, x1]) => (
|
|
226
|
+
<rect key={`b${y}`} x={x0} y={y} width={x1 - x0 + 1} height={1} fill={PALETTE.body} />
|
|
227
|
+
))}
|
|
228
|
+
<rect x={5} y={11} width={6} height={1} fill={PALETTE.shade} />
|
|
229
|
+
<rect x={9} y={10} width={2} height={1} fill={PALETTE.shade} />
|
|
230
|
+
<rect x={6} y={4} width={2} height={1} fill={PALETTE.highlight} />
|
|
231
|
+
<rect x={5} y={5} width={1} height={1} fill={PALETTE.highlight} />
|
|
232
|
+
<g className={animate ? face.eyeClass : undefined}>
|
|
233
|
+
{face.eyes.map(([x, y, w, h, color], index) => (
|
|
234
|
+
<rect key={index} x={x} y={y} width={w} height={h} fill={PALETTE[color]} />
|
|
235
|
+
))}
|
|
236
|
+
</g>
|
|
237
|
+
{face.extras.map(([x, y, w, h, color], index) => (
|
|
238
|
+
<rect key={`x${index}`} x={x} y={y} width={w} height={h} fill={PALETTE[color]} />
|
|
239
|
+
))}
|
|
240
|
+
</svg>
|
|
241
|
+
</span>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { FileUp, FolderTree, Hammer } from 'lucide-react'
|
|
3
|
+
import { buildPluginArchive, validatePluginManifest } from '../../shared/plugin-package.mjs'
|
|
4
|
+
import type { PluginCapability, PluginManifest } from '../../shared/plugin-package.mjs'
|
|
5
|
+
import { installPluginArchive, type InstalledPlugin } from '../lib/daemon'
|
|
6
|
+
import { isBinaryPath, loadPluginFolder, type PluginFolder } from '../lib/plugin-folder'
|
|
7
|
+
import { formatBytes } from '../lib/utils'
|
|
8
|
+
import { useUi } from '../store/ui'
|
|
9
|
+
import { Switch } from './ui'
|
|
10
|
+
|
|
11
|
+
const CAPABILITIES: PluginCapability[] = [
|
|
12
|
+
'workspace.list',
|
|
13
|
+
'workspace.read',
|
|
14
|
+
'workspace.write',
|
|
15
|
+
'workspace.edit',
|
|
16
|
+
'ui.commands',
|
|
17
|
+
'ui.panels',
|
|
18
|
+
'ui.themes',
|
|
19
|
+
'network.request',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const ACCEPT = '.js,.css,.json,.txt,.html,.svg,.png,.jpg,.jpeg,.gif,.webp,.woff,.woff2'
|
|
23
|
+
|
|
24
|
+
const fieldClass =
|
|
25
|
+
'h-8 w-full rounded-lg border border-line bg-panel px-2.5 text-[12px] text-ink outline-none transition-colors placeholder:text-faint focus:border-accent/50'
|
|
26
|
+
|
|
27
|
+
async function toPicked(file: File) {
|
|
28
|
+
const path = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name
|
|
29
|
+
return { path, content: isBinaryPath(path) ? new Uint8Array(await file.arrayBuffer()) : await file.text() }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function PluginBuilder({ onInstalled }: { onInstalled?: (plugin: InstalledPlugin) => void }) {
|
|
33
|
+
const notify = useUi((state) => state.notify)
|
|
34
|
+
const [id, setId] = useState('my-plugin')
|
|
35
|
+
const [name, setName] = useState('My plugin')
|
|
36
|
+
const [version, setVersion] = useState('1.0.0')
|
|
37
|
+
const [apiVersion, setApiVersion] = useState<1 | 2>(1)
|
|
38
|
+
const [capabilities, setCapabilities] = useState<PluginCapability[]>(['ui.commands'])
|
|
39
|
+
const [domains, setDomains] = useState('')
|
|
40
|
+
const [folder, setFolder] = useState<PluginFolder | null>(null)
|
|
41
|
+
const folderRef = useRef<HTMLInputElement>(null)
|
|
42
|
+
const fileRef = useRef<HTMLInputElement>(null)
|
|
43
|
+
|
|
44
|
+
const load = async (list: FileList | null) => {
|
|
45
|
+
if (!list?.length) return
|
|
46
|
+
try {
|
|
47
|
+
const picked = await Promise.all([...list].map(toPicked))
|
|
48
|
+
const next = loadPluginFolder(picked)
|
|
49
|
+
setFolder(next)
|
|
50
|
+
// A manifest.json in the folder is a starting point, not the last word: the fields below win.
|
|
51
|
+
if (next.manifest) {
|
|
52
|
+
setId(next.manifest.id)
|
|
53
|
+
setName(next.manifest.name)
|
|
54
|
+
setVersion(next.manifest.version)
|
|
55
|
+
setApiVersion(next.manifest.apiVersion === 2 ? 2 : 1)
|
|
56
|
+
setCapabilities(next.manifest.capabilities)
|
|
57
|
+
setDomains(next.manifest.domains.join(', '))
|
|
58
|
+
}
|
|
59
|
+
notify(
|
|
60
|
+
next.manifestError
|
|
61
|
+
? `Loaded ${Object.keys(next.files).length} file(s). Its manifest.json was ignored: ${next.manifestError}`
|
|
62
|
+
: `Loaded ${Object.keys(next.files).length} file(s) — entry ${next.entry}.`,
|
|
63
|
+
next.manifestError ? 'error' : 'info',
|
|
64
|
+
)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
setFolder(null)
|
|
67
|
+
notify(error instanceof Error ? error.message : 'Those files are not a plugin folder.', 'error')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const build = () => {
|
|
72
|
+
try {
|
|
73
|
+
if (!folder) throw new Error('Load a plugin folder or its files first.')
|
|
74
|
+
const manifest: PluginManifest = {
|
|
75
|
+
id: id.trim(),
|
|
76
|
+
name: name.trim(),
|
|
77
|
+
version: version.trim(),
|
|
78
|
+
apiVersion,
|
|
79
|
+
entry: folder.entry,
|
|
80
|
+
stylesheet: folder.stylesheet,
|
|
81
|
+
capabilities: apiVersion === 2 ? [] : capabilities,
|
|
82
|
+
domains: domains
|
|
83
|
+
.split(/[\s,]+/)
|
|
84
|
+
.map((entry) => entry.trim())
|
|
85
|
+
.filter(Boolean),
|
|
86
|
+
}
|
|
87
|
+
validatePluginManifest(manifest)
|
|
88
|
+
const files = { ...folder.files }
|
|
89
|
+
// A plugin with no stylesheet of its own still ships an empty one.
|
|
90
|
+
files[manifest.stylesheet] ??= ''
|
|
91
|
+
return { archive: buildPluginArchive({ manifest, files }) }
|
|
92
|
+
} catch (error) {
|
|
93
|
+
notify(error instanceof Error ? error.message : 'That package could not be built.', 'error')
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const download = () => {
|
|
99
|
+
const built = build()
|
|
100
|
+
if (!built) return
|
|
101
|
+
const url = URL.createObjectURL(new Blob([built.archive as BlobPart], { type: 'application/json' }))
|
|
102
|
+
const link = document.createElement('a')
|
|
103
|
+
link.href = url
|
|
104
|
+
link.download = `${id.trim() || 'plugin'}.plugin.json`
|
|
105
|
+
link.click()
|
|
106
|
+
URL.revokeObjectURL(url)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const install = async () => {
|
|
110
|
+
const built = build()
|
|
111
|
+
if (!built) return
|
|
112
|
+
try {
|
|
113
|
+
const { plugin } = await installPluginArchive(built.archive)
|
|
114
|
+
onInstalled?.(plugin)
|
|
115
|
+
notify(`${plugin.manifest.name} installed. It starts sandboxed and disabled.`, 'success')
|
|
116
|
+
} catch (error) {
|
|
117
|
+
notify(error instanceof Error ? error.message : 'The daemon rejected this package.', 'error')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const loaded = folder ? Object.keys(folder.files) : []
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<details className="rounded-xl border border-line bg-raised/20">
|
|
125
|
+
<summary className="flex cursor-pointer items-center gap-2 px-3 py-2.5 text-[11.5px] font-medium text-ink">
|
|
126
|
+
<Hammer size={13} className="text-faint" />
|
|
127
|
+
Build a plugin package from a folder
|
|
128
|
+
</summary>
|
|
129
|
+
<div className="space-y-2.5 border-t border-line px-3 py-3">
|
|
130
|
+
<p className="text-[10.5px] leading-relaxed text-faint">
|
|
131
|
+
Load a plugin folder, or the files themselves. One <code className="font-mono">.js</code> entry and one
|
|
132
|
+
optional <code className="font-mono">.css</code> stylesheet become the package. A{' '}
|
|
133
|
+
<code className="font-mono">manifest.json</code> in the folder fills these fields in; the fields win.
|
|
134
|
+
</p>
|
|
135
|
+
|
|
136
|
+
<div className="flex items-center gap-2 rounded-lg border border-line bg-panel px-2.5 py-2">
|
|
137
|
+
<Switch
|
|
138
|
+
checked={apiVersion === 2}
|
|
139
|
+
onChange={(on: boolean) => setApiVersion(on ? 2 : 1)}
|
|
140
|
+
label="API v2"
|
|
141
|
+
/>
|
|
142
|
+
<span className="text-[10.5px] font-medium text-ink">API v2 — free plugin (full app access)</span>
|
|
143
|
+
{apiVersion === 2 && (
|
|
144
|
+
<span className="ml-auto text-[9.5px] text-accent">ctx = Cordis kernel scope</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => folderRef.current?.click()}
|
|
152
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3 text-[10.5px] font-medium text-on-accent hover:bg-accent/90"
|
|
153
|
+
>
|
|
154
|
+
<FolderTree size={12} />
|
|
155
|
+
Load folder
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={() => fileRef.current?.click()}
|
|
160
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-line bg-panel px-3 text-[10.5px] font-medium text-ink hover:bg-raised"
|
|
161
|
+
>
|
|
162
|
+
<FileUp size={12} />
|
|
163
|
+
Load files
|
|
164
|
+
</button>
|
|
165
|
+
<input
|
|
166
|
+
type="file"
|
|
167
|
+
multiple
|
|
168
|
+
// React's types predate `webkitdirectory`; set it on the node instead.
|
|
169
|
+
ref={(node) => {
|
|
170
|
+
folderRef.current = node
|
|
171
|
+
if (node) node.webkitdirectory = true
|
|
172
|
+
}}
|
|
173
|
+
className="hidden"
|
|
174
|
+
onChange={(event) => {
|
|
175
|
+
void load(event.target.files)
|
|
176
|
+
event.target.value = ''
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
<input
|
|
180
|
+
ref={fileRef}
|
|
181
|
+
type="file"
|
|
182
|
+
multiple
|
|
183
|
+
accept={ACCEPT}
|
|
184
|
+
className="hidden"
|
|
185
|
+
onChange={(event) => {
|
|
186
|
+
void load(event.target.files)
|
|
187
|
+
event.target.value = ''
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{folder && (
|
|
193
|
+
<div className="rounded-lg border border-line bg-panel/60 px-2.5 py-2">
|
|
194
|
+
<p className="text-[10.5px] text-muted">
|
|
195
|
+
<span className="font-medium text-ink">{loaded.length} file(s)</span> · entry{' '}
|
|
196
|
+
<code className="font-mono">{folder.entry}</code> · stylesheet{' '}
|
|
197
|
+
<code className="font-mono">{folder.stylesheet}</code>
|
|
198
|
+
</p>
|
|
199
|
+
<ul className="mt-1 space-y-0.5">
|
|
200
|
+
{loaded.slice(0, 6).map((path) => (
|
|
201
|
+
<li key={path} className="truncate font-mono text-[9.5px] text-faint">
|
|
202
|
+
{path} · {formatBytes(
|
|
203
|
+
typeof folder.files[path] === 'string'
|
|
204
|
+
? new TextEncoder().encode(folder.files[path]).length
|
|
205
|
+
: (folder.files[path] as Uint8Array).byteLength,
|
|
206
|
+
)}
|
|
207
|
+
</li>
|
|
208
|
+
))}
|
|
209
|
+
{loaded.length > 6 && <li className="font-mono text-[9.5px] text-faint">+{loaded.length - 6} more</li>}
|
|
210
|
+
</ul>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
<div className="grid gap-2 sm:grid-cols-3">
|
|
215
|
+
<label className="block">
|
|
216
|
+
<span className="text-[10px] font-medium text-muted">Id</span>
|
|
217
|
+
<input className={fieldClass} value={id} onChange={(event) => setId(event.target.value)} />
|
|
218
|
+
</label>
|
|
219
|
+
<label className="block">
|
|
220
|
+
<span className="text-[10px] font-medium text-muted">Name</span>
|
|
221
|
+
<input className={fieldClass} value={name} onChange={(event) => setName(event.target.value)} />
|
|
222
|
+
</label>
|
|
223
|
+
<label className="block">
|
|
224
|
+
<span className="text-[10px] font-medium text-muted">Version</span>
|
|
225
|
+
<input className={fieldClass} value={version} onChange={(event) => setVersion(event.target.value)} />
|
|
226
|
+
</label>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div>
|
|
230
|
+
<span className="text-[10px] font-medium text-muted">Capabilities</span>
|
|
231
|
+
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
232
|
+
{apiVersion === 2 ? (
|
|
233
|
+
<p className="text-[10px] text-faint">Not needed — API v2 plugin has full access via the Cordis context.</p>
|
|
234
|
+
) : CAPABILITIES.map((capability) => {
|
|
235
|
+
const on = capabilities.includes(capability)
|
|
236
|
+
return (
|
|
237
|
+
<button
|
|
238
|
+
key={capability}
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={() =>
|
|
241
|
+
setCapabilities((current) =>
|
|
242
|
+
on ? current.filter((entry) => entry !== capability) : [...current, capability],
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
className={`rounded-full border px-2 py-1 font-mono text-[9.5px] ${
|
|
246
|
+
on ? 'border-accent/40 bg-accent-soft text-ink' : 'border-line text-faint'
|
|
247
|
+
}`}
|
|
248
|
+
>
|
|
249
|
+
{capability}
|
|
250
|
+
</button>
|
|
251
|
+
)
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<label className="block">
|
|
257
|
+
<span className="text-[10px] font-medium text-muted">Network domains (optional, comma separated)</span>
|
|
258
|
+
<input
|
|
259
|
+
className={fieldClass}
|
|
260
|
+
value={domains}
|
|
261
|
+
placeholder="Leave empty — a plugin that needs no network declares none"
|
|
262
|
+
onChange={(event) => setDomains(event.target.value)}
|
|
263
|
+
/>
|
|
264
|
+
</label>
|
|
265
|
+
|
|
266
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={download}
|
|
270
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3 text-[10.5px] font-medium text-on-accent hover:bg-accent/90"
|
|
271
|
+
>
|
|
272
|
+
Download plugin package
|
|
273
|
+
</button>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={() => void install()}
|
|
277
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-line bg-panel px-3 text-[10.5px] font-medium text-ink hover:bg-raised"
|
|
278
|
+
>
|
|
279
|
+
Build and install
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</details>
|
|
284
|
+
)
|
|
285
|
+
}
|