@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.
Files changed (127) hide show
  1. package/.github/workflows/build.yml +25 -0
  2. package/README.md +74 -2
  3. package/dist/assets/index-BmN7pBji.css +1 -0
  4. package/dist/assets/index-DgNDjkOz.js +301 -0
  5. package/dist/favicon.svg +15 -0
  6. package/dist/index.html +55 -0
  7. package/dist/preview-host.html +33 -0
  8. package/index.html +54 -0
  9. package/package.json +43 -4
  10. package/postcss.config.js +6 -0
  11. package/public/favicon.svg +15 -0
  12. package/public/preview-host.html +33 -0
  13. package/server/README.md +75 -0
  14. package/server/daemon.mjs +1082 -0
  15. package/server/lib/checkpoints.mjs +124 -0
  16. package/server/lib/checkpoints.test.mjs +110 -0
  17. package/server/lib/deploy.mjs +436 -0
  18. package/server/lib/http.mjs +237 -0
  19. package/server/lib/plugins.mjs +321 -0
  20. package/server/lib/plugins.test.mjs +140 -0
  21. package/server/lib/providers.mjs +594 -0
  22. package/server/lib/store.mjs +491 -0
  23. package/server/lib/tools.mjs +990 -0
  24. package/server/opensol.mjs +202 -0
  25. package/server/package.json +42 -0
  26. package/shared/plugin-builder.test.mjs +43 -0
  27. package/shared/plugin-package.d.mts +34 -0
  28. package/shared/plugin-package.mjs +197 -0
  29. package/shared/plugin-package.test.mjs +94 -0
  30. package/src/App.tsx +152 -0
  31. package/src/agent/adapters/anthropic.ts +233 -0
  32. package/src/agent/adapters/gemini.ts +225 -0
  33. package/src/agent/adapters/openai-responses.ts +215 -0
  34. package/src/agent/adapters/openai.ts +200 -0
  35. package/src/agent/adapters/shared.ts +96 -0
  36. package/src/agent/approval.ts +20 -0
  37. package/src/agent/compact.ts +234 -0
  38. package/src/agent/index.ts +36 -0
  39. package/src/agent/model.ts +20 -0
  40. package/src/agent/prompt.ts +107 -0
  41. package/src/agent/queue.test.mjs +111 -0
  42. package/src/agent/queue.ts +84 -0
  43. package/src/agent/subagent.ts +109 -0
  44. package/src/agent/tools.ts +541 -0
  45. package/src/components/ChatView.tsx +403 -0
  46. package/src/components/CommandPalette.tsx +416 -0
  47. package/src/components/Composer.tsx +678 -0
  48. package/src/components/ContextBar.tsx +58 -0
  49. package/src/components/DeployModal.tsx +512 -0
  50. package/src/components/ErrorBoundary.tsx +75 -0
  51. package/src/components/HomeDashboard.tsx +189 -0
  52. package/src/components/Logo.tsx +41 -0
  53. package/src/components/Markdown.tsx +232 -0
  54. package/src/components/MessageItem.tsx +655 -0
  55. package/src/components/ModelPicker.tsx +487 -0
  56. package/src/components/PixleSol.tsx +243 -0
  57. package/src/components/PluginBuilder.tsx +285 -0
  58. package/src/components/PluginManager.tsx +341 -0
  59. package/src/components/PreviewPane.tsx +166 -0
  60. package/src/components/SettingsModal.tsx +2052 -0
  61. package/src/components/Sidebar.tsx +504 -0
  62. package/src/components/Splash.tsx +104 -0
  63. package/src/components/ToolCallCard.tsx +177 -0
  64. package/src/components/TopBar.tsx +182 -0
  65. package/src/components/TrajectoryView.tsx +178 -0
  66. package/src/components/WorkspacePanel.tsx +710 -0
  67. package/src/components/ui.tsx +311 -0
  68. package/src/core/kernel.ts +45 -0
  69. package/src/core/plugins/tools/fs.ts +219 -0
  70. package/src/core/plugins/tools/git.ts +83 -0
  71. package/src/core/plugins/tools/grep.ts +98 -0
  72. package/src/core/plugins/tools/shell.ts +88 -0
  73. package/src/core/plugins/tools/subagent.ts +36 -0
  74. package/src/core/plugins/tools/web.ts +97 -0
  75. package/src/core/services/agent-loop.ts +417 -0
  76. package/src/core/services/approval.ts +66 -0
  77. package/src/core/services/compaction.ts +45 -0
  78. package/src/core/services/llm.ts +32 -0
  79. package/src/core/services/sessions.ts +47 -0
  80. package/src/core/services/subagents.ts +15 -0
  81. package/src/core/services/system-prompt.ts +177 -0
  82. package/src/core/services/tools.ts +135 -0
  83. package/src/core/services/ui.ts +109 -0
  84. package/src/core/types.ts +139 -0
  85. package/src/data/providers.ts +125 -0
  86. package/src/data/skills.ts +68 -0
  87. package/src/index.css +776 -0
  88. package/src/lib/boot.ts +94 -0
  89. package/src/lib/context.ts +75 -0
  90. package/src/lib/conversation.test.mjs +64 -0
  91. package/src/lib/conversation.ts +59 -0
  92. package/src/lib/daemon.ts +475 -0
  93. package/src/lib/hooks.ts +33 -0
  94. package/src/lib/permissions.ts +60 -0
  95. package/src/lib/pixlesol.test.mjs +51 -0
  96. package/src/lib/pixlesol.ts +55 -0
  97. package/src/lib/plugin-folder.test.mjs +145 -0
  98. package/src/lib/plugin-folder.ts +96 -0
  99. package/src/lib/reasoning.ts +87 -0
  100. package/src/lib/sse.ts +75 -0
  101. package/src/lib/themes.test.mjs +72 -0
  102. package/src/lib/themes.ts +52 -0
  103. package/src/lib/usage.test.mjs +72 -0
  104. package/src/lib/usage.ts +155 -0
  105. package/src/lib/utils.ts +73 -0
  106. package/src/main.tsx +16 -0
  107. package/src/plugins/api.test.mjs +77 -0
  108. package/src/plugins/api.ts +113 -0
  109. package/src/plugins/bridge.d.mts +1 -0
  110. package/src/plugins/bridge.mjs +26 -0
  111. package/src/plugins/bridge.test.mjs +17 -0
  112. package/src/plugins/index.ts +52 -0
  113. package/src/plugins/preview.test.mjs +18 -0
  114. package/src/plugins/preview.ts +34 -0
  115. package/src/plugins/runtime.ts +194 -0
  116. package/src/plugins/types.ts +40 -0
  117. package/src/store/chats.ts +278 -0
  118. package/src/store/models.ts +120 -0
  119. package/src/store/settings.ts +445 -0
  120. package/src/store/ui.ts +132 -0
  121. package/src/store/workspace.ts +299 -0
  122. package/src/types.ts +337 -0
  123. package/src/vite-env.d.ts +1 -0
  124. package/tailwind.config.js +83 -0
  125. package/tsconfig.json +22 -0
  126. package/tsconfig.tsbuildinfo +1 -0
  127. package/vite.config.ts +19 -0
@@ -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
+ }