@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,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
Message,
|
|
4
|
+
ToolMessage,
|
|
5
|
+
UserMessage,
|
|
6
|
+
} from '../types'
|
|
7
|
+
import { findModel } from '../store/models'
|
|
8
|
+
import { useSettings } from '../store/settings'
|
|
9
|
+
import { EMPTY_HANDLERS } from './adapters/shared'
|
|
10
|
+
import { streamModel } from './model'
|
|
11
|
+
import { buildSubagentPrompt } from './prompt'
|
|
12
|
+
import { executeTool } from './tools'
|
|
13
|
+
|
|
14
|
+
const MAX_SUBAGENT_ROUNDS = 6
|
|
15
|
+
|
|
16
|
+
function id(prefix: string) {
|
|
17
|
+
const raw =
|
|
18
|
+
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
19
|
+
? crypto.randomUUID()
|
|
20
|
+
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
|
|
21
|
+
return `${prefix}_${raw}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function abortError() {
|
|
25
|
+
return new DOMException('Aborted', 'AbortError')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Runs a short read-only investigation for the main agent. It shares the
|
|
30
|
+
* selected model and the encrypted daemon credentials, but never edits files.
|
|
31
|
+
*/
|
|
32
|
+
export async function runSubagent(
|
|
33
|
+
task: string,
|
|
34
|
+
context: string,
|
|
35
|
+
signal: AbortSignal,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const cleanTask = task.trim()
|
|
38
|
+
if (!cleanTask) return 'The subagent was not given a task.'
|
|
39
|
+
if (signal.aborted) throw abortError()
|
|
40
|
+
|
|
41
|
+
const settings = useSettings.getState()
|
|
42
|
+
const model = findModel(settings.provider, settings.model)
|
|
43
|
+
const system = buildSubagentPrompt(settings)
|
|
44
|
+
const now = Date.now()
|
|
45
|
+
const first: UserMessage = {
|
|
46
|
+
id: id('sub'),
|
|
47
|
+
role: 'user',
|
|
48
|
+
content: [
|
|
49
|
+
`Task: ${cleanTask}`,
|
|
50
|
+
context.trim() ? `\nContext from the main agent:\n${context.trim()}` : '',
|
|
51
|
+
'\nInvestigate now. Return a concise report with file paths and evidence.',
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join('\n'),
|
|
55
|
+
createdAt: now,
|
|
56
|
+
}
|
|
57
|
+
const history: Message[] = [first]
|
|
58
|
+
let lastContent = ''
|
|
59
|
+
|
|
60
|
+
for (let round = 0; round < MAX_SUBAGENT_ROUNDS; round += 1) {
|
|
61
|
+
if (signal.aborted) throw abortError()
|
|
62
|
+
const result = await streamModel({
|
|
63
|
+
settings,
|
|
64
|
+
model,
|
|
65
|
+
history,
|
|
66
|
+
system,
|
|
67
|
+
mode: 'plan',
|
|
68
|
+
signal,
|
|
69
|
+
handlers: EMPTY_HANDLERS,
|
|
70
|
+
})
|
|
71
|
+
if (result.content.trim()) lastContent = result.content.trim()
|
|
72
|
+
|
|
73
|
+
const assistant: AssistantMessage = {
|
|
74
|
+
id: id('submsg'),
|
|
75
|
+
role: 'assistant',
|
|
76
|
+
content: result.content,
|
|
77
|
+
reasoning: result.reasoning,
|
|
78
|
+
thinkingBlocks: result.thinkingBlocks,
|
|
79
|
+
toolCalls: result.toolCalls,
|
|
80
|
+
round,
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
}
|
|
83
|
+
history.push(assistant)
|
|
84
|
+
|
|
85
|
+
if (result.toolCalls.length === 0) return assistant.content.trim() || 'The subagent returned no report.'
|
|
86
|
+
if (signal.aborted) throw abortError()
|
|
87
|
+
|
|
88
|
+
for (const call of result.toolCalls) {
|
|
89
|
+
const output = await executeTool(call.name, call.args, settings, 'plan', signal)
|
|
90
|
+
const toolMessage: ToolMessage = {
|
|
91
|
+
id: id('subtool'),
|
|
92
|
+
role: 'tool',
|
|
93
|
+
call,
|
|
94
|
+
status: output.isError ? 'error' : 'done',
|
|
95
|
+
result: output.output,
|
|
96
|
+
round,
|
|
97
|
+
createdAt: Date.now(),
|
|
98
|
+
}
|
|
99
|
+
history.push(toolMessage)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [
|
|
104
|
+
lastContent,
|
|
105
|
+
`The subagent stopped after ${MAX_SUBAGENT_ROUNDS} investigation rounds. Continue with the evidence above or ask a narrower question.`,
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join('\n\n')
|
|
109
|
+
}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import type { AgentMode, AppSettings, ToolName } from '../types'
|
|
2
|
+
import { executeDaemonTool } from '../lib/daemon'
|
|
3
|
+
import { useWorkspace } from '../store/workspace'
|
|
4
|
+
import { kernel } from '../core/kernel'
|
|
5
|
+
|
|
6
|
+
export interface JsonSchema {
|
|
7
|
+
type: 'object'
|
|
8
|
+
properties: Record<string, unknown>
|
|
9
|
+
required?: string[]
|
|
10
|
+
additionalProperties?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToolSchema {
|
|
14
|
+
name: ToolName
|
|
15
|
+
description: string
|
|
16
|
+
parameters: JsonSchema
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_TOOL_OUTPUT = 28_000
|
|
20
|
+
const READ_ONLY_TOOLS = new Set<ToolName>([
|
|
21
|
+
'read_file',
|
|
22
|
+
'list_files',
|
|
23
|
+
'grep',
|
|
24
|
+
'web_search',
|
|
25
|
+
'read_url',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
function truncate(value: string, limit = MAX_TOOL_OUTPUT) {
|
|
29
|
+
if (value.length <= limit) return value
|
|
30
|
+
return `${value.slice(0, limit)}\n… truncated (${value.length - limit} more characters)`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function record(value: unknown): Record<string, unknown> {
|
|
34
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function textValue(value: unknown): string {
|
|
38
|
+
return typeof value === 'string' ? value : ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function numberValue(value: unknown): number | undefined {
|
|
42
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requireString(args: Record<string, unknown>, key: string): string {
|
|
46
|
+
const value = args[key]
|
|
47
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`Missing string argument "${key}".`)
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function optionalString(args: Record<string, unknown>, key: string): string | undefined {
|
|
52
|
+
const value = args[key]
|
|
53
|
+
return typeof value === 'string' ? value : undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function numberArg(args: Record<string, unknown>, key: string, fallback: number): number {
|
|
57
|
+
const value = args[key]
|
|
58
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
59
|
+
if (typeof value === 'string' && Number.isFinite(Number(value))) return Number(value)
|
|
60
|
+
return fallback
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toolAllowedInMode(name: ToolName, mode: AgentMode): boolean {
|
|
64
|
+
if (mode === 'build') return true
|
|
65
|
+
return READ_ONLY_TOOLS.has(name) || name === 'shell' || name === 'git' || name === 'subagent'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getToolSchemas(
|
|
69
|
+
settings: AppSettings,
|
|
70
|
+
mode: AgentMode = 'build',
|
|
71
|
+
{ subagent = false } = {},
|
|
72
|
+
): ToolSchema[] {
|
|
73
|
+
if (kernel.tools) {
|
|
74
|
+
return kernel.tools.schemas(settings, mode, { subagent })
|
|
75
|
+
}
|
|
76
|
+
const tools: ToolSchema[] = [
|
|
77
|
+
{
|
|
78
|
+
name: 'shell',
|
|
79
|
+
description:
|
|
80
|
+
'Run a shell command in the workspace root. The working directory resets between calls. Prefer dedicated file and git tools for edits. In Plan mode only read-only commands are accepted.',
|
|
81
|
+
parameters: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
command: { type: 'string', description: 'The shell command to run.' },
|
|
85
|
+
timeoutMs: { type: 'number', description: 'Timeout in milliseconds (max 600000).' },
|
|
86
|
+
},
|
|
87
|
+
required: ['command'],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'read_file',
|
|
93
|
+
description: 'Read a text file from the workspace. Use offset and limit for large files.',
|
|
94
|
+
parameters: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
path: { type: 'string', description: 'Relative path from the workspace root.' },
|
|
98
|
+
offset: { type: 'number', description: '1-based first line. Defaults to 1.' },
|
|
99
|
+
limit: { type: 'number', description: 'Maximum lines. Defaults to 400.' },
|
|
100
|
+
},
|
|
101
|
+
required: ['path'],
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'list_files',
|
|
107
|
+
description: 'List files and folders below a workspace path.',
|
|
108
|
+
parameters: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
path: { type: 'string', description: 'Folder path. Defaults to the workspace root.' },
|
|
112
|
+
depth: { type: 'number', description: 'Recursion depth, 1–8. Defaults to 3.' },
|
|
113
|
+
},
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'write_file',
|
|
119
|
+
description: 'Create a file or replace its full contents. Prefer edit_file for small changes.',
|
|
120
|
+
parameters: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
path: { type: 'string', description: 'Relative file path.' },
|
|
124
|
+
content: { type: 'string', description: 'Complete file content.' },
|
|
125
|
+
},
|
|
126
|
+
required: ['path', 'content'],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'edit_file',
|
|
132
|
+
description:
|
|
133
|
+
'Replace an exact text occurrence in a file. oldText must match whitespace exactly and be unique unless replaceAll is true.',
|
|
134
|
+
parameters: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
path: { type: 'string', description: 'Relative file path.' },
|
|
138
|
+
oldText: { type: 'string', description: 'Exact text to replace.' },
|
|
139
|
+
newText: { type: 'string', description: 'Replacement text.' },
|
|
140
|
+
replaceAll: { type: 'boolean', description: 'Replace every occurrence. Defaults to false.' },
|
|
141
|
+
},
|
|
142
|
+
required: ['path', 'oldText', 'newText'],
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'rewrite_file',
|
|
148
|
+
description: 'Replace the complete contents of an existing file. Use when most of the file changes.',
|
|
149
|
+
parameters: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
path: { type: 'string', description: 'Relative file path.' },
|
|
153
|
+
content: { type: 'string', description: 'The new complete file content.' },
|
|
154
|
+
},
|
|
155
|
+
required: ['path', 'content'],
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'grep',
|
|
161
|
+
description: 'Search workspace contents with a regular expression. Returns path, line and matching text.',
|
|
162
|
+
parameters: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
pattern: { type: 'string', description: 'Regular expression to search for.' },
|
|
166
|
+
path: { type: 'string', description: 'File or folder. Defaults to the workspace root.' },
|
|
167
|
+
ignoreCase: { type: 'boolean', description: 'Case-insensitive search. Defaults to false.' },
|
|
168
|
+
maxResults: { type: 'number', description: 'Maximum matches. Defaults to 80.' },
|
|
169
|
+
},
|
|
170
|
+
required: ['pattern'],
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'web_search',
|
|
176
|
+
description: 'Search the web for current information. Returns titles, URLs and snippets. Cite URLs you use.',
|
|
177
|
+
parameters: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
query: { type: 'string', description: 'Search query.' },
|
|
181
|
+
},
|
|
182
|
+
required: ['query'],
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'read_url',
|
|
188
|
+
description: 'Read a public web page as plain text.',
|
|
189
|
+
parameters: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
url: { type: 'string', description: 'The http(s) URL to read.' },
|
|
193
|
+
},
|
|
194
|
+
required: ['url'],
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'git',
|
|
200
|
+
description:
|
|
201
|
+
'Inspect or change git state. In Plan mode only status, diff, log, show and branch are accepted.',
|
|
202
|
+
parameters: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
action: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
enum: ['status', 'diff', 'log', 'show', 'branch', 'add', 'commit', 'init', 'checkout'],
|
|
208
|
+
},
|
|
209
|
+
path: { type: 'string', description: 'Limit diff to this file.' },
|
|
210
|
+
staged: { type: 'boolean', description: 'Diff the staged changes.' },
|
|
211
|
+
ref: { type: 'string', description: 'Commit or ref for show.' },
|
|
212
|
+
limit: { type: 'number', description: 'Log limit.' },
|
|
213
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'Files to stage.' },
|
|
214
|
+
message: { type: 'string', description: 'Commit message.' },
|
|
215
|
+
branch: { type: 'string', description: 'Branch to create or check out.' },
|
|
216
|
+
create: { type: 'boolean', description: 'Create a new branch. Defaults to true.' },
|
|
217
|
+
},
|
|
218
|
+
required: ['action'],
|
|
219
|
+
additionalProperties: false,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
if (!subagent) {
|
|
225
|
+
tools.push({
|
|
226
|
+
name: 'subagent',
|
|
227
|
+
description:
|
|
228
|
+
'Delegate a focused investigation to a read-only subagent. Use it to explore a large area, trace a bug or verify assumptions without changing files. The subagent returns a concise report.',
|
|
229
|
+
parameters: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
task: { type: 'string', description: 'The specific question or investigation to perform.' },
|
|
233
|
+
context: { type: 'string', description: 'Optional context, constraints or files to start from.' },
|
|
234
|
+
},
|
|
235
|
+
required: ['task'],
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return tools.filter((tool) => {
|
|
242
|
+
if (!settings.tools[tool.name]?.enabled) return false
|
|
243
|
+
if (!toolAllowedInMode(tool.name, mode)) return false
|
|
244
|
+
if ((tool.name === 'web_search' || tool.name === 'read_url') && !settings.webSearchEnabled) return false
|
|
245
|
+
return true
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface ToolResult {
|
|
250
|
+
output: string
|
|
251
|
+
isError: boolean
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function refreshTree() {
|
|
255
|
+
await useWorkspace.getState().refresh({ treeOnly: true }).catch(() => undefined)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function refreshWorkspace() {
|
|
259
|
+
await useWorkspace.getState().refresh().catch(() => undefined)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatList(result: Record<string, unknown>): string {
|
|
263
|
+
const entries = Array.isArray(result.entries) ? result.entries : []
|
|
264
|
+
if (entries.length === 0) return 'This folder is empty.'
|
|
265
|
+
const lines = entries.map((raw) => {
|
|
266
|
+
const entry = record(raw)
|
|
267
|
+
const path = textValue(entry.path)
|
|
268
|
+
if (entry.type === 'dir') return `${path}/`
|
|
269
|
+
if (entry.type === 'link') return `${path} -> link`
|
|
270
|
+
return `${path}${numberValue(entry.size) !== undefined ? ` (${numberValue(entry.size)} B)` : ''}`
|
|
271
|
+
})
|
|
272
|
+
const suffix = result.truncated === true ? '\n… listing truncated. Narrow the path or reduce depth.' : ''
|
|
273
|
+
return `${textValue(result.root) ? `Root: ${textValue(result.root)}\n` : ''}${lines.join('\n')}${suffix}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatShell(result: Record<string, unknown>, command: string): string {
|
|
277
|
+
const parts = [`$ ${command}`, textValue(result.stdout)]
|
|
278
|
+
if (textValue(result.stderr)) parts.push(`stderr:\n${textValue(result.stderr)}`)
|
|
279
|
+
const code = numberValue(result.code)
|
|
280
|
+
if (code !== undefined && code !== 0) parts.push(`exit code: ${code}`)
|
|
281
|
+
if (result.timedOut === true) parts.push('The command timed out.')
|
|
282
|
+
return parts.filter((part) => part !== '').join('\n')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatGrep(result: Record<string, unknown>, pattern: string): string {
|
|
286
|
+
const matches = Array.isArray(result.matches) ? result.matches : []
|
|
287
|
+
if (matches.length === 0) return `No matches for ${pattern}.`
|
|
288
|
+
const lines = matches.map((raw) => {
|
|
289
|
+
const match = record(raw)
|
|
290
|
+
return `${textValue(match.file)}:${numberValue(match.line) ?? 0}: ${textValue(match.text)}`
|
|
291
|
+
})
|
|
292
|
+
const suffix = result.truncated === true ? '\n… more matches exist. Narrow the pattern or path.' : ''
|
|
293
|
+
return lines.join('\n') + suffix
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatGit(result: Record<string, unknown>): string {
|
|
297
|
+
const parts: string[] = []
|
|
298
|
+
if (textValue(result.current)) parts.push(`Current branch: ${textValue(result.current)}`)
|
|
299
|
+
if (textValue(result.branches)) parts.push(textValue(result.branches))
|
|
300
|
+
if (textValue(result.stdout)) parts.push(textValue(result.stdout))
|
|
301
|
+
if (textValue(result.stderr)) parts.push(`stderr:\n${textValue(result.stderr)}`)
|
|
302
|
+
const code = numberValue(result.code)
|
|
303
|
+
if (code !== undefined && code !== 0) parts.push(`exit code: ${code}`)
|
|
304
|
+
return parts.filter(Boolean).join('\n') || 'Git completed with no output.'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function runTool(
|
|
308
|
+
name: Exclude<ToolName, 'subagent'>,
|
|
309
|
+
args: Record<string, unknown>,
|
|
310
|
+
settings: AppSettings,
|
|
311
|
+
mode: AgentMode,
|
|
312
|
+
signal: AbortSignal,
|
|
313
|
+
): Promise<ToolResult> {
|
|
314
|
+
if (signal.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
315
|
+
|
|
316
|
+
if (name === 'read_file') {
|
|
317
|
+
const path = requireString(args, 'path')
|
|
318
|
+
const result = record((await executeDaemonTool('read_file', { path }, mode, signal)).result)
|
|
319
|
+
if (result.binary === true) {
|
|
320
|
+
return { output: `${path} is binary (${numberValue(result.size) ?? 0} bytes).`, isError: false }
|
|
321
|
+
}
|
|
322
|
+
const offset = Math.max(1, Math.round(numberArg(args, 'offset', 1)))
|
|
323
|
+
const limit = Math.min(Math.max(1, Math.round(numberArg(args, 'limit', 400))), 2000)
|
|
324
|
+
const lines = textValue(result.content).split('\n')
|
|
325
|
+
const slice = lines.slice(offset - 1, offset - 1 + limit)
|
|
326
|
+
const remaining = lines.length - (offset - 1 + limit)
|
|
327
|
+
const header = `${textValue(result.path) || path} · ${lines.length} lines${result.truncated === true ? ' · file clipped by daemon' : ''}`
|
|
328
|
+
const footer = remaining > 0 ? `\n… ${remaining} more lines. Read again with offset ${offset + limit}.` : ''
|
|
329
|
+
return { output: truncate(`${header}\n────\n${slice.join('\n')}${footer}`), isError: false }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (name === 'list_files') {
|
|
333
|
+
const result = record(
|
|
334
|
+
(
|
|
335
|
+
await executeDaemonTool(
|
|
336
|
+
'list_files',
|
|
337
|
+
{
|
|
338
|
+
path: optionalString(args, 'path') ?? '',
|
|
339
|
+
depth: Math.min(Math.max(1, numberArg(args, 'depth', 3)), 8),
|
|
340
|
+
},
|
|
341
|
+
mode,
|
|
342
|
+
signal,
|
|
343
|
+
)
|
|
344
|
+
).result,
|
|
345
|
+
)
|
|
346
|
+
return { output: formatList(result), isError: false }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (name === 'write_file' || name === 'rewrite_file') {
|
|
350
|
+
const path = requireString(args, 'path')
|
|
351
|
+
const content = requireString(args, 'content')
|
|
352
|
+
const result = record((await executeDaemonTool(name, { path, content }, mode, signal)).result)
|
|
353
|
+
await refreshWorkspace()
|
|
354
|
+
const verb = result.created === true ? 'Created' : name === 'rewrite_file' ? 'Rewrote' : 'Updated'
|
|
355
|
+
return {
|
|
356
|
+
output: `${verb} ${textValue(result.path) || path} · ${content.split('\n').length} lines, ${numberValue(result.size) ?? content.length} bytes.`,
|
|
357
|
+
isError: false,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (name === 'edit_file') {
|
|
362
|
+
const path = requireString(args, 'path')
|
|
363
|
+
const oldText = requireString(args, 'oldText')
|
|
364
|
+
const newText = typeof args.newText === 'string' ? args.newText : ''
|
|
365
|
+
const result = record(
|
|
366
|
+
(
|
|
367
|
+
await executeDaemonTool(
|
|
368
|
+
'edit_file',
|
|
369
|
+
{ path, oldText, newText, replaceAll: args.replaceAll === true },
|
|
370
|
+
mode,
|
|
371
|
+
signal,
|
|
372
|
+
)
|
|
373
|
+
).result,
|
|
374
|
+
)
|
|
375
|
+
await refreshWorkspace()
|
|
376
|
+
return {
|
|
377
|
+
output: `Edited ${textValue(result.path) || path} · ${numberValue(result.replacements) ?? 1} replacement${numberValue(result.replacements) === 1 ? '' : 's'}.`,
|
|
378
|
+
isError: false,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (name === 'shell') {
|
|
383
|
+
const command = requireString(args, 'command')
|
|
384
|
+
const result = record(
|
|
385
|
+
(
|
|
386
|
+
await executeDaemonTool(
|
|
387
|
+
'shell',
|
|
388
|
+
{ command, timeoutMs: numberArg(args, 'timeoutMs', 120_000) },
|
|
389
|
+
mode,
|
|
390
|
+
signal,
|
|
391
|
+
)
|
|
392
|
+
).result,
|
|
393
|
+
)
|
|
394
|
+
void refreshTree()
|
|
395
|
+
return {
|
|
396
|
+
output: truncate(formatShell(result, command)),
|
|
397
|
+
isError: (numberValue(result.code) ?? 0) !== 0 || result.timedOut === true,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (name === 'grep') {
|
|
402
|
+
const pattern = requireString(args, 'pattern')
|
|
403
|
+
const result = record(
|
|
404
|
+
(
|
|
405
|
+
await executeDaemonTool(
|
|
406
|
+
'grep',
|
|
407
|
+
{
|
|
408
|
+
pattern,
|
|
409
|
+
path: optionalString(args, 'path') ?? '',
|
|
410
|
+
ignoreCase: args.ignoreCase === true,
|
|
411
|
+
},
|
|
412
|
+
mode,
|
|
413
|
+
signal,
|
|
414
|
+
)
|
|
415
|
+
).result,
|
|
416
|
+
)
|
|
417
|
+
const maxResults = Math.min(Math.max(1, Math.round(numberArg(args, 'maxResults', 80))), 200)
|
|
418
|
+
const matches = Array.isArray(result.matches) ? result.matches.slice(0, maxResults) : []
|
|
419
|
+
const clipped = { ...result, matches, truncated: result.truncated === true || (Array.isArray(result.matches) && result.matches.length > maxResults) }
|
|
420
|
+
return { output: truncate(formatGrep(clipped, pattern)), isError: false }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (name === 'web_search') {
|
|
424
|
+
const query = requireString(args, 'query')
|
|
425
|
+
const result = record(
|
|
426
|
+
(
|
|
427
|
+
await executeDaemonTool(
|
|
428
|
+
'web_search',
|
|
429
|
+
{ query, provider: settings.searchProvider },
|
|
430
|
+
mode,
|
|
431
|
+
signal,
|
|
432
|
+
)
|
|
433
|
+
).result,
|
|
434
|
+
)
|
|
435
|
+
const results = Array.isArray(result.results) ? result.results : []
|
|
436
|
+
if (results.length === 0) return { output: 'No results found.', isError: false }
|
|
437
|
+
const formatted = results
|
|
438
|
+
.map((raw, index) => {
|
|
439
|
+
const item = record(raw)
|
|
440
|
+
return `${index + 1}. ${textValue(item.title)}\n ${textValue(item.url)}${textValue(item.snippet) ? `\n ${textValue(item.snippet)}` : ''}`
|
|
441
|
+
})
|
|
442
|
+
.join('\n\n')
|
|
443
|
+
return {
|
|
444
|
+
output: truncate(`Search: ${query}\nProvider: ${textValue(result.provider) || 'web'}\n\n${formatted}`),
|
|
445
|
+
isError: false,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (name === 'read_url') {
|
|
450
|
+
const url = requireString(args, 'url')
|
|
451
|
+
const result = record((await executeDaemonTool('read_url', { url }, mode, signal)).result)
|
|
452
|
+
const title = textValue(result.title)
|
|
453
|
+
return {
|
|
454
|
+
output: truncate(
|
|
455
|
+
`Source: ${textValue(result.url) || url}${title ? `\nTitle: ${title}` : ''}\n\n${textValue(result.content)}${result.truncated === true ? '\n\n… page truncated.' : ''}`,
|
|
456
|
+
),
|
|
457
|
+
isError: false,
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const action = requireString(args, 'action')
|
|
462
|
+
const gitArgs: Record<string, unknown> = { ...args }
|
|
463
|
+
delete gitArgs.action
|
|
464
|
+
const result = record((await executeDaemonTool('git', { action, ...gitArgs }, mode, signal)).result)
|
|
465
|
+
if (action === 'add' || action === 'commit' || action === 'init' || action === 'checkout') {
|
|
466
|
+
await useWorkspace.getState().loadGit().catch(() => undefined)
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
output: truncate(formatGit(result)),
|
|
470
|
+
isError: (numberValue(result.code) ?? 0) !== 0,
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function executeTool(
|
|
475
|
+
name: ToolName,
|
|
476
|
+
args: Record<string, unknown>,
|
|
477
|
+
settings: AppSettings,
|
|
478
|
+
mode: AgentMode,
|
|
479
|
+
signal: AbortSignal,
|
|
480
|
+
): Promise<ToolResult> {
|
|
481
|
+
if (kernel.tools) {
|
|
482
|
+
return await kernel.tools.execute(name, args, settings, mode, signal)
|
|
483
|
+
}
|
|
484
|
+
if (!settings.tools[name]?.enabled) {
|
|
485
|
+
return { output: 'This tool is disabled in Settings → Tools.', isError: true }
|
|
486
|
+
}
|
|
487
|
+
if (!toolAllowedInMode(name, mode)) {
|
|
488
|
+
return {
|
|
489
|
+
output:
|
|
490
|
+
'Plan mode is read-only. Do not edit files or change git state. Finish with an implementation plan instead.',
|
|
491
|
+
isError: true,
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if ((name === 'web_search' || name === 'read_url') && !settings.webSearchEnabled) {
|
|
495
|
+
return { output: 'Web access is turned off for this session.', isError: true }
|
|
496
|
+
}
|
|
497
|
+
if (name === 'subagent') {
|
|
498
|
+
return { output: 'Subagents are handled by the agent loop.', isError: true }
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
return await runTool(name, args, settings, mode, signal)
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (error instanceof DOMException && error.name === 'AbortError') throw error
|
|
504
|
+
return { output: error instanceof Error ? error.message : 'The tool failed.', isError: true }
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function toolLabel(name: ToolName): string {
|
|
509
|
+
const labels: Record<ToolName, string> = {
|
|
510
|
+
shell: 'Shell',
|
|
511
|
+
read_file: 'Read file',
|
|
512
|
+
list_files: 'List files',
|
|
513
|
+
write_file: 'Write file',
|
|
514
|
+
edit_file: 'Edit file',
|
|
515
|
+
rewrite_file: 'Rewrite file',
|
|
516
|
+
grep: 'Grep',
|
|
517
|
+
web_search: 'Web search',
|
|
518
|
+
read_url: 'Read URL',
|
|
519
|
+
git: 'Git',
|
|
520
|
+
subagent: 'Subagent',
|
|
521
|
+
}
|
|
522
|
+
return labels[name] ?? 'Tool'
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function toolSummary(name: ToolName, args: Record<string, unknown>): string {
|
|
526
|
+
const value =
|
|
527
|
+
name === 'shell'
|
|
528
|
+
? args.command
|
|
529
|
+
: name === 'grep'
|
|
530
|
+
? args.pattern
|
|
531
|
+
: name === 'web_search'
|
|
532
|
+
? args.query
|
|
533
|
+
: name === 'read_url'
|
|
534
|
+
? args.url
|
|
535
|
+
: name === 'git'
|
|
536
|
+
? args.action
|
|
537
|
+
: name === 'subagent'
|
|
538
|
+
? args.task
|
|
539
|
+
: (args.path ?? '')
|
|
540
|
+
return typeof value === 'string' ? value : ''
|
|
541
|
+
}
|