@bibobonking/opensol 0.0.0-stage → 3.0.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/.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/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,177 @@
|
|
|
1
|
+
import { Service, type Context } from 'cordis'
|
|
2
|
+
import type { AgentMode, AppSettings } from '../../types'
|
|
3
|
+
import { activeModelId } from '../../store/settings'
|
|
4
|
+
import { useWorkspace } from '../../store/workspace'
|
|
5
|
+
import type { PromptSection } from '../types'
|
|
6
|
+
|
|
7
|
+
function workspaceContext(): string {
|
|
8
|
+
const state = useWorkspace.getState()
|
|
9
|
+
const root = state.root || state.info.root
|
|
10
|
+
if (!root) {
|
|
11
|
+
return 'No folder is open yet. Ask the user to choose a workspace folder in the Workspace panel before using file, shell or git tools.'
|
|
12
|
+
}
|
|
13
|
+
const topLevel = state.entries
|
|
14
|
+
.filter((entry) => !entry.path.includes('/'))
|
|
15
|
+
.slice(0, 40)
|
|
16
|
+
.map((entry) => `${entry.path}${entry.type === 'dir' ? '/' : ''}`)
|
|
17
|
+
const git = state.git.available
|
|
18
|
+
? `${state.git.branch || 'detached HEAD'} · ${state.git.entries.length} changed file${state.git.entries.length === 1 ? '' : 's'}`
|
|
19
|
+
: 'not a git repository'
|
|
20
|
+
return [
|
|
21
|
+
`Root: ${root}`,
|
|
22
|
+
`Git: ${git}`,
|
|
23
|
+
topLevel.length > 0 ? `Top level: ${topLevel.join(', ')}` : 'The folder is empty.',
|
|
24
|
+
state.truncated ? 'The file tree was truncated; use list_files or grep for deeper paths.' : '',
|
|
25
|
+
]
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join('\n')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SystemPromptService extends Service {
|
|
31
|
+
static provide = 'systemPrompt'
|
|
32
|
+
private customSections = new Map<string, (settings: AppSettings, mode: AgentMode) => PromptSection | null>()
|
|
33
|
+
|
|
34
|
+
constructor(ctx: Context) {
|
|
35
|
+
super(ctx, 'systemPrompt')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
registerSection(
|
|
39
|
+
name: string,
|
|
40
|
+
generator: (settings: AppSettings, mode: AgentMode) => PromptSection | null,
|
|
41
|
+
): () => void {
|
|
42
|
+
this.customSections.set(name, generator)
|
|
43
|
+
return () => this.customSections.delete(name)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assemble(settings: AppSettings, mode: AgentMode = settings.mode): string {
|
|
47
|
+
const sections: PromptSection[] = []
|
|
48
|
+
|
|
49
|
+
// 1. Base instruction
|
|
50
|
+
if (settings.systemPrompt?.trim()) {
|
|
51
|
+
sections.push({ name: 'base', priority: 10, content: settings.systemPrompt.trim() })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Loaded skills
|
|
55
|
+
const skills = settings.skills.filter((skill) => skill.enabled)
|
|
56
|
+
if (skills.length > 0) {
|
|
57
|
+
sections.push({
|
|
58
|
+
name: 'skills',
|
|
59
|
+
priority: 20,
|
|
60
|
+
content: [
|
|
61
|
+
'## Loaded skills',
|
|
62
|
+
'Apply these skills when the task matches their purpose. They refine the working rules above; they never override safety, tool permissions or plan mode.',
|
|
63
|
+
...skills.map(
|
|
64
|
+
(skill) =>
|
|
65
|
+
`<skill name="${skill.name}">\nPurpose: ${skill.description}\n${skill.instructions.trim()}\n</skill>`,
|
|
66
|
+
),
|
|
67
|
+
].join('\n\n'),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Workspace
|
|
72
|
+
sections.push({
|
|
73
|
+
name: 'workspace',
|
|
74
|
+
priority: 30,
|
|
75
|
+
content: [
|
|
76
|
+
'## Workspace',
|
|
77
|
+
'You operate on the real folder below through the local OpenSol daemon. Tools return real output; never invent file contents, command results, URLs or deployments.',
|
|
78
|
+
workspaceContext(),
|
|
79
|
+
].join('\n\n'),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// 3b. Project instructions (MAXCODE.md / AGENTS.md / CLAUDE.md in the workspace)
|
|
83
|
+
const instructions = useWorkspace.getState().instructions
|
|
84
|
+
if (instructions.content.trim()) {
|
|
85
|
+
sections.push({
|
|
86
|
+
name: 'project-instructions',
|
|
87
|
+
priority: 35,
|
|
88
|
+
content: [
|
|
89
|
+
`## Project instructions (${instructions.file})`,
|
|
90
|
+
'The owner of this project wrote the notes below. Follow them for conventions, commands and style. They never override safety, tool permissions or plan mode.',
|
|
91
|
+
`<project-instructions file="${instructions.file}">\n${instructions.content.trim()}${instructions.truncated ? '\n… (file clipped)' : ''}\n</project-instructions>`,
|
|
92
|
+
].join('\n\n'),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. Mode
|
|
97
|
+
sections.push({
|
|
98
|
+
name: 'mode',
|
|
99
|
+
priority: 40,
|
|
100
|
+
content:
|
|
101
|
+
mode === 'plan'
|
|
102
|
+
? [
|
|
103
|
+
'## Mode: Plan (read-only)',
|
|
104
|
+
'Investigate and design. Do not modify anything.',
|
|
105
|
+
'- Allowed: read_file, list_files, grep, web_search, read_url, read-only shell commands and read-only git actions.',
|
|
106
|
+
'- Forbidden: write_file, edit_file, rewrite_file, git add/commit/init/checkout, and shell commands that change files, install packages or change system state. The daemon rejects these.',
|
|
107
|
+
'- Read the relevant source before proposing changes. Grep for call sites and inspect the real files.',
|
|
108
|
+
'- Finish with a concrete, ordered implementation plan naming files and functions. State assumptions and open questions plainly.',
|
|
109
|
+
'- Never pretend an edit was made or a check was run.',
|
|
110
|
+
].join('\n')
|
|
111
|
+
: [
|
|
112
|
+
'## Mode: Build',
|
|
113
|
+
'You may inspect and change the real workspace.',
|
|
114
|
+
'- Inspect before editing: list files, read the source, grep for call sites, check git status.',
|
|
115
|
+
'- Make the smallest coherent change. Do not reformat unrelated code, add speculative abstractions or invent APIs.',
|
|
116
|
+
'- After a mutation, inspect the result. Run the project’s real typecheck, lint, tests and build when they exist, and read the output.',
|
|
117
|
+
'- Never claim a check passed without running it. If a check cannot run, say why.',
|
|
118
|
+
'- Ask before destructive actions, force pushes, credential changes or anything that leaves the workspace.',
|
|
119
|
+
].join('\n'),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// 4b. Vibe coding
|
|
123
|
+
sections.push({
|
|
124
|
+
name: 'vibe-coding',
|
|
125
|
+
priority: 45,
|
|
126
|
+
content: [
|
|
127
|
+
'## Vibe coding',
|
|
128
|
+
'- For a game, demo, landing page or visual prototype that does not need a project folder, reply with ONE self-contained HTML file in a single ```html code block (inline CSS and JS). OpenSol renders that block as a live, sandboxed preview, so it must run with no build step and no calls to localhost. Load libraries only from https://cdnjs.cloudflare.com or https://cdn.jsdelivr.net with a pinned version.',
|
|
129
|
+
'- With a folder open in Build mode, write real files with the tools instead of pasting large code into the chat, then say how to run them.',
|
|
130
|
+
'- Deliver something that runs first, then improve it in small steps based on what the user sees. Keep explanations to a few sentences; the code is the answer.',
|
|
131
|
+
'- If something needs an API key, a server or an asset you cannot create, say so plainly instead of faking it.',
|
|
132
|
+
].join('\n'),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// 5. Session Context
|
|
136
|
+
sections.push({
|
|
137
|
+
name: 'session-context',
|
|
138
|
+
priority: 50,
|
|
139
|
+
content: [
|
|
140
|
+
'## Session context',
|
|
141
|
+
`- Model: ${activeModelId(settings) || 'unset'}`,
|
|
142
|
+
`- Date: ${new Date().toISOString().slice(0, 10)}`,
|
|
143
|
+
`- Mode: ${mode}`,
|
|
144
|
+
settings.webSearchEnabled
|
|
145
|
+
? '- Web search is enabled. Use it when facts may be current or when the user asks for sources.'
|
|
146
|
+
: '- Web search is turned off for this session.',
|
|
147
|
+
'- Use relative paths from the workspace root. Do not use /workspace paths.',
|
|
148
|
+
].join('\n'),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Custom sections from plugins
|
|
152
|
+
for (const generator of this.customSections.values()) {
|
|
153
|
+
const section = generator(settings, mode)
|
|
154
|
+
if (section && section.content) sections.push(section)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Cordis waterfall event for extension points
|
|
158
|
+
return this.ctx.events.waterfall('system-prompt/assemble', sections, mode, () => {
|
|
159
|
+
sections.sort((a, b) => a.priority - b.priority)
|
|
160
|
+
return sections.map((s) => s.content).filter(Boolean).join('\n\n')
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
buildSubagentPrompt(settings: AppSettings): string {
|
|
165
|
+
return [
|
|
166
|
+
this.assemble(settings, 'plan'),
|
|
167
|
+
[
|
|
168
|
+
'## Subagent',
|
|
169
|
+
'You are a focused read-only investigator. Work only on the assigned task.',
|
|
170
|
+
'- Inspect the real workspace with read-only file, grep, shell and web tools.',
|
|
171
|
+
'- Do not edit files, change git state or run commands that mutate the machine. The daemon enforces this.',
|
|
172
|
+
'- Be concise: return findings, exact file paths and line references, and any uncertainty.',
|
|
173
|
+
'- Do not repeat the task back. Do not invent evidence. If something cannot be verified, say so.',
|
|
174
|
+
].join('\n'),
|
|
175
|
+
].join('\n\n')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Service, type Context } from 'cordis'
|
|
2
|
+
import type { AgentMode, AppSettings, ToolName } from '../../types'
|
|
3
|
+
import type {
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
ToolExecutionContext,
|
|
6
|
+
ToolPreExecuteResult,
|
|
7
|
+
ToolResult,
|
|
8
|
+
ToolSchema,
|
|
9
|
+
} from '../types'
|
|
10
|
+
|
|
11
|
+
const READ_ONLY_TOOLS = new Set<ToolName>([
|
|
12
|
+
'read_file',
|
|
13
|
+
'list_files',
|
|
14
|
+
'grep',
|
|
15
|
+
'web_search',
|
|
16
|
+
'read_url',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
export function toolAllowedInMode(name: ToolName, mode: AgentMode): boolean {
|
|
20
|
+
if (mode === 'build') return true
|
|
21
|
+
return READ_ONLY_TOOLS.has(name) || name === 'shell' || name === 'git' || name === 'subagent'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ToolService extends Service {
|
|
25
|
+
static provide = 'tools'
|
|
26
|
+
private definitions = new Map<ToolName, ToolDefinition>()
|
|
27
|
+
|
|
28
|
+
constructor(ctx: Context) {
|
|
29
|
+
super(ctx, 'tools')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
register(definition: ToolDefinition): () => void {
|
|
33
|
+
this.definitions.set(definition.name, definition)
|
|
34
|
+
return () => {
|
|
35
|
+
if (this.definitions.get(definition.name) === definition) {
|
|
36
|
+
this.definitions.delete(definition.name)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get(name: ToolName): ToolDefinition | undefined {
|
|
42
|
+
return this.definitions.get(name)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
list(): ToolDefinition[] {
|
|
46
|
+
return [...this.definitions.values()]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
schemas(
|
|
50
|
+
settings: AppSettings,
|
|
51
|
+
mode: AgentMode = 'build',
|
|
52
|
+
{ subagent = false }: { subagent?: boolean } = {},
|
|
53
|
+
): ToolSchema[] {
|
|
54
|
+
const list: ToolSchema[] = []
|
|
55
|
+
for (const def of this.definitions.values()) {
|
|
56
|
+
if (def.name === 'subagent' && subagent) continue
|
|
57
|
+
if (!settings.tools[def.name]?.enabled) continue
|
|
58
|
+
const allowed = def.allowedInMode ? def.allowedInMode(mode) : toolAllowedInMode(def.name, mode)
|
|
59
|
+
if (!allowed) continue
|
|
60
|
+
if ((def.name === 'web_search' || def.name === 'read_url') && !settings.webSearchEnabled) continue
|
|
61
|
+
list.push({
|
|
62
|
+
name: def.name,
|
|
63
|
+
description: def.description,
|
|
64
|
+
parameters: def.parameters,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return list
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async execute(
|
|
71
|
+
name: ToolName,
|
|
72
|
+
args: Record<string, unknown>,
|
|
73
|
+
settings: AppSettings,
|
|
74
|
+
mode: AgentMode,
|
|
75
|
+
signal: AbortSignal,
|
|
76
|
+
callId?: string,
|
|
77
|
+
turnId?: string,
|
|
78
|
+
): Promise<ToolResult> {
|
|
79
|
+
const exec: ToolExecutionContext = { name, args, settings, mode, signal, callId, turnId }
|
|
80
|
+
|
|
81
|
+
// 1. tools/pre-execute waterfall (approval, permission gate, mode checks)
|
|
82
|
+
const preDecision: ToolPreExecuteResult = await this.ctx.events.waterfall(
|
|
83
|
+
'tools/pre-execute',
|
|
84
|
+
exec,
|
|
85
|
+
async () => {
|
|
86
|
+
if (!settings.tools[name]?.enabled) {
|
|
87
|
+
return { decision: 'deny', reason: 'This tool is disabled in Settings → Tools.' }
|
|
88
|
+
}
|
|
89
|
+
const def = this.definitions.get(name)
|
|
90
|
+
const allowed = def?.allowedInMode ? def.allowedInMode(mode) : toolAllowedInMode(name, mode)
|
|
91
|
+
if (!allowed) {
|
|
92
|
+
return {
|
|
93
|
+
decision: 'deny',
|
|
94
|
+
reason: 'Plan mode is read-only. Do not edit files or change git state. Finish with an implementation plan instead.',
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if ((name === 'web_search' || name === 'read_url') && !settings.webSearchEnabled) {
|
|
98
|
+
return { decision: 'deny', reason: 'Web access is turned off for this session.' }
|
|
99
|
+
}
|
|
100
|
+
return { decision: 'allow' }
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (preDecision.decision === 'deny') {
|
|
105
|
+
const deniedResult: ToolResult = { output: preDecision.reason ?? 'Tool call denied.', isError: true }
|
|
106
|
+
this.ctx.events.emit('tools/result', deniedResult, exec)
|
|
107
|
+
return deniedResult
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (signal.aborted) {
|
|
111
|
+
throw new DOMException('Aborted', 'AbortError')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. tools/execute waterfall
|
|
115
|
+
const result: ToolResult = await this.ctx.events.waterfall('tools/execute', exec, async () => {
|
|
116
|
+
const def = this.definitions.get(name)
|
|
117
|
+
if (!def) {
|
|
118
|
+
return { output: `Tool "${name}" is not registered.`, isError: true }
|
|
119
|
+
}
|
|
120
|
+
return await def.execute(args, exec)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// 3. tools/post-execute waterfall
|
|
124
|
+
const finalResult: ToolResult = await this.ctx.events.waterfall(
|
|
125
|
+
'tools/post-execute',
|
|
126
|
+
result,
|
|
127
|
+
exec,
|
|
128
|
+
async () => result,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
// 4. tools/result emit
|
|
132
|
+
this.ctx.events.emit('tools/result', finalResult, exec)
|
|
133
|
+
return finalResult
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Service, type Context } from 'cordis'
|
|
2
|
+
import { registerTheme as registerThemeInApp, unregisterTheme as unregisterThemeInApp } from '../../lib/themes'
|
|
3
|
+
|
|
4
|
+
export interface UiCommand {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
run: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UiPanel {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
render: (host: HTMLElement) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UiTheme {
|
|
17
|
+
id: string
|
|
18
|
+
label: string
|
|
19
|
+
palette: Record<string, string>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const STYLE_ID = (id: string) => `maxcode-plugin-theme-${id}`
|
|
23
|
+
|
|
24
|
+
function tokens(palette: Record<string, string>): Array<[string, string]> {
|
|
25
|
+
return Object.entries(palette).filter(
|
|
26
|
+
([token, value]) => /^[a-z][a-z0-9-]*$/.test(token) && /^[\d\s.]+$/.test(value),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isDarkPalette(palette: Record<string, string>): boolean {
|
|
31
|
+
const canvas = (palette.canvas ?? '').trim().split(/\s+/).map(Number)
|
|
32
|
+
const [r, g, b] = canvas.length === 3 ? canvas : [255, 255, 255]
|
|
33
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 110
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function themeCss(themeId: string, palette: Record<string, string>): string {
|
|
37
|
+
const entries = tokens(palette).map(([token, value]) => `--${token}:${value.trim()}`)
|
|
38
|
+
return entries.length === 0 ? '' : `[data-theme='${themeId}']{${entries.join(';')}}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function applyThemeStyles(id: string, css: string) {
|
|
42
|
+
if (typeof document === 'undefined') return
|
|
43
|
+
let node = document.getElementById(STYLE_ID(id)) as HTMLStyleElement | null
|
|
44
|
+
if (!node) {
|
|
45
|
+
node = document.createElement('style')
|
|
46
|
+
node.id = STYLE_ID(id)
|
|
47
|
+
document.head.append(node)
|
|
48
|
+
}
|
|
49
|
+
node.textContent = css
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeThemeStyles(id: string) {
|
|
53
|
+
if (typeof document === 'undefined') return
|
|
54
|
+
document.getElementById(STYLE_ID(id))?.remove()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class UiService extends Service {
|
|
58
|
+
static provide = 'ui'
|
|
59
|
+
private _commands = new Map<string, UiCommand>()
|
|
60
|
+
private _panels = new Map<string, UiPanel>()
|
|
61
|
+
private _themes = new Map<string, UiTheme>()
|
|
62
|
+
|
|
63
|
+
constructor(ctx: Context) {
|
|
64
|
+
super(ctx, 'ui')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
registerCommand(id: string, label: string, run: () => void): () => void {
|
|
68
|
+
const cmd = { id, label, run }
|
|
69
|
+
this._commands.set(id, cmd)
|
|
70
|
+
return () => {
|
|
71
|
+
if (this._commands.get(id) === cmd) this._commands.delete(id)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
registerPanel(id: string, label: string, render: (host: HTMLElement) => void): () => void {
|
|
76
|
+
const panel = { id, label, render }
|
|
77
|
+
this._panels.set(id, panel)
|
|
78
|
+
return () => {
|
|
79
|
+
if (this._panels.get(id) === panel) this._panels.delete(id)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
registerTheme(id: string, label: string, palette: Record<string, string>): () => void {
|
|
84
|
+
const theme = { id, label, palette }
|
|
85
|
+
this._themes.set(id, theme)
|
|
86
|
+
registerThemeInApp({ id, label, dark: isDarkPalette(palette) })
|
|
87
|
+
applyThemeStyles(id, themeCss(id, palette))
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
if (this._themes.get(id) === theme) {
|
|
91
|
+
this._themes.delete(id)
|
|
92
|
+
unregisterThemeInApp(id)
|
|
93
|
+
removeThemeStyles(id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
commands(): UiCommand[] {
|
|
99
|
+
return [...this._commands.values()]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
panels(): UiPanel[] {
|
|
103
|
+
return [...this._panels.values()]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
themes(): Array<{ id: string; label: string }> {
|
|
107
|
+
return [...this._themes.values()].map((t) => ({ id: t.id, label: t.label }))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentMode,
|
|
3
|
+
AppSettings,
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
Message,
|
|
6
|
+
ModelInfo,
|
|
7
|
+
ToolCall,
|
|
8
|
+
ToolMessage,
|
|
9
|
+
ToolName,
|
|
10
|
+
Usage,
|
|
11
|
+
} from '../types'
|
|
12
|
+
import type { AdapterResult } from '../agent/adapters/shared'
|
|
13
|
+
|
|
14
|
+
export interface ThinkingBlock {
|
|
15
|
+
text: string
|
|
16
|
+
signature?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface JsonSchema {
|
|
20
|
+
type: 'object'
|
|
21
|
+
properties: Record<string, unknown>
|
|
22
|
+
required?: string[]
|
|
23
|
+
additionalProperties?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ToolSchema {
|
|
27
|
+
name: ToolName
|
|
28
|
+
description: string
|
|
29
|
+
parameters: JsonSchema
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ToolResult {
|
|
33
|
+
output: string
|
|
34
|
+
isError: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ToolExecutionContext {
|
|
38
|
+
name: ToolName
|
|
39
|
+
args: Record<string, unknown>
|
|
40
|
+
settings: AppSettings
|
|
41
|
+
mode: AgentMode
|
|
42
|
+
signal: AbortSignal
|
|
43
|
+
callId?: string
|
|
44
|
+
/** The user message that started this turn; file edits are checkpointed under it. */
|
|
45
|
+
turnId?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ToolPreExecuteDecision = 'allow' | 'ask' | 'deny'
|
|
49
|
+
|
|
50
|
+
export interface ToolPreExecuteResult {
|
|
51
|
+
decision: ToolPreExecuteDecision
|
|
52
|
+
reason?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StepContext {
|
|
56
|
+
sessionId: string
|
|
57
|
+
round: number
|
|
58
|
+
history: Message[]
|
|
59
|
+
settings: AppSettings
|
|
60
|
+
mode: AgentMode
|
|
61
|
+
signal: AbortSignal
|
|
62
|
+
assistantId: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface StepResult {
|
|
66
|
+
sessionId: string
|
|
67
|
+
round: number
|
|
68
|
+
assistant: AssistantMessage
|
|
69
|
+
toolResults: ToolMessage[]
|
|
70
|
+
stopped?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface TurnResult {
|
|
74
|
+
sessionId: string
|
|
75
|
+
userMessageId: string
|
|
76
|
+
rounds: number
|
|
77
|
+
durationMs: number
|
|
78
|
+
stopped?: boolean
|
|
79
|
+
error?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ModelRequest {
|
|
83
|
+
settings: AppSettings
|
|
84
|
+
model: ModelInfo | null
|
|
85
|
+
history: Message[]
|
|
86
|
+
system: string
|
|
87
|
+
mode: AgentMode
|
|
88
|
+
signal: AbortSignal
|
|
89
|
+
handlers: {
|
|
90
|
+
onText: (delta: string) => void
|
|
91
|
+
onReasoning: (delta: string) => void
|
|
92
|
+
onThinkingBlocks?: (blocks: ThinkingBlock[]) => void
|
|
93
|
+
onToolCalls: (calls: ToolCall[]) => void
|
|
94
|
+
onUsage: (usage: Usage) => void
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PromptSection {
|
|
99
|
+
name: string
|
|
100
|
+
priority: number
|
|
101
|
+
content: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ToolDefinition {
|
|
105
|
+
name: ToolName
|
|
106
|
+
description: string
|
|
107
|
+
parameters: JsonSchema
|
|
108
|
+
allowedInMode?: (mode: AgentMode) => boolean
|
|
109
|
+
execute: (args: Record<string, unknown>, exec: ToolExecutionContext) => Promise<ToolResult>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
declare module 'cordis' {
|
|
113
|
+
interface Context {
|
|
114
|
+
sessions: import('./services/sessions').SessionService
|
|
115
|
+
systemPrompt: import('./services/system-prompt').SystemPromptService
|
|
116
|
+
tools: import('./services/tools').ToolService
|
|
117
|
+
llm: import('./services/llm').LlmService
|
|
118
|
+
approval: import('./services/approval').ApprovalService
|
|
119
|
+
compaction: import('./services/compaction').CompactionService
|
|
120
|
+
subagents: import('./services/subagents').SubagentService
|
|
121
|
+
agentLoop: import('./services/agent-loop').AgentLoopService
|
|
122
|
+
ui: import('./services/ui').UiService
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface Events {
|
|
126
|
+
'agent/pre-step': (step: StepContext, next: () => Promise<void> | void) => Promise<void> | void
|
|
127
|
+
'agent/request': (req: ModelRequest, next: () => Promise<AdapterResult>) => Promise<AdapterResult>
|
|
128
|
+
'agent/request-error': (error: unknown, ctx: StepContext, next: () => Promise<void> | void) => Promise<void> | void
|
|
129
|
+
'agent/step-finish': (step: StepResult) => void
|
|
130
|
+
'agent/turn-stopping': (sessionId: string) => Promise<void> | void
|
|
131
|
+
'agent/turn-finish': (turn: TurnResult) => void
|
|
132
|
+
'tools/pre-execute': (exec: ToolExecutionContext, next: () => Promise<ToolPreExecuteResult>) => Promise<ToolPreExecuteResult>
|
|
133
|
+
'tools/execute': (exec: ToolExecutionContext, next: () => Promise<ToolResult>) => Promise<ToolResult>
|
|
134
|
+
'tools/post-execute': (result: ToolResult, exec: ToolExecutionContext, next: () => Promise<ToolResult>) => Promise<ToolResult>
|
|
135
|
+
'tools/result': (result: ToolResult, exec: ToolExecutionContext) => void
|
|
136
|
+
'system-prompt/assemble': (sections: PromptSection[], mode: AgentMode, next: () => Promise<string> | string) => Promise<string> | string
|
|
137
|
+
'session/flush': (sessionId: string) => Promise<void> | void
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ProviderDef, ProviderId } from '../types'
|
|
2
|
+
|
|
3
|
+
export const PROVIDERS: ProviderDef[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'openai',
|
|
6
|
+
name: 'OpenAI',
|
|
7
|
+
format: 'openai',
|
|
8
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
9
|
+
keyHint: 'sk-…',
|
|
10
|
+
note: 'GPT and Codex models. Requests are made by your local daemon, never by OpenSoft.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'anthropic',
|
|
14
|
+
name: 'Anthropic',
|
|
15
|
+
format: 'anthropic',
|
|
16
|
+
baseUrl: 'https://api.anthropic.com/v1',
|
|
17
|
+
keyHint: 'sk-ant-…',
|
|
18
|
+
note: 'Claude models, including extended thinking where the model supports it.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'google',
|
|
22
|
+
name: 'Google',
|
|
23
|
+
format: 'gemini',
|
|
24
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
25
|
+
keyHint: 'AIza…',
|
|
26
|
+
note: 'Gemini models through the Generative Language API.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'opencode',
|
|
30
|
+
name: 'OpenCode',
|
|
31
|
+
format: 'openai',
|
|
32
|
+
baseUrl: 'https://opencode.ai/inference/v1',
|
|
33
|
+
keyHint: 'OpenCode API key',
|
|
34
|
+
note: 'OpenCode routes each model to its native API. Free-tier models only work inside OpenCode and are hidden.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'deepseek',
|
|
38
|
+
name: 'DeepSeek',
|
|
39
|
+
format: 'openai',
|
|
40
|
+
baseUrl: 'https://api.deepseek.com',
|
|
41
|
+
keyHint: 'sk-…',
|
|
42
|
+
note: 'DeepSeek chat and reasoning models.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'openrouter',
|
|
46
|
+
name: 'OpenRouter',
|
|
47
|
+
format: 'openai',
|
|
48
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
49
|
+
keyHint: 'sk-or-…',
|
|
50
|
+
note: 'One key for hundreds of models and providers.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'groq',
|
|
54
|
+
name: 'Groq',
|
|
55
|
+
format: 'openai',
|
|
56
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
57
|
+
keyHint: 'gsk_…',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'mistral',
|
|
61
|
+
name: 'Mistral',
|
|
62
|
+
format: 'openai',
|
|
63
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
64
|
+
keyHint: '…',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'xai',
|
|
68
|
+
name: 'xAI',
|
|
69
|
+
format: 'openai',
|
|
70
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
71
|
+
keyHint: 'xai-…',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'together',
|
|
75
|
+
name: 'Together AI',
|
|
76
|
+
format: 'openai',
|
|
77
|
+
baseUrl: 'https://api.together.xyz/v1',
|
|
78
|
+
keyHint: '…',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'ollama',
|
|
82
|
+
name: 'Ollama',
|
|
83
|
+
format: 'openai',
|
|
84
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
85
|
+
keyless: true,
|
|
86
|
+
keyHint: 'Not required',
|
|
87
|
+
note: 'Runs on your machine. The daemon reaches it at localhost; no data leaves your computer.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'custom',
|
|
91
|
+
name: 'Custom endpoint',
|
|
92
|
+
format: 'openai',
|
|
93
|
+
baseUrl: '',
|
|
94
|
+
keyHint: 'Optional',
|
|
95
|
+
note: 'Any OpenAI-compatible endpoint or proxy you control.',
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
export const PROVIDER_MAP = Object.fromEntries(PROVIDERS.map((p) => [p.id, p])) as Record<
|
|
100
|
+
ProviderId,
|
|
101
|
+
ProviderDef
|
|
102
|
+
>
|
|
103
|
+
|
|
104
|
+
export function getProviderName(provider: ProviderId) {
|
|
105
|
+
return PROVIDER_MAP[provider]?.name ?? provider
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const EFFORT_ORDER = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
|
|
109
|
+
|
|
110
|
+
export const EFFORT_LABELS: Record<
|
|
111
|
+
(typeof EFFORT_ORDER)[number],
|
|
112
|
+
{ label: string; description: string }
|
|
113
|
+
> = {
|
|
114
|
+
none: { label: 'None', description: 'Disable extra reasoning' },
|
|
115
|
+
minimal: { label: 'Minimal', description: 'Very light reasoning' },
|
|
116
|
+
low: { label: 'Low', description: 'Fast, lighter reasoning' },
|
|
117
|
+
medium: { label: 'Medium', description: 'Balanced reasoning' },
|
|
118
|
+
high: { label: 'High', description: 'Deeper reasoning, slower' },
|
|
119
|
+
xhigh: { label: 'Extra high', description: 'Very deep reasoning' },
|
|
120
|
+
max: { label: 'Max', description: 'Maximum effort the model allows' },
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function effortLabel(effort: string) {
|
|
124
|
+
return EFFORT_LABELS[effort as keyof typeof EFFORT_LABELS]?.label ?? effort
|
|
125
|
+
}
|