@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,475 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentMode,
|
|
3
|
+
AppSettings,
|
|
4
|
+
ChatSession,
|
|
5
|
+
DaemonSnapshot,
|
|
6
|
+
DeployEvent,
|
|
7
|
+
DeployProviderId,
|
|
8
|
+
DeployRequest,
|
|
9
|
+
DeployResult,
|
|
10
|
+
ModelInfo,
|
|
11
|
+
ProviderId,
|
|
12
|
+
ProviderStatus,
|
|
13
|
+
SecretsSummary,
|
|
14
|
+
WorkspaceFile,
|
|
15
|
+
WorkspaceInfo,
|
|
16
|
+
} from '../types'
|
|
17
|
+
import type { PluginManifest, PluginCapability } from '../../shared/plugin-package.mjs'
|
|
18
|
+
|
|
19
|
+
export interface InstalledPlugin {
|
|
20
|
+
id: string
|
|
21
|
+
manifest: PluginManifest
|
|
22
|
+
manifestHash: string
|
|
23
|
+
installedAt: string
|
|
24
|
+
safe: boolean
|
|
25
|
+
enabled: boolean
|
|
26
|
+
grants: { capabilities: PluginCapability[]; domains: string[] }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class DaemonError extends Error {
|
|
30
|
+
status: number
|
|
31
|
+
code: string
|
|
32
|
+
|
|
33
|
+
constructor(message: string, status = 0, code = 'daemon_error') {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'DaemonError'
|
|
36
|
+
this.status = status
|
|
37
|
+
this.code = code
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let csrf = ''
|
|
42
|
+
let sessionPromise: Promise<void> | null = null
|
|
43
|
+
|
|
44
|
+
export function csrfToken() {
|
|
45
|
+
return csrf
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function parseError(response: Response): Promise<DaemonError> {
|
|
49
|
+
const raw = await response.text().catch(() => '')
|
|
50
|
+
if (raw) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw) as { error?: { message?: string; code?: string; status?: number } }
|
|
53
|
+
if (parsed.error?.message) {
|
|
54
|
+
return new DaemonError(
|
|
55
|
+
parsed.error.message,
|
|
56
|
+
parsed.error.status ?? response.status,
|
|
57
|
+
parsed.error.code ?? 'daemon_error',
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Fall through to the raw response.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return new DaemonError(
|
|
65
|
+
raw.trim().slice(0, 400) || `The local daemon returned ${response.status}.`,
|
|
66
|
+
response.status,
|
|
67
|
+
'daemon_error',
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function ensureSession(force = false): Promise<void> {
|
|
72
|
+
if (sessionPromise && !force) return sessionPromise
|
|
73
|
+
sessionPromise = (async () => {
|
|
74
|
+
let response: Response
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch('/api/session', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
credentials: 'same-origin',
|
|
80
|
+
})
|
|
81
|
+
} catch {
|
|
82
|
+
throw new DaemonError(
|
|
83
|
+
'OpenSol’s local daemon is not reachable. Start it with “npm start” in the OpenSol folder, then reload this page.',
|
|
84
|
+
0,
|
|
85
|
+
'daemon_unreachable',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) throw await parseError(response)
|
|
89
|
+
const data = (await response.json()) as { csrf?: string }
|
|
90
|
+
csrf = typeof data.csrf === 'string' ? data.csrf : ''
|
|
91
|
+
})().catch((error) => {
|
|
92
|
+
sessionPromise = null
|
|
93
|
+
throw error
|
|
94
|
+
})
|
|
95
|
+
return sessionPromise
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function api<T>(path: string, init: RequestInit = {}, retry = true): Promise<T> {
|
|
99
|
+
await ensureSession()
|
|
100
|
+
const method = (init.method ?? 'GET').toUpperCase()
|
|
101
|
+
const headers = new Headers(init.headers)
|
|
102
|
+
if (init.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
|
|
103
|
+
if (!['GET', 'HEAD'].includes(method)) headers.set('x-maxcode-csrf', csrf)
|
|
104
|
+
let response: Response
|
|
105
|
+
try {
|
|
106
|
+
response = await fetch(path, { ...init, method, headers, credentials: 'same-origin' })
|
|
107
|
+
} catch {
|
|
108
|
+
throw new DaemonError(
|
|
109
|
+
'Lost the connection to the local daemon. Check that it is still running.',
|
|
110
|
+
0,
|
|
111
|
+
'daemon_unreachable',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
if (response.status === 401 && retry) {
|
|
115
|
+
await ensureSession(true)
|
|
116
|
+
return api<T>(path, init, false)
|
|
117
|
+
}
|
|
118
|
+
if (!response.ok) throw await parseError(response)
|
|
119
|
+
if (response.status === 204) return undefined as T
|
|
120
|
+
const type = response.headers.get('content-type') ?? ''
|
|
121
|
+
if (!type.includes('application/json')) return (await response.text()) as T
|
|
122
|
+
return (await response.json()) as T
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function jsonBody(value: unknown): RequestInit {
|
|
126
|
+
return { method: 'POST', body: JSON.stringify(value) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getState(): Promise<DaemonSnapshot> {
|
|
130
|
+
return api<DaemonSnapshot>('/api/state')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getHealth(): Promise<{ ok: boolean; pid: number; port: number; dist: boolean }> {
|
|
134
|
+
return api('/api/health')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function listInstalledPlugins(): Promise<{ plugins: InstalledPlugin[] }> {
|
|
138
|
+
return api('/api/plugins')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function base64ToBytes(value: string): Uint8Array {
|
|
142
|
+
const binary = atob(value)
|
|
143
|
+
const bytes = new Uint8Array(binary.length)
|
|
144
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
|
|
145
|
+
return bytes
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function getInstalledPluginArchive(id: string): Promise<Uint8Array> {
|
|
149
|
+
const result = await api<{ archive: string }>(`/api/plugins/${encodeURIComponent(id)}/package`)
|
|
150
|
+
return base64ToBytes(result.archive)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
154
|
+
let binary = ''
|
|
155
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
156
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000))
|
|
157
|
+
}
|
|
158
|
+
return btoa(binary)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function installPluginArchive(archive: Uint8Array): Promise<{ plugin: InstalledPlugin }> {
|
|
162
|
+
return api('/api/plugins', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: JSON.stringify({ archive: bytesToBase64(archive) }),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function savePluginGrants(
|
|
169
|
+
id: string,
|
|
170
|
+
grants: { capabilities: PluginCapability[]; domains: string[] },
|
|
171
|
+
): Promise<{ plugin: InstalledPlugin }> {
|
|
172
|
+
return api(`/api/plugins/${encodeURIComponent(id)}/grants`, {
|
|
173
|
+
method: 'PUT',
|
|
174
|
+
body: JSON.stringify(grants),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function setPluginTrust(id: string, safe: boolean): Promise<{ plugin: InstalledPlugin }> {
|
|
179
|
+
return api(`/api/plugins/${encodeURIComponent(id)}/safe`, {
|
|
180
|
+
method: 'PUT',
|
|
181
|
+
body: JSON.stringify({ safe }),
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function setPluginActivation(id: string, enabled: boolean): Promise<{ plugin: InstalledPlugin }> {
|
|
186
|
+
return api(`/api/plugins/${encodeURIComponent(id)}/enabled`, {
|
|
187
|
+
method: 'PUT',
|
|
188
|
+
body: JSON.stringify({ enabled }),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function removeInstalledPlugin(id: string): Promise<{ id: string; removed: boolean }> {
|
|
193
|
+
return api(`/api/plugins/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function pluginNetworkRequest(id: string, request: {
|
|
197
|
+
url: string
|
|
198
|
+
method?: 'GET' | 'POST'
|
|
199
|
+
headers?: Record<string, string>
|
|
200
|
+
body?: string
|
|
201
|
+
}): Promise<{ status: number; contentType: string; body: string }> {
|
|
202
|
+
return api(`/api/plugins/${encodeURIComponent(id)}/network`, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
body: JSON.stringify(request),
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function patchSettings(patch: Partial<AppSettings>): Promise<{ settings: AppSettings }> {
|
|
209
|
+
return api('/api/settings', { method: 'PATCH', body: JSON.stringify(patch) })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function saveChats(chats: { sessions: ChatSession[]; activeId: string | null }) {
|
|
213
|
+
return api<{ sessions: number; activeId: string | null }>('/api/chats', {
|
|
214
|
+
method: 'PUT',
|
|
215
|
+
body: JSON.stringify(chats),
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function migrateLegacy(payload: { settings?: unknown; chats?: unknown }) {
|
|
220
|
+
return api<{
|
|
221
|
+
migratedSettings: boolean
|
|
222
|
+
migratedChats: number
|
|
223
|
+
migratedSecrets: boolean
|
|
224
|
+
state: DaemonSnapshot
|
|
225
|
+
}>('/api/migrate', jsonBody(payload))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function listProviders(): Promise<{ providers: ProviderStatus[] }> {
|
|
229
|
+
return api('/api/providers')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function fetchModels(provider: ProviderId) {
|
|
233
|
+
return api<{
|
|
234
|
+
provider: ProviderId
|
|
235
|
+
models: ModelInfo[]
|
|
236
|
+
source: 'live' | 'catalog'
|
|
237
|
+
fetchedAt: number
|
|
238
|
+
note: string
|
|
239
|
+
catalogError: string
|
|
240
|
+
}>(`/api/models?provider=${encodeURIComponent(provider)}`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function setProviderKey(provider: ProviderId, apiKey: string) {
|
|
244
|
+
if (!apiKey) return api<{ configured: boolean; hint: string }>(`/api/providers/${provider}/key`, { method: 'DELETE' })
|
|
245
|
+
return api<{ configured: boolean; hint: string }>(`/api/providers/${provider}/key`, {
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
body: JSON.stringify({ apiKey }),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function setSearchKey(apiKey: string) {
|
|
252
|
+
return api<{ configured: boolean; hint: string }>('/api/search/key', {
|
|
253
|
+
method: 'PUT',
|
|
254
|
+
body: JSON.stringify({ apiKey }),
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function setDeployCredentials(provider: DeployProviderId, fields: Record<string, string>) {
|
|
259
|
+
return api(`/api/deploy/credentials/${provider}`, {
|
|
260
|
+
method: 'PUT',
|
|
261
|
+
body: JSON.stringify(fields),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function resetScope(scope: 'history' | 'secrets' | 'settings' | 'all'): Promise<DaemonSnapshot> {
|
|
266
|
+
return api('/api/reset', jsonBody({ scope }))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function addWorkspace(path: string): Promise<{ workspace: WorkspaceInfo; entry: { path: string; name: string } }> {
|
|
270
|
+
return api('/api/workspaces', jsonBody({ path }))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function forgetWorkspace(): Promise<WorkspaceInfo> {
|
|
274
|
+
return api('/api/workspaces', { method: 'DELETE' })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function browseFolders(path: string) {
|
|
278
|
+
return api<{ path: string; parent: string; home: string; entries: Array<{ name: string; path: string }> }>(
|
|
279
|
+
`/api/workspaces/browse?path=${encodeURIComponent(path)}`,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function getWorkspaceTree(path = '', depth = 6) {
|
|
284
|
+
return api<{
|
|
285
|
+
root: string
|
|
286
|
+
path: string
|
|
287
|
+
entries: Array<{ name: string; path: string; type: 'file' | 'dir' | 'link'; size: number; mtime: number }>
|
|
288
|
+
truncated: boolean
|
|
289
|
+
workspace: WorkspaceInfo
|
|
290
|
+
}>(`/api/workspace/tree?depth=${depth}&path=${encodeURIComponent(path)}`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function readWorkspaceFile(path: string): Promise<WorkspaceFile> {
|
|
294
|
+
return api(`/api/workspace/file?path=${encodeURIComponent(path)}`)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function writeWorkspaceFile(path: string, content: string) {
|
|
298
|
+
return api<{ path: string; size: number; mtime: number; created: boolean; hash: string }>(
|
|
299
|
+
'/api/workspace/file',
|
|
300
|
+
{ method: 'PUT', body: JSON.stringify({ path, content }) },
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function createWorkspaceEntry(path: string, type: 'file' | 'dir') {
|
|
305
|
+
return api<{ path: string; type: 'file' | 'dir' }>('/api/workspace/file', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
body: JSON.stringify({ action: 'create', path, type }),
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function renameWorkspaceEntry(from: string, to: string) {
|
|
312
|
+
return api<{ from: string; to: string }>('/api/workspace/file', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({ action: 'rename', from, to }),
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function deleteWorkspaceEntry(path: string) {
|
|
319
|
+
return api<{ path: string; type: 'file' | 'dir' }>('/api/workspace/file', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
body: JSON.stringify({ action: 'delete', path }),
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function gitSummary() {
|
|
326
|
+
return api<{
|
|
327
|
+
available: boolean
|
|
328
|
+
root: string
|
|
329
|
+
branch: string
|
|
330
|
+
entries: Array<{ status: string; path: string }>
|
|
331
|
+
error: string
|
|
332
|
+
}>('/api/git')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface DaemonToolResult {
|
|
336
|
+
name: string
|
|
337
|
+
result: unknown
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function executeDaemonTool(
|
|
341
|
+
name: string,
|
|
342
|
+
args: Record<string, unknown>,
|
|
343
|
+
mode: AgentMode,
|
|
344
|
+
signal?: AbortSignal,
|
|
345
|
+
/** Groups file edits of one turn so they can be undone together. */
|
|
346
|
+
checkpoint?: string,
|
|
347
|
+
) {
|
|
348
|
+
return api<DaemonToolResult>(
|
|
349
|
+
'/api/tools/execute',
|
|
350
|
+
{ ...jsonBody({ name, args, mode, checkpoint }), signal },
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function getPreviewToken() {
|
|
355
|
+
return api<{ token: string }>('/api/preview/token')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function getProjectInstructions() {
|
|
359
|
+
return api<{ file: string; content: string; truncated: boolean }>('/api/workspace/instructions')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface CheckpointSummary {
|
|
363
|
+
id: string
|
|
364
|
+
createdAt: number
|
|
365
|
+
files: Array<{ path: string; existed: boolean; skipped: boolean }>
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function getCheckpoint(id: string) {
|
|
369
|
+
return api<CheckpointSummary>(`/api/checkpoints/${encodeURIComponent(id)}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function restoreCheckpoint(id: string) {
|
|
373
|
+
return api<{ id: string; restored: string[]; removed: string[]; skipped: string[] }>(
|
|
374
|
+
`/api/checkpoints/${encodeURIComponent(id)}/restore`,
|
|
375
|
+
{ method: 'POST', body: JSON.stringify({}) },
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function runGit(action: string, args: Record<string, unknown> = {}, mode: AgentMode = 'build') {
|
|
380
|
+
return api<Record<string, unknown>>('/api/git', jsonBody({ action, args, mode }))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function requestLLM(
|
|
384
|
+
provider: ProviderId,
|
|
385
|
+
kind: string,
|
|
386
|
+
model: string,
|
|
387
|
+
body: Record<string, unknown>,
|
|
388
|
+
signal?: AbortSignal,
|
|
389
|
+
retry = true,
|
|
390
|
+
): Promise<Response> {
|
|
391
|
+
await ensureSession()
|
|
392
|
+
let response: Response
|
|
393
|
+
try {
|
|
394
|
+
response = await fetch('/api/llm', {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: {
|
|
397
|
+
'Content-Type': 'application/json',
|
|
398
|
+
'x-maxcode-csrf': csrf,
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({ provider, kind, model, body }),
|
|
401
|
+
signal,
|
|
402
|
+
credentials: 'same-origin',
|
|
403
|
+
})
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error instanceof DOMException && error.name === 'AbortError') throw error
|
|
406
|
+
throw new DaemonError('Lost the connection to the local daemon while calling the model.', 0, 'daemon_unreachable')
|
|
407
|
+
}
|
|
408
|
+
// The daemon forgets sessions when it restarts; get a fresh one and retry once.
|
|
409
|
+
if (response.status === 401 && retry) {
|
|
410
|
+
await ensureSession(true)
|
|
411
|
+
return requestLLM(provider, kind, model, body, signal, false)
|
|
412
|
+
}
|
|
413
|
+
if (!response.ok) throw await parseError(response)
|
|
414
|
+
return response
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function* streamDeploy(
|
|
418
|
+
request: DeployRequest,
|
|
419
|
+
signal?: AbortSignal,
|
|
420
|
+
): AsyncGenerator<DeployEvent> {
|
|
421
|
+
await ensureSession()
|
|
422
|
+
let response: Response
|
|
423
|
+
try {
|
|
424
|
+
response = await fetch('/api/deploy', {
|
|
425
|
+
method: 'POST',
|
|
426
|
+
headers: {
|
|
427
|
+
'Content-Type': 'application/json',
|
|
428
|
+
'x-maxcode-csrf': csrf,
|
|
429
|
+
},
|
|
430
|
+
body: JSON.stringify(request),
|
|
431
|
+
signal,
|
|
432
|
+
credentials: 'same-origin',
|
|
433
|
+
})
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error instanceof DOMException && error.name === 'AbortError') throw error
|
|
436
|
+
throw new DaemonError('Lost the connection to the local daemon while deploying.', 0, 'daemon_unreachable')
|
|
437
|
+
}
|
|
438
|
+
if (!response.ok) throw await parseError(response)
|
|
439
|
+
if (!response.body) throw new DaemonError('The deploy stream was empty.', 502, 'empty_stream')
|
|
440
|
+
const reader = response.body.getReader()
|
|
441
|
+
const decoder = new TextDecoder()
|
|
442
|
+
let buffer = ''
|
|
443
|
+
try {
|
|
444
|
+
while (true) {
|
|
445
|
+
const { done, value } = await reader.read()
|
|
446
|
+
if (done) break
|
|
447
|
+
buffer += decoder.decode(value, { stream: true })
|
|
448
|
+
let index = buffer.indexOf('\n')
|
|
449
|
+
while (index !== -1) {
|
|
450
|
+
const line = buffer.slice(0, index).trim()
|
|
451
|
+
buffer = buffer.slice(index + 1)
|
|
452
|
+
if (line) {
|
|
453
|
+
try {
|
|
454
|
+
yield JSON.parse(line) as DeployEvent
|
|
455
|
+
} catch {
|
|
456
|
+
yield { type: 'log', stream: 'meta', text: line }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
index = buffer.indexOf('\n')
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const tail = buffer.trim()
|
|
463
|
+
if (tail) {
|
|
464
|
+
try {
|
|
465
|
+
yield JSON.parse(tail) as DeployEvent
|
|
466
|
+
} catch {
|
|
467
|
+
yield { type: 'log', stream: 'meta', text: tail }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} finally {
|
|
471
|
+
reader.releaseLock()
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export type { DeployResult, SecretsSummary }
|
package/src/lib/hooks.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useClickOutside<T extends HTMLElement>(
|
|
4
|
+
ref: RefObject<T | null>,
|
|
5
|
+
handler: () => void,
|
|
6
|
+
active = true,
|
|
7
|
+
ignoreRef?: RefObject<HTMLElement | null>,
|
|
8
|
+
) {
|
|
9
|
+
const saved = useRef(handler)
|
|
10
|
+
saved.current = handler
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!active) return
|
|
14
|
+
const onPointer = (event: PointerEvent) => {
|
|
15
|
+
const node = ref.current
|
|
16
|
+
if (!node) return
|
|
17
|
+
const target = event.target
|
|
18
|
+
if (!(target instanceof Node)) return
|
|
19
|
+
if (node.contains(target)) return
|
|
20
|
+
if (ignoreRef?.current?.contains(target)) return
|
|
21
|
+
saved.current()
|
|
22
|
+
}
|
|
23
|
+
const onKey = (event: KeyboardEvent) => {
|
|
24
|
+
if (event.key === 'Escape') saved.current()
|
|
25
|
+
}
|
|
26
|
+
document.addEventListener('pointerdown', onPointer, true)
|
|
27
|
+
document.addEventListener('keydown', onKey)
|
|
28
|
+
return () => {
|
|
29
|
+
document.removeEventListener('pointerdown', onPointer, true)
|
|
30
|
+
document.removeEventListener('keydown', onKey)
|
|
31
|
+
}
|
|
32
|
+
}, [active, ref, ignoreRef])
|
|
33
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AppSettings, ToolName, ToolSetting } from '../types'
|
|
2
|
+
|
|
3
|
+
export type PermissionMode = 'ask' | 'accept-edits' | 'full'
|
|
4
|
+
|
|
5
|
+
export const PERMISSION_MODES: Array<{ value: PermissionMode; label: string; description: string }> = [
|
|
6
|
+
{
|
|
7
|
+
value: 'ask',
|
|
8
|
+
label: 'Ask first',
|
|
9
|
+
description: 'Confirm shell commands, edits and git changes before they run.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
value: 'accept-edits',
|
|
13
|
+
label: 'Accept edits',
|
|
14
|
+
description: 'Apply file edits automatically; still confirm shell and git.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
value: 'full',
|
|
18
|
+
label: 'Full access',
|
|
19
|
+
description: 'Run every enabled tool without asking. Use only in trusted folders.',
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const WRITE_TOOLS: ToolName[] = ['write_file', 'edit_file', 'rewrite_file']
|
|
24
|
+
const GUARDED_TOOLS: ToolName[] = ['shell', 'git', 'subagent']
|
|
25
|
+
|
|
26
|
+
export function permissionMode(tools: AppSettings['tools']): PermissionMode {
|
|
27
|
+
const writesAuto = WRITE_TOOLS.every((tool) => tools[tool]?.approval === 'auto')
|
|
28
|
+
const guardedAuto = GUARDED_TOOLS.every((tool) => tools[tool]?.approval === 'auto')
|
|
29
|
+
if (writesAuto && guardedAuto) return 'full'
|
|
30
|
+
if (writesAuto) return 'accept-edits'
|
|
31
|
+
return 'ask'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function permissionTools(
|
|
35
|
+
tools: AppSettings['tools'],
|
|
36
|
+
mode: PermissionMode,
|
|
37
|
+
): Record<ToolName, ToolSetting> {
|
|
38
|
+
const next = {} as Record<ToolName, ToolSetting>
|
|
39
|
+
for (const key of Object.keys(tools) as ToolName[]) {
|
|
40
|
+
const mutating = WRITE_TOOLS.includes(key) || GUARDED_TOOLS.includes(key)
|
|
41
|
+
const approval: ToolSetting['approval'] =
|
|
42
|
+
mode === 'full'
|
|
43
|
+
? 'auto'
|
|
44
|
+
: mode === 'accept-edits'
|
|
45
|
+
? WRITE_TOOLS.includes(key)
|
|
46
|
+
? 'auto'
|
|
47
|
+
: GUARDED_TOOLS.includes(key)
|
|
48
|
+
? 'ask'
|
|
49
|
+
: tools[key].approval
|
|
50
|
+
: mutating
|
|
51
|
+
? 'ask'
|
|
52
|
+
: tools[key].approval
|
|
53
|
+
next[key] = { ...tools[key], approval }
|
|
54
|
+
}
|
|
55
|
+
return next
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function permissionLabel(mode: PermissionMode): string {
|
|
59
|
+
return PERMISSION_MODES.find((entry) => entry.value === mode)?.label ?? 'Ask first'
|
|
60
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { pixleMood, PIXLE_LABELS } from './pixlesol.ts'
|
|
4
|
+
|
|
5
|
+
const user = { id: 'u', role: 'user', content: 'hi', createdAt: 0 }
|
|
6
|
+
const assistant = (patch) => ({
|
|
7
|
+
id: 'a',
|
|
8
|
+
role: 'assistant',
|
|
9
|
+
content: '',
|
|
10
|
+
reasoning: '',
|
|
11
|
+
toolCalls: [],
|
|
12
|
+
round: 0,
|
|
13
|
+
createdAt: 0,
|
|
14
|
+
...patch,
|
|
15
|
+
})
|
|
16
|
+
const tool = (status) => ({
|
|
17
|
+
id: 't',
|
|
18
|
+
role: 'tool',
|
|
19
|
+
call: { id: 'c', name: 'read_file', args: {} },
|
|
20
|
+
status,
|
|
21
|
+
round: 0,
|
|
22
|
+
createdAt: 0,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('no messages means PixleSol waves', () => {
|
|
26
|
+
assert.equal(pixleMood([], false), 'wave')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('reasoning while streaming is thinking', () => {
|
|
30
|
+
assert.equal(pixleMood([user, assistant({ reasoning: 'hmm' })], true), 'thinking')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('streaming with content is typing', () => {
|
|
34
|
+
assert.equal(pixleMood([user, assistant({ reasoning: 'hmm', content: 'so far' })], true), 'typing')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('an awaiting tool needs approval, a failed tool is an error', () => {
|
|
38
|
+
assert.equal(pixleMood([user, tool('awaiting')], true), 'alert')
|
|
39
|
+
assert.equal(pixleMood([user, tool('error')], false), 'error')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('a finished answer is happy, a stopped answer is sleepy', () => {
|
|
43
|
+
assert.equal(pixleMood([user, assistant({ content: 'done' })], false), 'happy')
|
|
44
|
+
assert.equal(pixleMood([user, assistant({ stopped: true })], false), 'sleepy')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('every mood has a label', () => {
|
|
48
|
+
for (const mood of Object.keys(PIXLE_LABELS)) {
|
|
49
|
+
assert.ok(PIXLE_LABELS[mood])
|
|
50
|
+
}
|
|
51
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Message } from '../types'
|
|
2
|
+
|
|
3
|
+
export type PixleMood =
|
|
4
|
+
| 'wave'
|
|
5
|
+
| 'idle'
|
|
6
|
+
| 'thinking'
|
|
7
|
+
| 'working'
|
|
8
|
+
| 'typing'
|
|
9
|
+
| 'happy'
|
|
10
|
+
| 'alert'
|
|
11
|
+
| 'error'
|
|
12
|
+
| 'sleepy'
|
|
13
|
+
| 'party'
|
|
14
|
+
|
|
15
|
+
export const PIXLE_LABELS: Record<PixleMood, string> = {
|
|
16
|
+
wave: 'Ready when you are',
|
|
17
|
+
idle: 'Standing by',
|
|
18
|
+
thinking: 'Thinking it through…',
|
|
19
|
+
working: 'Running tools…',
|
|
20
|
+
typing: 'Writing the answer…',
|
|
21
|
+
happy: 'Answer ready',
|
|
22
|
+
alert: 'Needs your approval',
|
|
23
|
+
error: 'Hit a snag',
|
|
24
|
+
sleepy: 'Waiting for the next turn',
|
|
25
|
+
party: 'All steps finished',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pick PixleSol's face from the tail of the conversation. Presentation only —
|
|
30
|
+
* nothing here mutates a session.
|
|
31
|
+
*/
|
|
32
|
+
export function pixleMood(messages: Message[], streaming: boolean): PixleMood {
|
|
33
|
+
if (messages.length === 0) return 'wave'
|
|
34
|
+
const last = messages[messages.length - 1]
|
|
35
|
+
|
|
36
|
+
if (last.role === 'tool') {
|
|
37
|
+
if (last.status === 'awaiting') return 'alert'
|
|
38
|
+
if (last.status === 'error') return 'error'
|
|
39
|
+
if (streaming || last.status === 'running') return 'working'
|
|
40
|
+
return 'party'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (last.role === 'assistant') {
|
|
44
|
+
if (last.error) return 'error'
|
|
45
|
+
if (streaming) {
|
|
46
|
+
if (last.reasoning.trim() && !last.content.trim()) return 'thinking'
|
|
47
|
+
return 'typing'
|
|
48
|
+
}
|
|
49
|
+
if (last.stopped) return 'sleepy'
|
|
50
|
+
if (last.content.trim()) return 'happy'
|
|
51
|
+
return 'idle'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return streaming ? 'working' : 'idle'
|
|
55
|
+
}
|