@bibobonking/opensol 3.1.6 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +9 -7
  2. package/README.md +24 -20
  3. package/desktop/README.md +1 -1
  4. package/dist/assets/index-CyxlAgKM.js +271 -0
  5. package/dist/assets/index-DFeF90S8.css +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +3 -6
  8. package/server/README.md +15 -68
  9. package/server/daemon.mjs +6 -1
  10. package/server/lib/deploy.mjs +5 -10
  11. package/server/lib/shell.mjs +149 -0
  12. package/server/lib/shell.test.mjs +72 -0
  13. package/server/lib/tools.mjs +8 -44
  14. package/src/agent/adapters/anthropic.ts +2 -3
  15. package/src/agent/adapters/gemini.ts +2 -3
  16. package/src/agent/adapters/openai-responses.ts +2 -3
  17. package/src/agent/adapters/openai.ts +2 -3
  18. package/src/agent/adapters/shared.ts +3 -0
  19. package/src/components/ChatView.tsx +17 -54
  20. package/src/components/Composer.tsx +143 -135
  21. package/src/components/ContextBar.tsx +2 -2
  22. package/src/components/HomeDashboard.tsx +7 -4
  23. package/src/components/MessageItem.tsx +33 -1
  24. package/src/components/ModelDeck.tsx +5 -27
  25. package/src/components/SettingsModal.tsx +8 -0
  26. package/src/components/ToolCallCard.tsx +2 -2
  27. package/src/components/TrajectoryView.tsx +1 -1
  28. package/src/components/composer/PlusMenu.tsx +117 -0
  29. package/src/components/composer/Popover.tsx +109 -0
  30. package/src/components/composer/WorkspacePicker.tsx +102 -0
  31. package/src/core/plugins/tools/shell.ts +2 -2
  32. package/src/core/plugins/tools/subagent.ts +18 -8
  33. package/src/core/services/agent-loop.ts +45 -43
  34. package/src/core/services/compaction.ts +289 -22
  35. package/src/core/services/llm.ts +6 -2
  36. package/src/core/services/subagents.ts +191 -3
  37. package/src/core/services/system-prompt.ts +35 -8
  38. package/src/core/services/tools.ts +2 -1
  39. package/src/core/types.ts +5 -0
  40. package/src/lib/compact-plan.test.mjs +62 -0
  41. package/src/lib/compact-plan.ts +188 -0
  42. package/src/lib/subagent-types.ts +54 -0
  43. package/src/lib/tool-meta.ts +40 -0
  44. package/src/store/settings.ts +3 -0
  45. package/src/store/ui.ts +12 -0
  46. package/src/types.ts +26 -0
  47. package/shared/plugin-builder.test.mjs +0 -43
  48. package/shared/plugin-package.d.mts +0 -34
  49. package/shared/plugin-package.mjs +0 -197
  50. package/shared/plugin-package.test.mjs +0 -94
  51. package/tsconfig.tsbuildinfo +0 -1
@@ -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
- try {
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
- if (mode === 'plan' && !planShellAllowed(command)) {
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('/bin/bash', ['-lc', command], {
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, mode, signal, handlers } = request
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 = getToolSchemas(settings, mode).map((tool) => ({
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, mode, signal, handlers } = request
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 = getToolSchemas(settings, mode)
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, mode, signal, handlers } = request
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 = getToolSchemas(settings, mode)
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, mode, signal, handlers } = request
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 = getToolSchemas(settings, mode)
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, FlaskConical, FolderSearch, Gamepad2, Globe2, KeyRound, LayoutTemplate, ListChecks, Sparkles } from 'lucide-react'
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-5 grid grid-cols-2 gap-2 sm:grid-cols-3">
286
- {STARTERS.slice(0, 6).map((starter) => (
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
  </>