@bibobonking/opensol 3.1.6 → 3.2.1
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/CHANGELOG.md +9 -7
- package/README.md +24 -20
- package/desktop/README.md +1 -1
- package/dist/assets/index-CyxlAgKM.js +271 -0
- package/dist/assets/index-DFeF90S8.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +3 -6
- package/server/README.md +15 -68
- package/server/daemon.mjs +6 -1
- package/server/lib/deploy.mjs +5 -10
- package/server/lib/shell.mjs +149 -0
- package/server/lib/shell.test.mjs +72 -0
- package/server/lib/tools.mjs +8 -44
- package/src/agent/adapters/anthropic.ts +2 -3
- package/src/agent/adapters/gemini.ts +2 -3
- package/src/agent/adapters/openai-responses.ts +2 -3
- package/src/agent/adapters/openai.ts +2 -3
- package/src/agent/adapters/shared.ts +3 -0
- package/src/components/ChatView.tsx +17 -54
- package/src/components/Composer.tsx +143 -135
- package/src/components/ContextBar.tsx +2 -2
- package/src/components/HomeDashboard.tsx +7 -4
- package/src/components/MessageItem.tsx +33 -1
- package/src/components/ModelDeck.tsx +5 -27
- package/src/components/SettingsModal.tsx +8 -0
- package/src/components/ToolCallCard.tsx +2 -2
- package/src/components/TrajectoryView.tsx +1 -1
- package/src/components/composer/PlusMenu.tsx +117 -0
- package/src/components/composer/Popover.tsx +109 -0
- package/src/components/composer/WorkspacePicker.tsx +102 -0
- package/src/core/plugins/tools/shell.ts +2 -2
- package/src/core/plugins/tools/subagent.ts +18 -8
- package/src/core/services/agent-loop.ts +45 -43
- package/src/core/services/compaction.ts +289 -22
- package/src/core/services/llm.ts +6 -2
- package/src/core/services/subagents.ts +191 -3
- package/src/core/services/system-prompt.ts +35 -8
- package/src/core/services/tools.ts +2 -1
- package/src/core/types.ts +5 -0
- package/src/lib/compact-plan.test.mjs +62 -0
- package/src/lib/compact-plan.ts +188 -0
- package/src/lib/subagent-types.ts +54 -0
- package/src/lib/tool-meta.ts +40 -0
- package/src/store/settings.ts +3 -0
- package/src/store/ui.ts +12 -0
- package/src/types.ts +26 -0
- package/tsconfig.tsbuildinfo +0 -1
package/server/lib/tools.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { currentShell, describeShell, killTree, planShellAllowed } from './shell.mjs'
|
|
2
3
|
import { createHash } from 'node:crypto'
|
|
3
4
|
import { lookup } from 'node:dns/promises'
|
|
4
5
|
import {
|
|
@@ -481,17 +482,13 @@ function runProcess(command, args, { cwd, env, timeoutMs = 120_000, maxOutput =
|
|
|
481
482
|
cwd,
|
|
482
483
|
env: { ...process.env, ...(env ?? {}) },
|
|
483
484
|
shell: false,
|
|
485
|
+
windowsHide: true,
|
|
484
486
|
detached: process.platform !== 'win32',
|
|
485
487
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
486
488
|
})
|
|
487
489
|
const terminate = () => {
|
|
488
490
|
killed = true
|
|
489
|
-
|
|
490
|
-
if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, 'SIGKILL')
|
|
491
|
-
else child.kill('SIGKILL')
|
|
492
|
-
} catch {
|
|
493
|
-
child.kill('SIGKILL')
|
|
494
|
-
}
|
|
491
|
+
killTree(child)
|
|
495
492
|
}
|
|
496
493
|
const timer = setTimeout(() => {
|
|
497
494
|
timedOut = true
|
|
@@ -541,46 +538,13 @@ function runProcess(command, args, { cwd, env, timeoutMs = 120_000, maxOutput =
|
|
|
541
538
|
})
|
|
542
539
|
}
|
|
543
540
|
|
|
544
|
-
function planShellAllowed(command) {
|
|
545
|
-
if (/[;&|><`$(){}[\]\n]/.test(command)) return false
|
|
546
|
-
const tokens = command.trim().split(/\s+/).filter(Boolean)
|
|
547
|
-
if (tokens.length === 0) return false
|
|
548
|
-
const head = tokens[0]
|
|
549
|
-
const safe = new Set([
|
|
550
|
-
'pwd',
|
|
551
|
-
'ls',
|
|
552
|
-
'cat',
|
|
553
|
-
'head',
|
|
554
|
-
'tail',
|
|
555
|
-
'wc',
|
|
556
|
-
'grep',
|
|
557
|
-
'rg',
|
|
558
|
-
'find',
|
|
559
|
-
'tree',
|
|
560
|
-
'file',
|
|
561
|
-
'stat',
|
|
562
|
-
'du',
|
|
563
|
-
'df',
|
|
564
|
-
'date',
|
|
565
|
-
'whoami',
|
|
566
|
-
'git',
|
|
567
|
-
])
|
|
568
|
-
if (!safe.has(head)) return false
|
|
569
|
-
if (head === 'git') {
|
|
570
|
-
const sub = tokens[1] ?? ''
|
|
571
|
-
if (!['status', 'diff', 'log', 'show', 'branch', 'remote', 'rev-parse', 'ls-files'].includes(sub)) {
|
|
572
|
-
return false
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return true
|
|
576
|
-
}
|
|
577
|
-
|
|
578
541
|
export async function runShell(command, { mode = 'build', timeoutMs = 120_000 } = {}) {
|
|
579
542
|
if (typeof command !== 'string' || !command.trim()) {
|
|
580
543
|
throw new HttpError(400, 'A command is required.', 'bad_command')
|
|
581
544
|
}
|
|
582
545
|
if (command.length > 20_000) throw new HttpError(400, 'That command is too long.', 'bad_command')
|
|
583
|
-
|
|
546
|
+
const shell = currentShell()
|
|
547
|
+
if (mode === 'plan' && !planShellAllowed(command, shell)) {
|
|
584
548
|
throw new HttpError(
|
|
585
549
|
403,
|
|
586
550
|
'Plan mode is read-only. Use read_file, list_files, grep or a read-only shell command.',
|
|
@@ -588,12 +552,12 @@ export async function runShell(command, { mode = 'build', timeoutMs = 120_000 }
|
|
|
588
552
|
)
|
|
589
553
|
}
|
|
590
554
|
const root = await workspaceRoot()
|
|
591
|
-
const result = await runProcess(
|
|
555
|
+
const result = await runProcess(shell.file, shell.args(command), {
|
|
592
556
|
cwd: root,
|
|
593
557
|
timeoutMs: Math.max(1000, Math.min(Number(timeoutMs) || 120_000, 600_000)),
|
|
594
|
-
env: { MAXCODE: '1', MAXCODE_WORKSPACE: root },
|
|
558
|
+
env: { OPENSOL: '1', OPENSOL_WORKSPACE: root, MAXCODE: '1', MAXCODE_WORKSPACE: root },
|
|
595
559
|
})
|
|
596
|
-
return { ...result, command, cwd: root }
|
|
560
|
+
return { ...result, command, cwd: root, shell: describeShell(shell) }
|
|
597
561
|
}
|
|
598
562
|
|
|
599
563
|
const GIT_ACTIONS = new Set(['status', 'diff', 'log', 'show', 'branch', 'add', 'commit', 'init', 'checkout'])
|
|
@@ -2,7 +2,6 @@ import type { Message, ToolCall } from '../../types'
|
|
|
2
2
|
import { requestLLM } from '../../lib/daemon'
|
|
3
3
|
import { budgetForEffort, effortValues, hasBudget, normalizeEffort } from '../../lib/reasoning'
|
|
4
4
|
import { readSSE } from '../../lib/sse'
|
|
5
|
-
import { getToolSchemas } from '../tools'
|
|
6
5
|
import {
|
|
7
6
|
assistantHistory,
|
|
8
7
|
imageAttachments,
|
|
@@ -73,7 +72,7 @@ function buildMessages(history: Message[]): AnthropicMessage[] {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
export async function streamAnthropic(request: AdapterRequest): Promise<AdapterResult> {
|
|
76
|
-
const { settings, model, history, system,
|
|
75
|
+
const { settings, model, history, system, signal, handlers } = request
|
|
77
76
|
const modelId = settings.model.trim()
|
|
78
77
|
if (!modelId) throw new Error('Choose a model before sending a message.')
|
|
79
78
|
|
|
@@ -91,7 +90,7 @@ export async function streamAnthropic(request: AdapterRequest): Promise<AdapterR
|
|
|
91
90
|
if (budget < 1024) budget = 0
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
const tools =
|
|
93
|
+
const tools = request.tools.map((tool) => ({
|
|
95
94
|
name: tool.name,
|
|
96
95
|
description: tool.description,
|
|
97
96
|
input_schema: tool.parameters,
|
|
@@ -2,7 +2,6 @@ import type { Message, ToolCall } from '../../types'
|
|
|
2
2
|
import { requestLLM } from '../../lib/daemon'
|
|
3
3
|
import { budgetForEffort, effortValues, hasBudget, normalizeEffort, hasToggle } from '../../lib/reasoning'
|
|
4
4
|
import { readSSE } from '../../lib/sse'
|
|
5
|
-
import { getToolSchemas } from '../tools'
|
|
6
5
|
import {
|
|
7
6
|
assistantHistory,
|
|
8
7
|
imageAttachments,
|
|
@@ -83,7 +82,7 @@ function cleanSchema(value: unknown): unknown {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
export async function streamGemini(request: AdapterRequest): Promise<AdapterResult> {
|
|
86
|
-
const { settings, model, history, system,
|
|
85
|
+
const { settings, model, history, system, signal, handlers } = request
|
|
87
86
|
const modelId = settings.model.trim()
|
|
88
87
|
if (!modelId) throw new Error('Choose a model before sending a message.')
|
|
89
88
|
|
|
@@ -97,7 +96,7 @@ export async function streamGemini(request: AdapterRequest): Promise<AdapterResu
|
|
|
97
96
|
if (model?.maxOutput) maxOutputTokens = Math.min(maxOutputTokens, model.maxOutput)
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
const tools =
|
|
99
|
+
const tools = request.tools
|
|
101
100
|
|
|
102
101
|
const generationConfig: Record<string, unknown> = {
|
|
103
102
|
maxOutputTokens,
|
|
@@ -2,7 +2,6 @@ import type { Message, ToolCall } from '../../types'
|
|
|
2
2
|
import { requestLLM } from '../../lib/daemon'
|
|
3
3
|
import { effortValues, normalizeEffort } from '../../lib/reasoning'
|
|
4
4
|
import { readSSE } from '../../lib/sse'
|
|
5
|
-
import { getToolSchemas } from '../tools'
|
|
6
5
|
import {
|
|
7
6
|
assistantHistory,
|
|
8
7
|
imageAttachments,
|
|
@@ -71,10 +70,10 @@ function itemRecord(value: unknown): Record<string, unknown> {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
export async function streamOpenAIResponses(request: AdapterRequest): Promise<AdapterResult> {
|
|
74
|
-
const { settings, model, history, system,
|
|
73
|
+
const { settings, model, history, system, signal, handlers } = request
|
|
75
74
|
const modelId = settings.model.trim()
|
|
76
75
|
if (!modelId) throw new Error('Choose a model before sending a message.')
|
|
77
|
-
const tools =
|
|
76
|
+
const tools = request.tools
|
|
78
77
|
const availableTools = new Set<string>(tools.map((tool) => tool.name))
|
|
79
78
|
|
|
80
79
|
const body: Record<string, unknown> = {
|
|
@@ -2,7 +2,6 @@ import type { Message, ToolCall } from '../../types'
|
|
|
2
2
|
import { requestLLM } from '../../lib/daemon'
|
|
3
3
|
import { effortValues, normalizeEffort } from '../../lib/reasoning'
|
|
4
4
|
import { readSSE } from '../../lib/sse'
|
|
5
|
-
import { getToolSchemas } from '../tools'
|
|
6
5
|
import {
|
|
7
6
|
assistantHistory,
|
|
8
7
|
imageAttachments,
|
|
@@ -69,10 +68,10 @@ function usesCompletionTokens(provider: string, modelId: string) {
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
export async function streamOpenAI(request: AdapterRequest): Promise<AdapterResult> {
|
|
72
|
-
const { settings, model, history, system,
|
|
71
|
+
const { settings, model, history, system, signal, handlers } = request
|
|
73
72
|
const modelId = settings.model.trim()
|
|
74
73
|
if (!modelId) throw new Error('Choose a model before sending a message.')
|
|
75
|
-
const tools =
|
|
74
|
+
const tools = request.tools
|
|
76
75
|
const availableTools = new Set<string>(tools.map((tool) => tool.name))
|
|
77
76
|
const modelTools = tools.map((tool) => ({
|
|
78
77
|
type: 'function',
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ToolCall,
|
|
9
9
|
Usage,
|
|
10
10
|
} from '../../types'
|
|
11
|
+
import type { ToolSchema } from '../../core/types'
|
|
11
12
|
|
|
12
13
|
export interface AdapterRequest {
|
|
13
14
|
settings: AppSettings
|
|
@@ -17,6 +18,8 @@ export interface AdapterRequest {
|
|
|
17
18
|
mode: AgentMode
|
|
18
19
|
signal: AbortSignal
|
|
19
20
|
handlers: StreamHandlers
|
|
21
|
+
/** Tools offered to the model. Empty for plain completions such as summaries. */
|
|
22
|
+
tools: ToolSchema[]
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface AdapterResult {
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { useSkills } from '../store/skills'
|
|
2
1
|
import { useEffect, useRef, useState } from 'react'
|
|
3
|
-
import { ArrowUp, Bug,
|
|
2
|
+
import { ArrowUp, Bug, Gamepad2, KeyRound, LayoutTemplate } from 'lucide-react'
|
|
4
3
|
import type { Attachment } from '../types'
|
|
5
4
|
import { createId, useChats } from '../store/chats'
|
|
6
5
|
import { activeModelId, hasCredentials, useSettings } from '../store/settings'
|
|
7
6
|
import { useActiveModel } from '../store/models'
|
|
8
7
|
import { useRuntime, useUi } from '../store/ui'
|
|
9
|
-
import { useWorkspace } from '../store/workspace'
|
|
10
8
|
import { getProviderName } from '../data/providers'
|
|
11
9
|
import { stopRun, runAgent } from '../agent'
|
|
10
|
+
import { kernel } from '../core/kernel'
|
|
12
11
|
import { getContextUsage } from '../lib/context'
|
|
13
12
|
import { pixleMood } from '../lib/pixlesol'
|
|
14
13
|
import { formatTokens } from '../lib/usage'
|
|
@@ -38,24 +37,6 @@ const STARTERS = [
|
|
|
38
37
|
prompt:
|
|
39
38
|
'Inspect this workspace for a real bug. Explain the root cause, make the smallest correct fix, and run the project checks.',
|
|
40
39
|
},
|
|
41
|
-
{
|
|
42
|
-
icon: FolderSearch,
|
|
43
|
-
label: 'Audit this folder',
|
|
44
|
-
prompt:
|
|
45
|
-
'Audit this workspace. Read the key files first, then list the three most important improvements with file references and a short plan.',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
icon: FlaskConical,
|
|
49
|
-
label: 'Write tests',
|
|
50
|
-
prompt:
|
|
51
|
-
'Find the least-tested, highest-risk module in this workspace, write focused tests for it, run them, and fix anything they reveal.',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
icon: Globe2,
|
|
55
|
-
label: 'Research with sources',
|
|
56
|
-
prompt:
|
|
57
|
-
'Search the web for the current release notes of the main framework in this project, then summarise what matters and cite the pages you read.',
|
|
58
|
-
},
|
|
59
40
|
]
|
|
60
41
|
|
|
61
42
|
export function ChatView() {
|
|
@@ -72,8 +53,6 @@ export function ChatView() {
|
|
|
72
53
|
const setWorkspaceOpen = useUi((state) => state.setWorkspaceOpen)
|
|
73
54
|
const setDeployOpen = useUi((state) => state.setDeployOpen)
|
|
74
55
|
const askConfirm = useUi((state) => state.askConfirm)
|
|
75
|
-
const workspace = useWorkspace((state) => state.info)
|
|
76
|
-
const git = useWorkspace((state) => state.git)
|
|
77
56
|
const activeModel = useActiveModel()
|
|
78
57
|
const [draft, setDraft] = useState('')
|
|
79
58
|
const [view, setView] = useState<ChatViewMode>('chat')
|
|
@@ -87,7 +66,6 @@ export function ChatView() {
|
|
|
87
66
|
const lastAssistantId = [...messages].reverse().find((message) => message.role === 'assistant')?.id
|
|
88
67
|
const connected = hasCredentials(settings)
|
|
89
68
|
const selectedModel = activeModelId(settings)
|
|
90
|
-
const activeSkills = useSkills((state) => state.skills.filter((skill) => skill.enabled).length)
|
|
91
69
|
const turns = groupConversationTurns(messages)
|
|
92
70
|
const context = getContextUsage(messages, activeModel)
|
|
93
71
|
const mood = pixleMood(messages, streaming)
|
|
@@ -224,6 +202,19 @@ export function ChatView() {
|
|
|
224
202
|
openSettings('models')
|
|
225
203
|
return true
|
|
226
204
|
}
|
|
205
|
+
case 'compact': {
|
|
206
|
+
const current = useChats.getState().sessions.find((item) => item.id === activeId)
|
|
207
|
+
if (!current || current.messages.length < 4) {
|
|
208
|
+
notify('There is not enough conversation to compact yet.', 'info')
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
if (streaming) {
|
|
212
|
+
notify('Wait for the current answer to finish, then run /compact.', 'info')
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
await kernel.compaction.compact(current.id, { focus: args.trim() || undefined, manual: true })
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
227
218
|
case 'clear': {
|
|
228
219
|
const current = useChats.getState().sessions.find((item) => item.id === activeId)
|
|
229
220
|
if (!current || current.messages.length === 0) return true
|
|
@@ -280,10 +271,9 @@ export function ChatView() {
|
|
|
280
271
|
hints
|
|
281
272
|
autoFocus
|
|
282
273
|
placeholder={connected ? 'How can I help you today?' : 'Add a provider key to start'}
|
|
283
|
-
pixleMood={connected ? 'idle' : 'sleepy'}
|
|
284
274
|
/>
|
|
285
|
-
<div className="stagger mt-
|
|
286
|
-
{STARTERS.
|
|
275
|
+
<div className="stagger mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
276
|
+
{STARTERS.map((starter) => (
|
|
287
277
|
<button
|
|
288
278
|
key={starter.label}
|
|
289
279
|
type="button"
|
|
@@ -297,17 +287,6 @@ export function ChatView() {
|
|
|
297
287
|
</button>
|
|
298
288
|
))}
|
|
299
289
|
</div>
|
|
300
|
-
<div className="mt-2.5 flex items-center justify-center gap-3 px-1 text-[11px] text-faint">
|
|
301
|
-
<span className="truncate">
|
|
302
|
-
{workspace.available ? workspace.name : 'No folder open'}
|
|
303
|
-
{workspace.available && git.available && git.branch ? ` · ${git.branch}` : ''}
|
|
304
|
-
</span>
|
|
305
|
-
<span aria-hidden="true">·</span>
|
|
306
|
-
<span className="shrink-0">
|
|
307
|
-
{activeSkills} skill{activeSkills === 1 ? '' : 's'} ·{' '}
|
|
308
|
-
{!connected ? 'Provider needed' : !selectedModel ? 'Model needed' : 'Ready'}
|
|
309
|
-
</span>
|
|
310
|
-
</div>
|
|
311
290
|
</div>
|
|
312
291
|
|
|
313
292
|
<div className="mt-10 animate-rise [animation-delay:420ms]">
|
|
@@ -379,22 +358,6 @@ export function ChatView() {
|
|
|
379
358
|
contextTitle={`Context: ~${formatTokens(context.used)} / ${formatTokens(context.limit)} tokens`}
|
|
380
359
|
pixleMood={mood}
|
|
381
360
|
/>
|
|
382
|
-
<div className="mt-2 flex items-center justify-between gap-3 px-2 text-[11px] text-faint">
|
|
383
|
-
<span className="flex items-center gap-1">
|
|
384
|
-
{settings.mode === 'plan' ? <ListChecks size={10} /> : <Sparkles size={10} />}
|
|
385
|
-
{settings.mode === 'plan'
|
|
386
|
-
? 'Plan mode · read-only tools'
|
|
387
|
-
: activeSkills > 0
|
|
388
|
-
? `${activeSkills} skill${activeSkills === 1 ? '' : 's'} loaded`
|
|
389
|
-
: 'Build mode · full tools'}
|
|
390
|
-
</span>
|
|
391
|
-
<span className="hidden sm:inline">
|
|
392
|
-
{workspace.available ? `Working in ${workspace.name}` : 'No workspace open'}
|
|
393
|
-
</span>
|
|
394
|
-
<span className="sm:hidden">
|
|
395
|
-
{settings.tools.shell.approval === 'ask' ? 'Shell asks first' : 'Shell auto-runs'}
|
|
396
|
-
</span>
|
|
397
|
-
</div>
|
|
398
361
|
</div>
|
|
399
362
|
</div>
|
|
400
363
|
</>
|