@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
package/src/lib/boot.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import type { DaemonSnapshot } from '../types'
|
|
3
|
+
import { ensureSession, getState, migrateLegacy } from './daemon'
|
|
4
|
+
import { enableChatPersistence, useChats } from '../store/chats'
|
|
5
|
+
import { enableSettingsPersistence, useSettings } from '../store/settings'
|
|
6
|
+
import { useWorkspace } from '../store/workspace'
|
|
7
|
+
|
|
8
|
+
const LEGACY_KEYS = {
|
|
9
|
+
settings: 'maxcode-settings-v1',
|
|
10
|
+
chats: 'maxcode-chats-v1',
|
|
11
|
+
workspace: 'maxcode-workspace-v1',
|
|
12
|
+
} as const
|
|
13
|
+
|
|
14
|
+
type BootPhase = 'idle' | 'loading' | 'ready' | 'error'
|
|
15
|
+
|
|
16
|
+
interface BootState {
|
|
17
|
+
phase: BootPhase
|
|
18
|
+
error: string
|
|
19
|
+
startedAt: number
|
|
20
|
+
boot: (force?: boolean) => Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useBoot = create<BootState>()((set, get) => ({
|
|
24
|
+
phase: 'idle',
|
|
25
|
+
error: '',
|
|
26
|
+
startedAt: 0,
|
|
27
|
+
boot: async (force = false) => {
|
|
28
|
+
if (get().phase === 'loading') return
|
|
29
|
+
if (get().phase === 'ready' && !force) return
|
|
30
|
+
set({ phase: 'loading', error: '', startedAt: Date.now() })
|
|
31
|
+
try {
|
|
32
|
+
await ensureSession()
|
|
33
|
+
let snapshot = await getState()
|
|
34
|
+
const legacy = readLegacy()
|
|
35
|
+
if (legacy.found) {
|
|
36
|
+
if (!snapshot.migration?.migratedAt) {
|
|
37
|
+
const result = await migrateLegacy({ settings: legacy.settings, chats: legacy.chats })
|
|
38
|
+
snapshot = result.state
|
|
39
|
+
}
|
|
40
|
+
clearLegacy()
|
|
41
|
+
}
|
|
42
|
+
await applySnapshot(snapshot)
|
|
43
|
+
enableSettingsPersistence()
|
|
44
|
+
enableChatPersistence()
|
|
45
|
+
set({ phase: 'ready', error: '' })
|
|
46
|
+
} catch (error) {
|
|
47
|
+
set({
|
|
48
|
+
phase: 'error',
|
|
49
|
+
error:
|
|
50
|
+
error instanceof Error
|
|
51
|
+
? error.message
|
|
52
|
+
: 'OpenSol could not reach its local daemon.',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
export async function applySnapshot(snapshot: DaemonSnapshot): Promise<void> {
|
|
59
|
+
useSettings.getState().hydrate(snapshot)
|
|
60
|
+
useChats.getState().hydrate(snapshot.chats)
|
|
61
|
+
await useWorkspace.getState().hydrate(snapshot.workspace)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseLegacy(value: string | null): unknown {
|
|
65
|
+
if (!value) return undefined
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(value) as unknown
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readLegacy() {
|
|
74
|
+
if (typeof window === 'undefined') return { found: false, settings: undefined, chats: undefined }
|
|
75
|
+
let settings: unknown
|
|
76
|
+
let chats: unknown
|
|
77
|
+
let found = false
|
|
78
|
+
try {
|
|
79
|
+
settings = parseLegacy(window.localStorage.getItem(LEGACY_KEYS.settings))
|
|
80
|
+
chats = parseLegacy(window.localStorage.getItem(LEGACY_KEYS.chats))
|
|
81
|
+
found = Boolean(settings || chats || window.localStorage.getItem(LEGACY_KEYS.workspace))
|
|
82
|
+
} catch {
|
|
83
|
+
// Local storage can be unavailable; the daemon state is still usable.
|
|
84
|
+
}
|
|
85
|
+
return { found, settings, chats }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearLegacy() {
|
|
89
|
+
try {
|
|
90
|
+
for (const key of Object.values(LEGACY_KEYS)) window.localStorage.removeItem(key)
|
|
91
|
+
} catch {
|
|
92
|
+
// Nothing else to clean up.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context usage utilities.
|
|
3
|
+
*
|
|
4
|
+
* Estimates the number of tokens in the current session and computes the
|
|
5
|
+
* percentage of the active model's context window consumed. Because we count
|
|
6
|
+
* characters rather than sub-word tokens, the estimate will differ from the
|
|
7
|
+
* provider's actual token count; it is intentionally conservative (we divide
|
|
8
|
+
* by 3 instead of the usual ~4 chars/token) so the auto-compact threshold is
|
|
9
|
+
* hit slightly before the model actually runs out of context.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Message, ModelInfo } from '../types'
|
|
13
|
+
|
|
14
|
+
/** Chars-per-token ratio used for the estimate. Intentionally conservative. */
|
|
15
|
+
const CHARS_PER_TOKEN = 3
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rough token estimate for a single message. All roles are counted; tool
|
|
19
|
+
* results tend to be verbose so they get the same treatment as text.
|
|
20
|
+
*/
|
|
21
|
+
function estimateMessageTokens(message: Message): number {
|
|
22
|
+
if (message.role === 'user') {
|
|
23
|
+
const attachmentChars = (message.attachments ?? []).reduce((sum, a) => sum + (a.text?.length ?? 0), 0)
|
|
24
|
+
return Math.ceil((message.content.length + attachmentChars) / CHARS_PER_TOKEN) + 4
|
|
25
|
+
}
|
|
26
|
+
if (message.role === 'assistant') {
|
|
27
|
+
const toolChars = message.toolCalls.reduce(
|
|
28
|
+
(sum, call) => sum + JSON.stringify(call.args).length + call.name.length + 10,
|
|
29
|
+
0,
|
|
30
|
+
)
|
|
31
|
+
return Math.ceil((message.content.length + message.reasoning.length + toolChars) / CHARS_PER_TOKEN) + 4
|
|
32
|
+
}
|
|
33
|
+
// tool message
|
|
34
|
+
return Math.ceil(((message.result ?? '').length) / CHARS_PER_TOKEN) + 4
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Sum of estimated tokens across all messages in a session. */
|
|
38
|
+
export function estimateSessionTokens(messages: Message[]): number {
|
|
39
|
+
return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Context window ceiling for a given model (falls back to a safe default). */
|
|
43
|
+
export function contextLimit(model: ModelInfo | null): number {
|
|
44
|
+
return model?.context ?? 128_000
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a value in [0, 1] representing how full the context window is.
|
|
49
|
+
* Also accounts for the system prompt overhead (estimated at ~2 000 tokens).
|
|
50
|
+
*/
|
|
51
|
+
export function contextUsageRatio(messages: Message[], model: ModelInfo | null): number {
|
|
52
|
+
const SYSTEM_OVERHEAD = 2_000
|
|
53
|
+
const used = estimateSessionTokens(messages) + SYSTEM_OVERHEAD
|
|
54
|
+
const limit = contextLimit(model)
|
|
55
|
+
return Math.min(used / limit, 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns { used, limit, ratio, pct } for display and decision-making. */
|
|
59
|
+
export interface ContextUsage {
|
|
60
|
+
used: number
|
|
61
|
+
limit: number
|
|
62
|
+
ratio: number
|
|
63
|
+
pct: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getContextUsage(messages: Message[], model: ModelInfo | null): ContextUsage {
|
|
67
|
+
const SYSTEM_OVERHEAD = 2_000
|
|
68
|
+
const used = estimateSessionTokens(messages) + SYSTEM_OVERHEAD
|
|
69
|
+
const limit = contextLimit(model)
|
|
70
|
+
const ratio = Math.min(used / limit, 1)
|
|
71
|
+
return { used, limit, ratio, pct: Math.round(ratio * 100) }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The fraction at which auto-compact is triggered. */
|
|
75
|
+
export const AUTO_COMPACT_THRESHOLD = 0.85
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { groupConversationTurns } from './conversation.ts'
|
|
4
|
+
|
|
5
|
+
const user = (id) => ({ id, role: 'user', content: 'hi', createdAt: 0 })
|
|
6
|
+
const assistant = (id, content, toolCalls = 0) => ({
|
|
7
|
+
id,
|
|
8
|
+
role: 'assistant',
|
|
9
|
+
content,
|
|
10
|
+
reasoning: '',
|
|
11
|
+
toolCalls: Array.from({ length: toolCalls }, (_, index) => ({ id: `${id}-c${index}`, name: 'read_file', args: {} })),
|
|
12
|
+
round: 0,
|
|
13
|
+
createdAt: 0,
|
|
14
|
+
})
|
|
15
|
+
const tool = (id) => ({ id, role: 'tool', call: { id: `${id}-call`, name: 'read_file', args: {} }, status: 'done', round: 0, createdAt: 0 })
|
|
16
|
+
|
|
17
|
+
test('tool rounds with no answer collapse into one Used group', () => {
|
|
18
|
+
const turns = groupConversationTurns([
|
|
19
|
+
user('u1'),
|
|
20
|
+
assistant('a1', '', 1),
|
|
21
|
+
tool('t1'),
|
|
22
|
+
assistant('a2', '', 1),
|
|
23
|
+
tool('t2'),
|
|
24
|
+
assistant('a3', 'done'),
|
|
25
|
+
])
|
|
26
|
+
assert.equal(turns.length, 1)
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
turns[0].segments.map((segment) => segment.kind),
|
|
29
|
+
['tools', 'assistant'],
|
|
30
|
+
)
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
turns[0].segments[0].tools.map((entry) => entry.id),
|
|
33
|
+
['t1', 't2'],
|
|
34
|
+
)
|
|
35
|
+
assert.equal(turns[0].segments[1].message.id, 'a3')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('answering closes the Used group and a later tool round opens a new one', () => {
|
|
39
|
+
const turns = groupConversationTurns([
|
|
40
|
+
user('u1'),
|
|
41
|
+
assistant('a1', '', 1),
|
|
42
|
+
tool('t1'),
|
|
43
|
+
assistant('a2', 'first answer'),
|
|
44
|
+
assistant('a3', '', 1),
|
|
45
|
+
tool('t2'),
|
|
46
|
+
assistant('a4', 'second answer'),
|
|
47
|
+
])
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
turns[0].segments.map((segment) => segment.kind),
|
|
50
|
+
['tools', 'assistant', 'tools', 'assistant'],
|
|
51
|
+
)
|
|
52
|
+
assert.deepEqual(turns[0].segments[2].tools.map((entry) => entry.id), ['t2'])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('every user message starts a new turn and an unanswered run keeps its turn', () => {
|
|
56
|
+
const turns = groupConversationTurns([user('u1'), assistant('a1', 'one'), user('u2'), assistant('a2', '', 1), tool('t1')])
|
|
57
|
+
assert.deepEqual(turns.map((turn) => turn.id), ['u1', 'u2'])
|
|
58
|
+
assert.deepEqual(turns[1].segments.map((segment) => segment.kind), ['tools'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('a truly empty answer is kept so the empty-response notice still renders', () => {
|
|
62
|
+
const turns = groupConversationTurns([user('u1'), assistant('a1', '')])
|
|
63
|
+
assert.deepEqual(turns[0].segments.map((segment) => segment.kind), ['assistant'])
|
|
64
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AssistantMessage, Message, ToolMessage, UserMessage } from '../types'
|
|
2
|
+
|
|
3
|
+
export type TurnSegment =
|
|
4
|
+
| { kind: 'assistant'; message: AssistantMessage }
|
|
5
|
+
| { kind: 'tools'; tools: ToolMessage[] }
|
|
6
|
+
|
|
7
|
+
export interface ConversationTurn {
|
|
8
|
+
id: string
|
|
9
|
+
user?: UserMessage
|
|
10
|
+
segments: TurnSegment[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasBody(message: AssistantMessage) {
|
|
14
|
+
return Boolean(message.content.trim() || message.reasoning.trim() || message.error || message.stopped)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One turn per user message. A tool round with no answer yet stays inside a single
|
|
19
|
+
* `tools` segment, so consecutive rounds collapse into one Used group; the group
|
|
20
|
+
* closes as soon as the model answers, and the next tool round opens a new one.
|
|
21
|
+
*/
|
|
22
|
+
export function groupConversationTurns(messages: Message[]): ConversationTurn[] {
|
|
23
|
+
const turns: ConversationTurn[] = []
|
|
24
|
+
let turn: ConversationTurn | null = null
|
|
25
|
+
let pending: ToolMessage[] = []
|
|
26
|
+
|
|
27
|
+
const flushTools = () => {
|
|
28
|
+
if (pending.length > 0) turn?.segments.push({ kind: 'tools', tools: pending })
|
|
29
|
+
pending = []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const message of messages) {
|
|
33
|
+
if (message.role === 'tool') {
|
|
34
|
+
if (!turn) continue
|
|
35
|
+
pending.push(message)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (message.role === 'user') {
|
|
40
|
+
flushTools()
|
|
41
|
+
turn = { id: message.id, user: message, segments: [] }
|
|
42
|
+
turns.push(turn)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A round that only called tools is represented by those tool messages.
|
|
47
|
+
if (!hasBody(message) && message.toolCalls.length > 0) continue
|
|
48
|
+
|
|
49
|
+
if (!turn) {
|
|
50
|
+
turn = { id: message.id, segments: [] }
|
|
51
|
+
turns.push(turn)
|
|
52
|
+
}
|
|
53
|
+
flushTools()
|
|
54
|
+
turn.segments.push({ kind: 'assistant', message })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
flushTools()
|
|
58
|
+
return turns
|
|
59
|
+
}
|