@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,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
+ }