workermill 0.1.8 → 0.2.0
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/README.md +14 -5
- package/dist/{chunk-3KIFXIBC.js → chunk-VC6VNVEY.js} +214 -468
- package/dist/index.js +1707 -690
- package/dist/{orchestrator-NMTZUS23.js → orchestrator-5I7BGPC7.js} +597 -162
- package/package.json +7 -1
- package/personas/planner.md +1 -1
- package/dist/chunk-2NTK7H4W.js +0 -10
- package/dist/chunk-LVCJZJJH.js +0 -29
- package/dist/terminal-ILMO7Z3P.js +0 -17
- package/personas/ml_engineer.md +0 -32
- /package/personas/{data_engineer.md → data_ml_engineer.md} +0 -0
- /package/personas/{reviewer.md → tech_lead.md} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
exitTerminal,
|
|
4
|
-
initTerminal,
|
|
5
|
-
setStatusBar,
|
|
6
|
-
showStatusBar
|
|
7
|
-
} from "./chunk-LVCJZJJH.js";
|
|
8
2
|
import {
|
|
9
3
|
CostTracker,
|
|
10
|
-
|
|
4
|
+
buildOllamaOptions,
|
|
11
5
|
createModel,
|
|
12
6
|
createToolDefinitions,
|
|
13
7
|
getProviderForPersona,
|
|
14
8
|
killActiveProcess,
|
|
15
9
|
loadConfig,
|
|
16
|
-
printError,
|
|
17
|
-
printHeader,
|
|
18
|
-
printStatusBar,
|
|
19
|
-
printToolCall,
|
|
20
|
-
printToolResult,
|
|
21
10
|
saveConfig
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import "./chunk-2NTK7H4W.js";
|
|
11
|
+
} from "./chunk-VC6VNVEY.js";
|
|
24
12
|
|
|
25
13
|
// src/index.ts
|
|
14
|
+
import React5 from "react";
|
|
15
|
+
import { render } from "ink";
|
|
26
16
|
import { Command } from "commander";
|
|
27
17
|
|
|
28
18
|
// src/setup.js
|
|
@@ -32,8 +22,8 @@ import chalk from "chalk";
|
|
|
32
22
|
var PROVIDERS = [
|
|
33
23
|
{ name: "ollama", display: "Ollama (local, no API key needed)", needsKey: false, defaultModel: "qwen3-coder:30b" },
|
|
34
24
|
{ name: "anthropic", display: "Anthropic (Claude)", needsKey: true, defaultModel: "claude-sonnet-4-6", envVar: "ANTHROPIC_API_KEY" },
|
|
35
|
-
{ name: "openai", display: "OpenAI (GPT)", needsKey: true, defaultModel: "gpt-
|
|
36
|
-
{ name: "google", display: "Google (Gemini)", needsKey: true, defaultModel: "gemini-
|
|
25
|
+
{ name: "openai", display: "OpenAI (GPT)", needsKey: true, defaultModel: "gpt-5.4", envVar: "OPENAI_API_KEY" },
|
|
26
|
+
{ name: "google", display: "Google (Gemini)", needsKey: true, defaultModel: "gemini-3.1-pro", envVar: "GOOGLE_API_KEY" }
|
|
37
27
|
];
|
|
38
28
|
function ask(rl, question) {
|
|
39
29
|
return new Promise((resolve) => rl.question(question, resolve));
|
|
@@ -97,9 +87,11 @@ async function runSetup() {
|
|
|
97
87
|
continue;
|
|
98
88
|
}
|
|
99
89
|
}
|
|
90
|
+
providerConfig.contextLength = 65536;
|
|
100
91
|
if (connectedHost) {
|
|
101
92
|
providerConfig.host = connectedHost;
|
|
102
93
|
console.log(chalk.green(` \u2713 Connected to Ollama at ${connectedHost}`));
|
|
94
|
+
console.log(chalk.dim(` Context window: ${providerConfig.contextLength.toLocaleString()} tokens`));
|
|
103
95
|
if (models.length > 0) {
|
|
104
96
|
console.log(chalk.dim(` Available models: ${models.map((m) => m.name).join(", ")}`));
|
|
105
97
|
}
|
|
@@ -126,80 +118,18 @@ async function runSetup() {
|
|
|
126
118
|
return config;
|
|
127
119
|
}
|
|
128
120
|
|
|
129
|
-
// src/
|
|
130
|
-
import
|
|
131
|
-
import
|
|
132
|
-
import
|
|
133
|
-
import chalk3 from "chalk";
|
|
134
|
-
import ora2 from "ora";
|
|
135
|
-
import { execSync as execSync3 } from "child_process";
|
|
136
|
-
import { streamText, stepCountIs } from "ai";
|
|
137
|
-
|
|
138
|
-
// src/commands.js
|
|
139
|
-
import chalk2 from "chalk";
|
|
140
|
-
import ora from "ora";
|
|
121
|
+
// src/ui/Root.tsx
|
|
122
|
+
import { useState as useState5, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
123
|
+
import { useApp as useApp2 } from "ink";
|
|
124
|
+
import { execSync as execSync2 } from "child_process";
|
|
141
125
|
import fs2 from "fs";
|
|
142
|
-
import os from "os";
|
|
143
126
|
import path2 from "path";
|
|
144
|
-
import
|
|
127
|
+
import os from "os";
|
|
145
128
|
|
|
146
|
-
// src/
|
|
147
|
-
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"claude-opus": 2e5,
|
|
151
|
-
"claude-sonnet": 2e5,
|
|
152
|
-
"claude-haiku": 2e5,
|
|
153
|
-
// OpenAI
|
|
154
|
-
"gpt-4o": 128e3,
|
|
155
|
-
"gpt-4o-mini": 128e3,
|
|
156
|
-
"o3": 2e5,
|
|
157
|
-
"o3-mini": 128e3,
|
|
158
|
-
// Google
|
|
159
|
-
"gemini-2.5-pro": 1e6,
|
|
160
|
-
"gemini-2.5-flash": 1e6,
|
|
161
|
-
// Ollama (conservative default)
|
|
162
|
-
"default": 32e3
|
|
163
|
-
};
|
|
164
|
-
function getContextLimit(model) {
|
|
165
|
-
for (const [prefix, limit] of Object.entries(CONTEXT_LIMITS)) {
|
|
166
|
-
if (model.includes(prefix))
|
|
167
|
-
return limit;
|
|
168
|
-
}
|
|
169
|
-
return CONTEXT_LIMITS["default"];
|
|
170
|
-
}
|
|
171
|
-
function shouldCompact(totalTokens, model) {
|
|
172
|
-
const limit = getContextLimit(model);
|
|
173
|
-
if (totalTokens >= limit * 0.95)
|
|
174
|
-
return "hard";
|
|
175
|
-
if (totalTokens >= limit * 0.8)
|
|
176
|
-
return "soft";
|
|
177
|
-
return "none";
|
|
178
|
-
}
|
|
179
|
-
async function compactMessages(model, messages, mode) {
|
|
180
|
-
if (messages.length <= 4)
|
|
181
|
-
return messages;
|
|
182
|
-
const keepCount = mode === "hard" ? 2 : 4;
|
|
183
|
-
const toCompact = messages.slice(0, -keepCount);
|
|
184
|
-
const toKeep = messages.slice(-keepCount);
|
|
185
|
-
if (toCompact.length === 0)
|
|
186
|
-
return messages;
|
|
187
|
-
const summaryText = toCompact.map((m) => `${m.role}: ${m.content.slice(0, 500)}`).join("\n\n");
|
|
188
|
-
try {
|
|
189
|
-
const result = await generateText({
|
|
190
|
-
model,
|
|
191
|
-
system: "Summarize this conversation history concisely. Focus on: what was discussed, what decisions were made, what files were modified, and what the current state of work is. Be brief but preserve all important context.",
|
|
192
|
-
prompt: summaryText
|
|
193
|
-
});
|
|
194
|
-
return [
|
|
195
|
-
{ role: "assistant", content: `[Conversation summary]
|
|
196
|
-
${result.text}` },
|
|
197
|
-
...toKeep
|
|
198
|
-
];
|
|
199
|
-
} catch {
|
|
200
|
-
return toKeep;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
129
|
+
// src/ui/useAgent.ts
|
|
130
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
131
|
+
import { streamText, stepCountIs } from "ai";
|
|
132
|
+
import crypto2 from "crypto";
|
|
203
133
|
|
|
204
134
|
// src/session.js
|
|
205
135
|
import fs from "fs";
|
|
@@ -271,270 +201,1369 @@ function listSessions(max = 20) {
|
|
|
271
201
|
return [];
|
|
272
202
|
}
|
|
273
203
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
204
|
+
|
|
205
|
+
// src/compaction.js
|
|
206
|
+
import { generateText } from "ai";
|
|
207
|
+
var CONTEXT_LIMITS = {
|
|
208
|
+
// Anthropic (Claude 4.5/4.6)
|
|
209
|
+
"claude-opus": 2e5,
|
|
210
|
+
"claude-sonnet": 2e5,
|
|
211
|
+
"claude-haiku": 2e5,
|
|
212
|
+
// OpenAI (GPT-5.x)
|
|
213
|
+
"gpt-5.4": 4e5,
|
|
214
|
+
"gpt-5.3": 4e5,
|
|
215
|
+
"gpt-5.2": 2e5,
|
|
216
|
+
"gpt-5.4-mini": 2e5,
|
|
217
|
+
// Google (Gemini 3.x)
|
|
218
|
+
"gemini-3.1": 1e6,
|
|
219
|
+
"gemini-3.0": 1e6,
|
|
220
|
+
"gemini-2.5": 1e6,
|
|
221
|
+
// Ollama — uses configured contextLength, this is just fallback
|
|
222
|
+
"default": 65536
|
|
223
|
+
};
|
|
224
|
+
function getContextLimit(model) {
|
|
225
|
+
for (const [prefix, limit] of Object.entries(CONTEXT_LIMITS)) {
|
|
226
|
+
if (model.includes(prefix))
|
|
227
|
+
return limit;
|
|
286
228
|
}
|
|
287
|
-
return
|
|
229
|
+
return CONTEXT_LIMITS["default"];
|
|
288
230
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
231
|
+
function shouldCompact(totalTokens, model, configuredContextLength) {
|
|
232
|
+
const limit = configuredContextLength || getContextLimit(model);
|
|
233
|
+
if (totalTokens >= limit * 0.95)
|
|
234
|
+
return "hard";
|
|
235
|
+
if (totalTokens >= limit * 0.8)
|
|
236
|
+
return "soft";
|
|
237
|
+
return "none";
|
|
238
|
+
}
|
|
239
|
+
async function compactMessages(model, messages, mode) {
|
|
240
|
+
if (messages.length <= 4)
|
|
241
|
+
return messages;
|
|
242
|
+
const keepCount = mode === "hard" ? 2 : 4;
|
|
243
|
+
const toCompact = messages.slice(0, -keepCount);
|
|
244
|
+
const toKeep = messages.slice(-keepCount);
|
|
245
|
+
if (toCompact.length === 0)
|
|
246
|
+
return messages;
|
|
247
|
+
const summaryText = toCompact.map((m) => `${m.role}: ${m.content.slice(0, 500)}`).join("\n\n");
|
|
291
248
|
try {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
249
|
+
const result = await generateText({
|
|
250
|
+
model,
|
|
251
|
+
system: "Summarize this conversation history concisely. Focus on: what was discussed, what decisions were made, what files were modified, and what the current state of work is. Be brief but preserve all important context.",
|
|
252
|
+
prompt: summaryText
|
|
253
|
+
});
|
|
254
|
+
return [
|
|
255
|
+
{ role: "assistant", content: `[Conversation summary]
|
|
256
|
+
${result.text}` },
|
|
257
|
+
...toKeep
|
|
258
|
+
];
|
|
297
259
|
} catch {
|
|
260
|
+
return toKeep;
|
|
298
261
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/ui/useAgent.ts
|
|
265
|
+
var DANGEROUS_PATTERNS = [
|
|
266
|
+
{
|
|
267
|
+
pattern: /rm\s+(-[a-z]*f|-[a-z]*r|--force|--recursive)/i,
|
|
268
|
+
label: "recursive/forced delete"
|
|
269
|
+
},
|
|
270
|
+
{ pattern: /git\s+reset\s+--hard/i, label: "hard reset" },
|
|
271
|
+
{ pattern: /git\s+push\s+.*--force/i, label: "force push" },
|
|
272
|
+
{ pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
|
|
273
|
+
{ pattern: /drop\s+table/i, label: "drop table" },
|
|
274
|
+
{ pattern: /truncate\s+/i, label: "truncate" },
|
|
275
|
+
{ pattern: /chmod\s+777/i, label: "chmod 777" }
|
|
276
|
+
];
|
|
277
|
+
var READ_TOOLS = /* @__PURE__ */ new Set([
|
|
278
|
+
"read_file",
|
|
279
|
+
"glob",
|
|
280
|
+
"grep",
|
|
281
|
+
"ls",
|
|
282
|
+
"sub_agent"
|
|
283
|
+
]);
|
|
284
|
+
function buildSystemPrompt(workingDir) {
|
|
285
|
+
return `You are WorkerMill, an AI coding agent running in the user's terminal.
|
|
286
|
+
|
|
287
|
+
Working directory: ${workingDir}
|
|
288
|
+
|
|
289
|
+
## How to behave
|
|
290
|
+
|
|
291
|
+
- Be concise. Short replies unless the task demands detail.
|
|
292
|
+
- If the user says hello or asks a casual question, respond briefly. Do NOT explore the codebase, read files, or use tools unless the user asks you to do something specific.
|
|
293
|
+
- Only use tools when you have a concrete task. "Hello" is not a task.
|
|
294
|
+
- When you DO have a task, read relevant files first, make changes, and verify they work.
|
|
295
|
+
- Prefer editing existing files over creating new ones.
|
|
296
|
+
- Run tests after changes when test infrastructure exists.
|
|
297
|
+
|
|
298
|
+
## Communication style
|
|
299
|
+
|
|
300
|
+
Direct. No filler. No "Perfect!", "Great!", "Sure!". Lead with substance.
|
|
301
|
+
Do NOT repeat yourself across steps. Each response adds new information only.
|
|
302
|
+
Do NOT list your capabilities unless asked. Do NOT offer menus of options unprompted.
|
|
303
|
+
|
|
304
|
+
## Rules
|
|
305
|
+
|
|
306
|
+
- NEVER start long-running processes (dev servers, watch modes, etc.)
|
|
307
|
+
- NEVER run interactive commands that wait for user input
|
|
308
|
+
- Only run commands that complete and exit
|
|
309
|
+
- If the task specifies a dependency version, use that version. Trust the spec.
|
|
310
|
+
|
|
311
|
+
## Learnings
|
|
312
|
+
|
|
313
|
+
When you discover something non-obvious about this codebase, emit:
|
|
314
|
+
::learning::The test suite requires DATABASE_URL or tests silently skip`;
|
|
315
|
+
}
|
|
316
|
+
function useAgent(options) {
|
|
317
|
+
const modelRef = useRef(null);
|
|
318
|
+
const toolsRef = useRef(null);
|
|
319
|
+
const aiProviderRef = useRef(options.provider);
|
|
320
|
+
const [messages, setMessages] = useState([]);
|
|
321
|
+
const [streamingText, setStreamingText] = useState("");
|
|
322
|
+
const [streamingToolCalls, setStreamingToolCalls] = useState(
|
|
323
|
+
[]
|
|
324
|
+
);
|
|
325
|
+
const [status, setStatus] = useState("idle");
|
|
326
|
+
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
327
|
+
const [tokens, setTokens] = useState(0);
|
|
328
|
+
const [cost, setCost] = useState(0);
|
|
329
|
+
const [trustAll, setTrustAllState] = useState(options.trustAll);
|
|
330
|
+
const [planMode, setPlanModeState] = useState(options.planMode);
|
|
331
|
+
const abortRef = useRef(null);
|
|
332
|
+
const sessionRef = useRef(null);
|
|
333
|
+
const costTrackerRef = useRef(new CostTracker());
|
|
334
|
+
const sessionAllowRef = useRef(/* @__PURE__ */ new Set());
|
|
335
|
+
const trustAllRef = useRef(options.trustAll);
|
|
336
|
+
const planModeRef = useRef(options.planMode);
|
|
337
|
+
const workingDirRef = useRef(process.cwd());
|
|
338
|
+
const initDoneRef = useRef(false);
|
|
339
|
+
trustAllRef.current = trustAll;
|
|
340
|
+
planModeRef.current = planMode;
|
|
341
|
+
if (!initDoneRef.current) {
|
|
342
|
+
initDoneRef.current = true;
|
|
343
|
+
if (options.apiKey) {
|
|
344
|
+
const envMap = {
|
|
345
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
346
|
+
openai: "OPENAI_API_KEY",
|
|
347
|
+
google: "GOOGLE_API_KEY"
|
|
348
|
+
};
|
|
349
|
+
const envVar = envMap[options.provider];
|
|
350
|
+
if (envVar && !process.env[envVar]) {
|
|
351
|
+
process.env[envVar] = options.apiKey;
|
|
338
352
|
}
|
|
339
|
-
const spinner = ora({ stream: process.stdout, text: "Compacting conversation...", prefixText: " " }).start();
|
|
340
|
-
const compacted = await compactMessages(model, plainMessages, "soft");
|
|
341
|
-
ctx.session.messages = compacted.map((m) => ({
|
|
342
|
-
role: m.role,
|
|
343
|
-
content: m.content,
|
|
344
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
345
|
-
}));
|
|
346
|
-
saveSession(ctx.session);
|
|
347
|
-
spinner.succeed(`Compacted to ${ctx.session.messages.length} messages`);
|
|
348
|
-
console.log();
|
|
349
|
-
break;
|
|
350
353
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
354
|
+
aiProviderRef.current = options.provider;
|
|
355
|
+
modelRef.current = createModel(
|
|
356
|
+
aiProviderRef.current,
|
|
357
|
+
options.model,
|
|
358
|
+
options.host,
|
|
359
|
+
options.contextLength
|
|
360
|
+
);
|
|
361
|
+
toolsRef.current = createToolDefinitions(
|
|
362
|
+
workingDirRef.current,
|
|
363
|
+
modelRef.current,
|
|
364
|
+
options.sandboxed
|
|
365
|
+
);
|
|
366
|
+
if (options.resume) {
|
|
367
|
+
const loaded = loadLatestSession();
|
|
368
|
+
if (loaded) {
|
|
369
|
+
sessionRef.current = loaded;
|
|
370
|
+
const restored = loaded.messages.map((m) => ({
|
|
371
|
+
id: crypto2.randomUUID(),
|
|
372
|
+
role: m.role,
|
|
373
|
+
content: m.content,
|
|
374
|
+
timestamp: m.timestamp
|
|
375
|
+
}));
|
|
376
|
+
sessionRef.current._restored = restored;
|
|
377
|
+
} else {
|
|
378
|
+
sessionRef.current = createSession(options.provider, options.model);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
sessionRef.current = createSession(options.provider, options.model);
|
|
366
382
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
383
|
+
}
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
const s = sessionRef.current;
|
|
386
|
+
if (s._restored) {
|
|
387
|
+
setMessages(s._restored);
|
|
388
|
+
delete s._restored;
|
|
389
|
+
}
|
|
390
|
+
}, []);
|
|
391
|
+
function detectDanger(toolName, toolInput) {
|
|
392
|
+
if (toolName !== "bash") return null;
|
|
393
|
+
const command = String(toolInput.command ?? "");
|
|
394
|
+
for (const { pattern, label } of DANGEROUS_PATTERNS) {
|
|
395
|
+
if (pattern.test(command)) return label;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const checkPermission = useCallback(
|
|
400
|
+
(toolName, toolInput) => {
|
|
401
|
+
const dangerLabel = detectDanger(toolName, toolInput);
|
|
402
|
+
if (dangerLabel) {
|
|
403
|
+
return new Promise((resolve) => {
|
|
404
|
+
setPermissionRequest({
|
|
405
|
+
toolName,
|
|
406
|
+
toolInput,
|
|
407
|
+
isDangerous: true,
|
|
408
|
+
dangerLabel,
|
|
409
|
+
resolve: (allowed, mode) => {
|
|
410
|
+
setPermissionRequest(null);
|
|
411
|
+
resolve({ allowed, mode });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (trustAllRef.current) {
|
|
417
|
+
return Promise.resolve({ allowed: true });
|
|
418
|
+
}
|
|
419
|
+
if (READ_TOOLS.has(toolName)) {
|
|
420
|
+
return Promise.resolve({ allowed: true });
|
|
421
|
+
}
|
|
422
|
+
if (sessionAllowRef.current.has(toolName)) {
|
|
423
|
+
return Promise.resolve({ allowed: true });
|
|
424
|
+
}
|
|
425
|
+
return new Promise((resolve) => {
|
|
426
|
+
setPermissionRequest({
|
|
427
|
+
toolName,
|
|
428
|
+
toolInput,
|
|
429
|
+
isDangerous: false,
|
|
430
|
+
resolve: (allowed, mode) => {
|
|
431
|
+
setPermissionRequest(null);
|
|
432
|
+
resolve({ allowed, mode });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
[]
|
|
438
|
+
// trustAllRef and sessionAllowRef are refs -- stable across renders.
|
|
439
|
+
);
|
|
440
|
+
const buildPermissionedTools = useCallback(() => {
|
|
441
|
+
const raw = toolsRef.current;
|
|
442
|
+
if (!raw) return {};
|
|
443
|
+
const wrapped = {};
|
|
444
|
+
for (const [name, toolDef] of Object.entries(raw)) {
|
|
445
|
+
const td = toolDef;
|
|
446
|
+
wrapped[name] = {
|
|
447
|
+
...td,
|
|
448
|
+
execute: async (input) => {
|
|
449
|
+
const callId = crypto2.randomUUID();
|
|
450
|
+
const info = {
|
|
451
|
+
id: callId,
|
|
452
|
+
name,
|
|
453
|
+
input,
|
|
454
|
+
status: "pending"
|
|
455
|
+
};
|
|
456
|
+
setStreamingToolCalls((prev) => [...prev, info]);
|
|
457
|
+
setMessages((prev) => [
|
|
458
|
+
...prev,
|
|
459
|
+
{
|
|
460
|
+
id: `tc-${callId}`,
|
|
461
|
+
role: "assistant",
|
|
462
|
+
content: "",
|
|
463
|
+
toolCalls: [{ ...info, status: "running" }],
|
|
464
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
465
|
+
}
|
|
466
|
+
]);
|
|
467
|
+
setStatus("permission");
|
|
468
|
+
const { allowed, mode } = await checkPermission(name, input);
|
|
469
|
+
if (mode === "trust") {
|
|
470
|
+
setTrustAllState(true);
|
|
471
|
+
} else if (mode === "always") {
|
|
472
|
+
sessionAllowRef.current.add(name);
|
|
473
|
+
}
|
|
474
|
+
if (!allowed) {
|
|
475
|
+
setStreamingToolCalls(
|
|
476
|
+
(prev) => prev.map(
|
|
477
|
+
(tc) => tc.id === callId ? { ...tc, status: "denied" } : tc
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
setStatus("streaming");
|
|
481
|
+
return "Tool execution denied by user.";
|
|
482
|
+
}
|
|
483
|
+
setStreamingToolCalls(
|
|
484
|
+
(prev) => prev.map(
|
|
485
|
+
(tc) => tc.id === callId ? { ...tc, status: "running" } : tc
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
setStatus("tool_running");
|
|
489
|
+
try {
|
|
490
|
+
const result = await td.execute(input);
|
|
491
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
492
|
+
setStreamingToolCalls(
|
|
493
|
+
(prev) => prev.map(
|
|
494
|
+
(tc) => tc.id === callId ? { ...tc, status: "done", result: resultStr } : tc
|
|
495
|
+
)
|
|
496
|
+
);
|
|
497
|
+
setStatus("streaming");
|
|
498
|
+
return result;
|
|
499
|
+
} catch (err) {
|
|
500
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
501
|
+
setStreamingToolCalls(
|
|
502
|
+
(prev) => prev.map(
|
|
503
|
+
(tc) => tc.id === callId ? {
|
|
504
|
+
...tc,
|
|
505
|
+
status: "done",
|
|
506
|
+
result: `Error: ${errMsg}`
|
|
507
|
+
} : tc
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
setStatus("streaming");
|
|
511
|
+
throw err;
|
|
382
512
|
}
|
|
383
|
-
} else {
|
|
384
|
-
console.log(chalk2.dim(" Working tree clean"));
|
|
385
513
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return wrapped;
|
|
517
|
+
}, [checkPermission]);
|
|
518
|
+
const getActiveTools = useCallback(() => {
|
|
519
|
+
const all = buildPermissionedTools();
|
|
520
|
+
if (!planModeRef.current) return all;
|
|
521
|
+
const filtered = {};
|
|
522
|
+
for (const [name, def] of Object.entries(all)) {
|
|
523
|
+
if (READ_TOOLS.has(name)) {
|
|
524
|
+
filtered[name] = def;
|
|
389
525
|
}
|
|
390
|
-
break;
|
|
391
526
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
console.log(chalk2.red(`
|
|
402
|
-
Session not found: ${deleteId}
|
|
403
|
-
`));
|
|
527
|
+
return filtered;
|
|
528
|
+
}, [buildPermissionedTools]);
|
|
529
|
+
const submit = useCallback(
|
|
530
|
+
(input) => {
|
|
531
|
+
void (async () => {
|
|
532
|
+
const session = sessionRef.current;
|
|
533
|
+
addMessage(session, "user", input);
|
|
534
|
+
if (!session.name) {
|
|
535
|
+
session.name = input.slice(0, 50).replace(/\n/g, " ");
|
|
404
536
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
537
|
+
const userMsg = {
|
|
538
|
+
id: crypto2.randomUUID(),
|
|
539
|
+
role: "user",
|
|
540
|
+
content: input,
|
|
541
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
542
|
+
};
|
|
543
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
544
|
+
setStreamingText("");
|
|
545
|
+
setStreamingToolCalls([]);
|
|
546
|
+
setStatus("thinking");
|
|
547
|
+
const controller = new AbortController();
|
|
548
|
+
abortRef.current = controller;
|
|
549
|
+
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1e3);
|
|
550
|
+
try {
|
|
551
|
+
const model = modelRef.current;
|
|
552
|
+
const systemPrompt = buildSystemPrompt(workingDirRef.current);
|
|
553
|
+
const stream = streamText({
|
|
554
|
+
model,
|
|
555
|
+
system: systemPrompt,
|
|
556
|
+
messages: session.messages.map((m) => ({
|
|
557
|
+
role: m.role,
|
|
558
|
+
content: m.content
|
|
559
|
+
})),
|
|
560
|
+
tools: getActiveTools(),
|
|
561
|
+
stopWhen: stepCountIs(100),
|
|
562
|
+
abortSignal: controller.signal,
|
|
563
|
+
...buildOllamaOptions(
|
|
564
|
+
aiProviderRef.current,
|
|
565
|
+
options.contextLength
|
|
566
|
+
),
|
|
567
|
+
onStepFinish({ text }) {
|
|
568
|
+
if (text) {
|
|
569
|
+
setMessages((prev) => [
|
|
570
|
+
...prev,
|
|
571
|
+
{
|
|
572
|
+
id: crypto2.randomUUID(),
|
|
573
|
+
role: "assistant",
|
|
574
|
+
content: text,
|
|
575
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
576
|
+
}
|
|
577
|
+
]);
|
|
578
|
+
setStreamingText(text);
|
|
579
|
+
setStatus("streaming");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
for await (const _chunk of stream.textStream) {
|
|
415
584
|
}
|
|
585
|
+
clearTimeout(timeoutId);
|
|
586
|
+
const finalText = await stream.text;
|
|
587
|
+
const usage = await stream.totalUsage;
|
|
588
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
589
|
+
const outputTokens = usage?.outputTokens ?? 0;
|
|
590
|
+
setStreamingToolCalls([]);
|
|
591
|
+
setStreamingText("");
|
|
592
|
+
addMessage(session, "assistant", finalText);
|
|
593
|
+
session.totalTokens += inputTokens + outputTokens;
|
|
594
|
+
costTrackerRef.current.addUsage(
|
|
595
|
+
"agent",
|
|
596
|
+
options.provider,
|
|
597
|
+
options.model,
|
|
598
|
+
inputTokens,
|
|
599
|
+
outputTokens
|
|
600
|
+
);
|
|
601
|
+
setTokens(inputTokens);
|
|
602
|
+
setCost(costTrackerRef.current.getTotalCost());
|
|
603
|
+
saveSession(session);
|
|
604
|
+
const compactionLevel = shouldCompact(
|
|
605
|
+
inputTokens,
|
|
606
|
+
options.model,
|
|
607
|
+
options.contextLength
|
|
608
|
+
);
|
|
609
|
+
if (compactionLevel !== "none") {
|
|
610
|
+
setStatus("thinking");
|
|
611
|
+
const plainMessages = session.messages.map((m) => ({
|
|
612
|
+
role: m.role,
|
|
613
|
+
content: m.content
|
|
614
|
+
}));
|
|
615
|
+
const compacted = await compactMessages(
|
|
616
|
+
model,
|
|
617
|
+
plainMessages,
|
|
618
|
+
compactionLevel
|
|
619
|
+
);
|
|
620
|
+
session.messages = compacted.map((m) => ({
|
|
621
|
+
role: m.role,
|
|
622
|
+
content: m.content,
|
|
623
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
624
|
+
}));
|
|
625
|
+
saveSession(session);
|
|
626
|
+
}
|
|
627
|
+
setStatus("idle");
|
|
628
|
+
abortRef.current = null;
|
|
629
|
+
} catch (err) {
|
|
630
|
+
clearTimeout(timeoutId);
|
|
631
|
+
abortRef.current = null;
|
|
632
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
633
|
+
setStatus("idle");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const errText = err instanceof Error ? err.message : String(err);
|
|
637
|
+
const errorMsg = {
|
|
638
|
+
id: crypto2.randomUUID(),
|
|
639
|
+
role: "assistant",
|
|
640
|
+
content: `Error: ${errText}`,
|
|
641
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
642
|
+
};
|
|
643
|
+
setMessages((prev) => [...prev, errorMsg]);
|
|
644
|
+
setStreamingText("");
|
|
645
|
+
setStreamingToolCalls([]);
|
|
646
|
+
setStatus("idle");
|
|
416
647
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
648
|
+
})();
|
|
649
|
+
},
|
|
650
|
+
[getActiveTools, options.provider, options.model, options.contextLength]
|
|
651
|
+
);
|
|
652
|
+
const cancel = useCallback(() => {
|
|
653
|
+
if (abortRef.current) {
|
|
654
|
+
abortRef.current.abort();
|
|
655
|
+
abortRef.current = null;
|
|
656
|
+
killActiveProcess();
|
|
657
|
+
}
|
|
658
|
+
setStatus("idle");
|
|
659
|
+
setStreamingText("");
|
|
660
|
+
setStreamingToolCalls([]);
|
|
661
|
+
setPermissionRequest(null);
|
|
662
|
+
}, []);
|
|
663
|
+
const setTrustAll = useCallback((v) => {
|
|
664
|
+
setTrustAllState(v);
|
|
665
|
+
trustAllRef.current = v;
|
|
666
|
+
}, []);
|
|
667
|
+
const setPlanMode = useCallback((v) => {
|
|
668
|
+
setPlanModeState(v);
|
|
669
|
+
planModeRef.current = v;
|
|
670
|
+
}, []);
|
|
671
|
+
const addSystemMessage = useCallback((content) => {
|
|
672
|
+
const msg = {
|
|
673
|
+
id: crypto2.randomUUID(),
|
|
674
|
+
role: "assistant",
|
|
675
|
+
content,
|
|
676
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
677
|
+
};
|
|
678
|
+
setMessages((prev) => [...prev, msg]);
|
|
679
|
+
}, []);
|
|
680
|
+
const addUserMessage = useCallback((content) => {
|
|
681
|
+
const msg = {
|
|
682
|
+
id: crypto2.randomUUID(),
|
|
683
|
+
role: "user",
|
|
684
|
+
content,
|
|
685
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
686
|
+
};
|
|
687
|
+
setMessages((prev) => [...prev, msg]);
|
|
688
|
+
}, []);
|
|
689
|
+
return {
|
|
690
|
+
messages,
|
|
691
|
+
streamingText,
|
|
692
|
+
streamingToolCalls,
|
|
693
|
+
status,
|
|
694
|
+
permissionRequest,
|
|
695
|
+
tokens,
|
|
696
|
+
cost,
|
|
697
|
+
session: sessionRef.current,
|
|
698
|
+
submit,
|
|
699
|
+
cancel,
|
|
700
|
+
setTrustAll,
|
|
701
|
+
setPlanMode,
|
|
702
|
+
addSystemMessage,
|
|
703
|
+
addUserMessage
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/ui/useOrchestrator.ts
|
|
708
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
709
|
+
var PERSONA_EMOJIS = {
|
|
710
|
+
frontend_developer: "\u{1F3A8}",
|
|
711
|
+
// 🎨
|
|
712
|
+
backend_developer: "\u{1F4BB}",
|
|
713
|
+
// 💻
|
|
714
|
+
fullstack_developer: "\u{1F4BB}",
|
|
715
|
+
// 💻
|
|
716
|
+
devops_engineer: "\u{1F527}",
|
|
717
|
+
// 🔧
|
|
718
|
+
security_engineer: "\u{1F512}",
|
|
719
|
+
// 🔐
|
|
720
|
+
qa_engineer: "\u{1F9EA}",
|
|
721
|
+
// 🧪
|
|
722
|
+
tech_writer: "\u{1F4DD}",
|
|
723
|
+
// 📝
|
|
724
|
+
project_manager: "\u{1F4CB}",
|
|
725
|
+
// 📋
|
|
726
|
+
architect: "\u{1F3D7}\uFE0F",
|
|
727
|
+
// 🏗️
|
|
728
|
+
database_engineer: "\u{1F4CA}",
|
|
729
|
+
// 📊
|
|
730
|
+
data_engineer: "\u{1F4CA}",
|
|
731
|
+
// 📊
|
|
732
|
+
data_ml_engineer: "\u{1F4CA}",
|
|
733
|
+
// 📊
|
|
734
|
+
ml_engineer: "\u{1F4CA}",
|
|
735
|
+
// 📊
|
|
736
|
+
mobile_developer: "\u{1F4F1}",
|
|
737
|
+
// 📱
|
|
738
|
+
tech_lead: "\u{1F451}",
|
|
739
|
+
// 👑
|
|
740
|
+
manager: "\u{1F454}",
|
|
741
|
+
// 👔
|
|
742
|
+
support_agent: "\u{1F4AC}",
|
|
743
|
+
// 💬
|
|
744
|
+
planner: "\u{1F4A1}",
|
|
745
|
+
// 💡
|
|
746
|
+
coordinator: "\u{1F3AF}",
|
|
747
|
+
// 🎯
|
|
748
|
+
critic: "\u{1F50D}",
|
|
749
|
+
// 🔍
|
|
750
|
+
reviewer: "\u{1F50D}"
|
|
751
|
+
// 🔍
|
|
752
|
+
};
|
|
753
|
+
function getEmoji(persona) {
|
|
754
|
+
return PERSONA_EMOJIS[persona] || "\u{1F916}";
|
|
755
|
+
}
|
|
756
|
+
function useOrchestrator(addMessage2) {
|
|
757
|
+
const [running, setRunning] = useState2(false);
|
|
758
|
+
const [statusMessage, setStatusMessage] = useState2("");
|
|
759
|
+
const [confirmRequest, setConfirmRequest] = useState2(null);
|
|
760
|
+
const start = useCallback2(
|
|
761
|
+
(task, trustAll, sandboxed) => {
|
|
762
|
+
setRunning(true);
|
|
763
|
+
setStatusMessage("");
|
|
764
|
+
setConfirmRequest(null);
|
|
765
|
+
void (async () => {
|
|
766
|
+
try {
|
|
767
|
+
const config = loadConfig();
|
|
768
|
+
if (!config) {
|
|
769
|
+
addMessage2(
|
|
770
|
+
"No provider configured. Run `workermill` (setup) first."
|
|
771
|
+
);
|
|
772
|
+
setRunning(false);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const { classifyComplexity, runOrchestration } = await import("./orchestrator-5I7BGPC7.js");
|
|
776
|
+
const output = {
|
|
777
|
+
log(persona, message) {
|
|
778
|
+
const emoji = getEmoji(persona);
|
|
779
|
+
const trimmed = message.trim();
|
|
780
|
+
if (trimmed) {
|
|
781
|
+
addMessage2(`[${emoji} ${persona}] ${trimmed}`);
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
coordinatorLog(message) {
|
|
785
|
+
addMessage2(`[${getEmoji("coordinator")} coordinator] ${message}`);
|
|
786
|
+
},
|
|
787
|
+
error(message) {
|
|
788
|
+
addMessage2(`**Error:** ${message}`);
|
|
789
|
+
},
|
|
790
|
+
status(message) {
|
|
791
|
+
setStatusMessage(message);
|
|
792
|
+
},
|
|
793
|
+
statusDone(message) {
|
|
794
|
+
if (message) {
|
|
795
|
+
addMessage2(message);
|
|
796
|
+
}
|
|
797
|
+
setStatusMessage("");
|
|
798
|
+
},
|
|
799
|
+
confirm(prompt) {
|
|
800
|
+
return new Promise((resolve) => {
|
|
801
|
+
setConfirmRequest({
|
|
802
|
+
prompt,
|
|
803
|
+
resolve: (yes) => {
|
|
804
|
+
setConfirmRequest(null);
|
|
805
|
+
resolve(yes);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
},
|
|
810
|
+
toolCall(persona, toolName, toolInput) {
|
|
811
|
+
let detail = "";
|
|
812
|
+
if (toolInput.file_path) {
|
|
813
|
+
detail = String(toolInput.file_path);
|
|
814
|
+
} else if (toolInput.path) {
|
|
815
|
+
detail = String(toolInput.path);
|
|
816
|
+
} else if (toolInput.command) {
|
|
817
|
+
const cmd = String(toolInput.command);
|
|
818
|
+
detail = cmd.length > 120 ? cmd.slice(0, 117) + "..." : cmd;
|
|
819
|
+
} else if (toolInput.query) {
|
|
820
|
+
detail = String(toolInput.query).slice(0, 120);
|
|
821
|
+
} else if (toolInput.prompt) {
|
|
822
|
+
detail = String(toolInput.prompt).slice(0, 120);
|
|
823
|
+
} else if (toolInput.pattern) {
|
|
824
|
+
detail = `pattern: ${String(toolInput.pattern)}`;
|
|
825
|
+
} else if (toolInput.url) {
|
|
826
|
+
detail = String(toolInput.url);
|
|
827
|
+
} else if (toolInput.action) {
|
|
828
|
+
detail = String(toolInput.action);
|
|
829
|
+
} else {
|
|
830
|
+
const keys = Object.keys(toolInput).slice(0, 3);
|
|
831
|
+
if (keys.length > 0) {
|
|
832
|
+
detail = keys.map((k) => `${k}: ${String(toolInput[k]).slice(0, 80)}`).join(", ");
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const emoji = getEmoji(persona);
|
|
836
|
+
addMessage2(
|
|
837
|
+
`[${emoji} ${persona}] \u2193 ${toolName}${detail ? " " + detail : ""}`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
addMessage2("Analyzing task complexity\u2026");
|
|
842
|
+
const classification = await classifyComplexity(config, task, output);
|
|
843
|
+
if (!classification.isMulti) {
|
|
844
|
+
addMessage2(
|
|
845
|
+
`Task classified as single-agent (${classification.reason}). Use a normal prompt instead of /build for single tasks.`
|
|
846
|
+
);
|
|
847
|
+
setRunning(false);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
addMessage2(
|
|
851
|
+
`Task classified as multi-expert: ${classification.reason}`
|
|
852
|
+
);
|
|
853
|
+
await runOrchestration(config, task, trustAll, sandboxed, output);
|
|
854
|
+
addMessage2("**Orchestration complete.**");
|
|
855
|
+
} catch (err) {
|
|
856
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
857
|
+
addMessage2(`**Orchestration failed:** ${msg}`);
|
|
858
|
+
} finally {
|
|
859
|
+
setRunning(false);
|
|
860
|
+
setStatusMessage("");
|
|
861
|
+
setConfirmRequest(null);
|
|
426
862
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
863
|
+
})();
|
|
864
|
+
},
|
|
865
|
+
[addMessage2]
|
|
866
|
+
);
|
|
867
|
+
return { running, start, statusMessage, confirmRequest };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/ui/App.tsx
|
|
871
|
+
import React3, { useRef as useRef2 } from "react";
|
|
872
|
+
import { Box as Box6, Text as Text6, Static, useApp, useInput as useInput3, useStdout as useStdout2 } from "ink";
|
|
873
|
+
|
|
874
|
+
// src/ui/Markdown.tsx
|
|
875
|
+
import { Box, Text } from "ink";
|
|
876
|
+
|
|
877
|
+
// src/ui/theme.ts
|
|
878
|
+
var theme = {
|
|
879
|
+
/** Brand/diamond color — warm orange. */
|
|
880
|
+
brand: "#D77757",
|
|
881
|
+
/** Primary text on dark background. */
|
|
882
|
+
text: "#FFFFFF",
|
|
883
|
+
/** Subtle/dim text — light gray. */
|
|
884
|
+
subtle: "#AFAFAF",
|
|
885
|
+
/** Subtle/dim text — dark gray. */
|
|
886
|
+
subtleDark: "#505050",
|
|
887
|
+
/** Pure black background. */
|
|
888
|
+
background: "#000000",
|
|
889
|
+
/** User message background tint. */
|
|
890
|
+
userBg: "#373737",
|
|
891
|
+
/** Success indicators. */
|
|
892
|
+
success: "#4EBA65",
|
|
893
|
+
/** Warning indicators. */
|
|
894
|
+
warning: "#FFCC00",
|
|
895
|
+
/** Error indicators. */
|
|
896
|
+
error: "#FF6B80",
|
|
897
|
+
/** Permission/suggestion accent — blue-purple. */
|
|
898
|
+
permission: "#5769F7",
|
|
899
|
+
/** Ice blue for highlights. */
|
|
900
|
+
iceBlue: "#ADD8E6",
|
|
901
|
+
/** Professional blue for Read/file tools. */
|
|
902
|
+
blue: "#6A9BCC",
|
|
903
|
+
/** Bash border — pink/magenta. */
|
|
904
|
+
bashBorder: "#FD5DB1",
|
|
905
|
+
/** Inactive/disabled elements. */
|
|
906
|
+
inactive: "#666666",
|
|
907
|
+
/** Default border color. */
|
|
908
|
+
border: "gray"
|
|
909
|
+
};
|
|
910
|
+
var toolColors = {
|
|
911
|
+
Read: theme.blue,
|
|
912
|
+
Write: theme.warning,
|
|
913
|
+
Edit: theme.warning,
|
|
914
|
+
Bash: theme.bashBorder,
|
|
915
|
+
Glob: theme.iceBlue,
|
|
916
|
+
Grep: theme.iceBlue,
|
|
917
|
+
List: theme.subtle,
|
|
918
|
+
Fetch: theme.blue,
|
|
919
|
+
Patch: theme.warning,
|
|
920
|
+
Agent: theme.permission,
|
|
921
|
+
Git: theme.success,
|
|
922
|
+
Search: theme.blue,
|
|
923
|
+
Todo: theme.subtle
|
|
924
|
+
};
|
|
925
|
+
var syntax = {
|
|
926
|
+
keyword: "#C586C0",
|
|
927
|
+
string: "#4EBA65",
|
|
928
|
+
comment: "#666666",
|
|
929
|
+
type: "#FFCC00",
|
|
930
|
+
number: "#B5CEA8",
|
|
931
|
+
punctuation: "#AFAFAF",
|
|
932
|
+
default: "#FFFFFF"
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// src/ui/Markdown.tsx
|
|
936
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
937
|
+
function highlightCode(line, lang) {
|
|
938
|
+
const parts = [];
|
|
939
|
+
let remaining = line;
|
|
940
|
+
let idx = 0;
|
|
941
|
+
const commentPatterns = [/^(\s*\/\/.*)/, /^(\s*#.*)/, /^(\s*--.*)/, /^(\s*\/\*.*\*\/)/];
|
|
942
|
+
for (const cp of commentPatterns) {
|
|
943
|
+
const cm = remaining.match(cp);
|
|
944
|
+
if (cm) {
|
|
945
|
+
return [/* @__PURE__ */ jsx(Text, { color: syntax.comment, children: remaining }, `c-${idx}`)];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const stringRe = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/;
|
|
949
|
+
const keywordRe = /\b(import|export|from|const|let|var|function|class|interface|type|return|if|else|for|while|do|switch|case|break|continue|new|this|super|extends|implements|async|await|try|catch|finally|throw|typeof|instanceof|in|of|as|default|void|null|undefined|true|false|def|fn|pub|mod|use|struct|enum|impl|trait|match|self|mut|ref|where|crate|macro|move|loop|static|extern|unsafe|dyn)\b/;
|
|
950
|
+
const typeRe = /\b([A-Z][a-zA-Z0-9]+)\b/;
|
|
951
|
+
const numberRe = /\b(\d+\.?\d*(?:e[+-]?\d+)?)\b/;
|
|
952
|
+
while (remaining.length > 0) {
|
|
953
|
+
const candidates = [];
|
|
954
|
+
const sm = stringRe.exec(remaining);
|
|
955
|
+
if (sm) candidates.push({ type: "string", match: sm, pos: sm.index });
|
|
956
|
+
const km = keywordRe.exec(remaining);
|
|
957
|
+
if (km) candidates.push({ type: "keyword", match: km, pos: km.index });
|
|
958
|
+
const tm = typeRe.exec(remaining);
|
|
959
|
+
if (tm) candidates.push({ type: "type", match: tm, pos: tm.index });
|
|
960
|
+
const nm = numberRe.exec(remaining);
|
|
961
|
+
if (nm) candidates.push({ type: "number", match: nm, pos: nm.index });
|
|
962
|
+
candidates.sort((a, b) => a.pos - b.pos);
|
|
963
|
+
const winner = candidates[0];
|
|
964
|
+
if (!winner) {
|
|
965
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: syntax.default, children: remaining }, `d-${idx++}`));
|
|
443
966
|
break;
|
|
444
967
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
968
|
+
if (winner.pos > 0) {
|
|
969
|
+
parts.push(
|
|
970
|
+
/* @__PURE__ */ jsx(Text, { color: syntax.default, children: remaining.slice(0, winner.pos) }, `d-${idx++}`)
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const matchStr = winner.type === "string" ? winner.match[0] : winner.match[1];
|
|
974
|
+
const fullMatch = winner.type === "string" ? winner.match[0] : winner.match[0];
|
|
975
|
+
const color = winner.type === "string" ? syntax.string : winner.type === "keyword" ? syntax.keyword : winner.type === "type" ? syntax.type : syntax.number;
|
|
976
|
+
parts.push(
|
|
977
|
+
/* @__PURE__ */ jsx(Text, { color, children: matchStr }, `h-${idx++}`)
|
|
978
|
+
);
|
|
979
|
+
remaining = remaining.slice(winner.pos + fullMatch.length);
|
|
980
|
+
}
|
|
981
|
+
return parts;
|
|
982
|
+
}
|
|
983
|
+
function renderInline(line, key) {
|
|
984
|
+
const parts = [];
|
|
985
|
+
let remaining = line;
|
|
986
|
+
let idx = 0;
|
|
987
|
+
while (remaining.length > 0) {
|
|
988
|
+
const boldMatch = remaining.match(/^(.*?)\*\*(.+?)\*\*/);
|
|
989
|
+
const boldMatch2 = remaining.match(/^(.*?)__(.+?)__/);
|
|
990
|
+
const bold = boldMatch && (!boldMatch2 || boldMatch.index <= boldMatch2.index) ? boldMatch : boldMatch2;
|
|
991
|
+
const codeMatch = remaining.match(/^(.*?)`([^`]+)`/);
|
|
992
|
+
const candidates = [];
|
|
993
|
+
if (bold) candidates.push({ type: "bold", match: bold, pos: bold[1].length });
|
|
994
|
+
if (codeMatch) candidates.push({ type: "code", match: codeMatch, pos: codeMatch[1].length });
|
|
995
|
+
candidates.sort((a, b) => a.pos - b.pos);
|
|
996
|
+
const winner = candidates[0];
|
|
997
|
+
if (!winner) {
|
|
998
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: theme.text, children: remaining }, `${key}-${idx++}`));
|
|
461
999
|
break;
|
|
462
1000
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1001
|
+
const m = winner.match;
|
|
1002
|
+
if (m[1]) {
|
|
1003
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: theme.text, children: m[1] }, `${key}-${idx++}`));
|
|
1004
|
+
}
|
|
1005
|
+
if (winner.type === "bold") {
|
|
1006
|
+
parts.push(/* @__PURE__ */ jsx(Text, { bold: true, color: theme.text, children: m[2] }, `${key}-${idx++}`));
|
|
1007
|
+
} else {
|
|
1008
|
+
parts.push(/* @__PURE__ */ jsx(Text, { color: theme.iceBlue, children: m[2] }, `${key}-${idx++}`));
|
|
1009
|
+
}
|
|
1010
|
+
remaining = remaining.slice(m[0].length);
|
|
1011
|
+
}
|
|
1012
|
+
if (parts.length === 0) {
|
|
1013
|
+
return /* @__PURE__ */ jsx(Text, { children: " " }, key);
|
|
1014
|
+
}
|
|
1015
|
+
return /* @__PURE__ */ jsx(Text, { children: parts }, key);
|
|
1016
|
+
}
|
|
1017
|
+
function renderCodeBlock(lines, lang, key) {
|
|
1018
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 1, marginY: 0, children: [
|
|
1019
|
+
lang && /* @__PURE__ */ jsx(Text, { color: theme.subtle, children: ` ${lang}` }),
|
|
1020
|
+
/* @__PURE__ */ jsx(
|
|
1021
|
+
Box,
|
|
1022
|
+
{
|
|
1023
|
+
borderStyle: "round",
|
|
1024
|
+
borderColor: theme.subtleDark,
|
|
1025
|
+
paddingX: 1,
|
|
1026
|
+
flexDirection: "column",
|
|
1027
|
+
children: lines.map((codeLine, i) => /* @__PURE__ */ jsx(Text, { children: highlightCode(codeLine || " ", lang) }, `${key}-code-${i}`))
|
|
471
1028
|
}
|
|
472
|
-
|
|
1029
|
+
)
|
|
1030
|
+
] }, key);
|
|
1031
|
+
}
|
|
1032
|
+
function Markdown({ content }) {
|
|
1033
|
+
const sourceLines = content.split("\n");
|
|
1034
|
+
const elements = [];
|
|
1035
|
+
let i = 0;
|
|
1036
|
+
let elemKey = 0;
|
|
1037
|
+
while (i < sourceLines.length) {
|
|
1038
|
+
const line = sourceLines[i];
|
|
1039
|
+
const fenceMatch = line.match(/^```(\w*)/);
|
|
1040
|
+
if (fenceMatch) {
|
|
1041
|
+
const lang = fenceMatch[1] || "";
|
|
1042
|
+
const codeLines = [];
|
|
1043
|
+
i++;
|
|
1044
|
+
while (i < sourceLines.length && !sourceLines[i].startsWith("```")) {
|
|
1045
|
+
codeLines.push(sourceLines[i]);
|
|
1046
|
+
i++;
|
|
1047
|
+
}
|
|
1048
|
+
if (i < sourceLines.length) i++;
|
|
1049
|
+
elements.push(renderCodeBlock(codeLines, lang, `block-${elemKey++}`));
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (/^(---|\*\*\*|___)/.test(line.trim())) {
|
|
1053
|
+
elements.push(
|
|
1054
|
+
/* @__PURE__ */ jsx(Text, { color: theme.subtleDark, children: "\u2500".repeat(60) }, `hr-${elemKey++}`)
|
|
1055
|
+
);
|
|
1056
|
+
i++;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.*)/);
|
|
1060
|
+
if (headerMatch) {
|
|
1061
|
+
const depth = headerMatch[1].length;
|
|
1062
|
+
const text = headerMatch[2];
|
|
1063
|
+
elements.push(
|
|
1064
|
+
/* @__PURE__ */ jsx(
|
|
1065
|
+
Text,
|
|
1066
|
+
{
|
|
1067
|
+
bold: true,
|
|
1068
|
+
color: depth <= 2 ? theme.text : theme.subtle,
|
|
1069
|
+
children: text
|
|
1070
|
+
},
|
|
1071
|
+
`h-${elemKey++}`
|
|
1072
|
+
)
|
|
1073
|
+
);
|
|
1074
|
+
i++;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (line.startsWith(">")) {
|
|
1078
|
+
const quoteText = line.replace(/^>\s?/, "");
|
|
1079
|
+
elements.push(
|
|
1080
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1081
|
+
/* @__PURE__ */ jsx(Text, { color: theme.subtleDark, children: " \u2502 " }),
|
|
1082
|
+
/* @__PURE__ */ jsx(Text, { color: theme.subtle, children: quoteText })
|
|
1083
|
+
] }, `bq-${elemKey++}`)
|
|
1084
|
+
);
|
|
1085
|
+
i++;
|
|
1086
|
+
continue;
|
|
473
1087
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1088
|
+
const listMatch = line.match(/^(\s*)[-*]\s+(.*)/);
|
|
1089
|
+
if (listMatch) {
|
|
1090
|
+
const indent = listMatch[1] || "";
|
|
1091
|
+
const text = listMatch[2];
|
|
1092
|
+
elements.push(
|
|
1093
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1094
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.subtle, children: [
|
|
1095
|
+
indent,
|
|
1096
|
+
" ",
|
|
1097
|
+
"\u2022 "
|
|
1098
|
+
] }),
|
|
1099
|
+
renderInline(text, `li-inline-${elemKey}`)
|
|
1100
|
+
] }, `li-${elemKey++}`)
|
|
1101
|
+
);
|
|
1102
|
+
i++;
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const olMatch = line.match(/^(\s*)\d+\.\s+(.*)/);
|
|
1106
|
+
if (olMatch) {
|
|
1107
|
+
const indent = olMatch[1] || "";
|
|
1108
|
+
const text = olMatch[2];
|
|
1109
|
+
const numMatch = line.match(/^(\s*)(\d+)\./);
|
|
1110
|
+
const num = numMatch ? numMatch[2] : "1";
|
|
1111
|
+
elements.push(
|
|
1112
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1113
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.subtle, children: [
|
|
1114
|
+
indent,
|
|
1115
|
+
" ",
|
|
1116
|
+
num,
|
|
1117
|
+
". "
|
|
1118
|
+
] }),
|
|
1119
|
+
renderInline(text, `ol-inline-${elemKey}`)
|
|
1120
|
+
] }, `ol-${elemKey++}`)
|
|
1121
|
+
);
|
|
1122
|
+
i++;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (line.trim() === "") {
|
|
1126
|
+
elements.push(/* @__PURE__ */ jsx(Text, { children: " " }, `empty-${elemKey++}`));
|
|
1127
|
+
i++;
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
elements.push(
|
|
1131
|
+
/* @__PURE__ */ jsx(Box, { children: renderInline(line, `inline-${elemKey}`) }, `p-${elemKey++}`)
|
|
1132
|
+
);
|
|
1133
|
+
i++;
|
|
1134
|
+
}
|
|
1135
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: elements });
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/ui/ToolCall.tsx
|
|
1139
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1140
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1141
|
+
var LABELS = {
|
|
1142
|
+
bash: "Bash",
|
|
1143
|
+
read_file: "Read",
|
|
1144
|
+
write_file: "Write",
|
|
1145
|
+
edit_file: "Edit",
|
|
1146
|
+
glob: "Glob",
|
|
1147
|
+
grep: "Grep",
|
|
1148
|
+
ls: "List",
|
|
1149
|
+
fetch: "Fetch",
|
|
1150
|
+
patch: "Patch",
|
|
1151
|
+
sub_agent: "Agent",
|
|
1152
|
+
git: "Git",
|
|
1153
|
+
web_search: "Search",
|
|
1154
|
+
todo: "Todo"
|
|
1155
|
+
};
|
|
1156
|
+
var SPINNER_FRAMES = ["\u28CB", "\u28D9", "\u28F9", "\u28F8", "\u28FC", "\u28F4", "\u28E6", "\u28E7", "\u28C7", "\u28CF"];
|
|
1157
|
+
function extractDetail(input) {
|
|
1158
|
+
if (input.file_path) return String(input.file_path);
|
|
1159
|
+
if (input.path) return String(input.path);
|
|
1160
|
+
if (input.command) {
|
|
1161
|
+
const cmd = String(input.command);
|
|
1162
|
+
return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
|
|
1163
|
+
}
|
|
1164
|
+
if (input.pattern) return `pattern: ${String(input.pattern)}`;
|
|
1165
|
+
if (input.query) return String(input.query).slice(0, 80);
|
|
1166
|
+
const keys = Object.keys(input).slice(0, 2);
|
|
1167
|
+
if (keys.length === 0) return "";
|
|
1168
|
+
return keys.map((k) => {
|
|
1169
|
+
const v = String(input[k]);
|
|
1170
|
+
return `${k}: ${v.length > 40 ? v.slice(0, 37) + "..." : v}`;
|
|
1171
|
+
}).join(", ");
|
|
1172
|
+
}
|
|
1173
|
+
function ToolCallDisplay({ tool }) {
|
|
1174
|
+
const label = LABELS[tool.name] || tool.name;
|
|
1175
|
+
const detail = extractDetail(tool.input);
|
|
1176
|
+
const labelColor = toolColors[label] || theme.blue;
|
|
1177
|
+
let statusIcon;
|
|
1178
|
+
switch (tool.status) {
|
|
1179
|
+
case "done":
|
|
1180
|
+
statusIcon = /* @__PURE__ */ jsx2(Text2, { color: theme.success, children: "\u2713" });
|
|
1181
|
+
break;
|
|
1182
|
+
case "denied":
|
|
1183
|
+
statusIcon = /* @__PURE__ */ jsx2(Text2, { color: theme.error, children: "\u2717" });
|
|
1184
|
+
break;
|
|
1185
|
+
case "running": {
|
|
1186
|
+
const frame = SPINNER_FRAMES[Math.floor(Date.now() / 100) % SPINNER_FRAMES.length];
|
|
1187
|
+
statusIcon = /* @__PURE__ */ jsx2(Text2, { color: theme.warning, children: frame });
|
|
480
1188
|
break;
|
|
481
1189
|
}
|
|
482
1190
|
default:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
`));
|
|
1191
|
+
statusIcon = /* @__PURE__ */ jsx2(Text2, { color: theme.subtle, children: "\u2193" });
|
|
1192
|
+
break;
|
|
486
1193
|
}
|
|
1194
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1195
|
+
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
1196
|
+
statusIcon,
|
|
1197
|
+
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
1198
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: labelColor, children: label }),
|
|
1199
|
+
detail ? /* @__PURE__ */ jsxs2(Text2, { color: theme.subtle, children: [
|
|
1200
|
+
" ",
|
|
1201
|
+
detail
|
|
1202
|
+
] }) : null
|
|
1203
|
+
] });
|
|
487
1204
|
}
|
|
488
1205
|
|
|
489
|
-
// src/
|
|
490
|
-
import
|
|
491
|
-
import
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
1206
|
+
// src/ui/PermissionPrompt.tsx
|
|
1207
|
+
import { useState as useState3 } from "react";
|
|
1208
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
1209
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1210
|
+
function describeAction(request) {
|
|
1211
|
+
const input = request.toolInput;
|
|
1212
|
+
if (input.file_path) return String(input.file_path);
|
|
1213
|
+
if (input.path) return String(input.path);
|
|
1214
|
+
if (input.command) {
|
|
1215
|
+
const cmd = String(input.command);
|
|
1216
|
+
return cmd.length > 120 ? cmd.slice(0, 117) + "..." : cmd;
|
|
498
1217
|
}
|
|
1218
|
+
if (input.pattern) return `pattern: ${String(input.pattern)}`;
|
|
1219
|
+
return "";
|
|
499
1220
|
}
|
|
500
|
-
function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1221
|
+
function PermissionPrompt({ request }) {
|
|
1222
|
+
const options = request.isDangerous ? [
|
|
1223
|
+
{ key: "y", label: "Yes, allow" },
|
|
1224
|
+
{ key: "n", label: "No, deny" }
|
|
1225
|
+
] : [
|
|
1226
|
+
{ key: "y", label: "Allow" },
|
|
1227
|
+
{ key: "n", label: "Deny" },
|
|
1228
|
+
{ key: "a", label: "Always allow this tool" },
|
|
1229
|
+
{ key: "t", label: "Trust all tools" }
|
|
1230
|
+
];
|
|
1231
|
+
const [selected, setSelected] = useState3(0);
|
|
1232
|
+
const [resolved, setResolved] = useState3(false);
|
|
1233
|
+
useInput(
|
|
1234
|
+
(input, key) => {
|
|
1235
|
+
if (resolved) return;
|
|
1236
|
+
if (key.upArrow) {
|
|
1237
|
+
setSelected((s) => Math.max(0, s - 1));
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (key.downArrow) {
|
|
1241
|
+
setSelected((s) => Math.min(options.length - 1, s + 1));
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (key.return) {
|
|
1245
|
+
setResolved(true);
|
|
1246
|
+
const opt = options[selected];
|
|
1247
|
+
if (opt.key === "n") request.resolve(false);
|
|
1248
|
+
else if (opt.key === "t") request.resolve(true, "trust");
|
|
1249
|
+
else if (opt.key === "a") request.resolve(true, "always");
|
|
1250
|
+
else request.resolve(true);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (input === "y" || input === "Y") {
|
|
1254
|
+
setResolved(true);
|
|
1255
|
+
request.resolve(true);
|
|
1256
|
+
} else if (input === "n" || input === "N") {
|
|
1257
|
+
setResolved(true);
|
|
1258
|
+
request.resolve(false);
|
|
1259
|
+
} else if (!request.isDangerous && (input === "a" || input === "A")) {
|
|
1260
|
+
setResolved(true);
|
|
1261
|
+
request.resolve(true, "always");
|
|
1262
|
+
} else if (!request.isDangerous && (input === "t" || input === "T")) {
|
|
1263
|
+
setResolved(true);
|
|
1264
|
+
request.resolve(true, "trust");
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
{ isActive: !resolved }
|
|
1268
|
+
);
|
|
1269
|
+
const detail = describeAction(request);
|
|
1270
|
+
const borderColor = request.isDangerous ? theme.error : theme.permission;
|
|
1271
|
+
return /* @__PURE__ */ jsxs3(
|
|
1272
|
+
Box3,
|
|
1273
|
+
{
|
|
1274
|
+
flexDirection: "column",
|
|
1275
|
+
borderStyle: "round",
|
|
1276
|
+
borderColor,
|
|
1277
|
+
paddingX: 1,
|
|
1278
|
+
marginY: 1,
|
|
1279
|
+
marginLeft: 2,
|
|
1280
|
+
children: [
|
|
1281
|
+
request.isDangerous ? /* @__PURE__ */ jsxs3(Text3, { color: theme.error, bold: true, children: [
|
|
1282
|
+
"\u26A0 ",
|
|
1283
|
+
"Dangerous: ",
|
|
1284
|
+
request.dangerLabel || request.toolName
|
|
1285
|
+
] }) : /* @__PURE__ */ jsxs3(Text3, { color: theme.permission, bold: true, children: [
|
|
1286
|
+
"? ",
|
|
1287
|
+
"Allow ",
|
|
1288
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.text, bold: true, children: request.toolName })
|
|
1289
|
+
] }),
|
|
1290
|
+
detail ? /* @__PURE__ */ jsxs3(Text3, { color: theme.subtle, children: [
|
|
1291
|
+
" ",
|
|
1292
|
+
detail
|
|
1293
|
+
] }) : null,
|
|
1294
|
+
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
1295
|
+
options.map((opt, i) => {
|
|
1296
|
+
const isSelected = i === selected;
|
|
1297
|
+
const radio = isSelected ? "\u25C9" : "\u25CB";
|
|
1298
|
+
return /* @__PURE__ */ jsx3(Box3, { marginLeft: 1, children: /* @__PURE__ */ jsxs3(
|
|
1299
|
+
Text3,
|
|
1300
|
+
{
|
|
1301
|
+
color: isSelected ? theme.permission : theme.subtle,
|
|
1302
|
+
bold: isSelected,
|
|
1303
|
+
children: [
|
|
1304
|
+
radio,
|
|
1305
|
+
" (",
|
|
1306
|
+
opt.key,
|
|
1307
|
+
") ",
|
|
1308
|
+
opt.label
|
|
1309
|
+
]
|
|
1310
|
+
}
|
|
1311
|
+
) }, opt.key);
|
|
1312
|
+
})
|
|
1313
|
+
]
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
506
1316
|
}
|
|
507
|
-
|
|
508
|
-
|
|
1317
|
+
|
|
1318
|
+
// src/ui/StatusBar.tsx
|
|
1319
|
+
import { Box as Box4, Text as Text4, useStdout } from "ink";
|
|
1320
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1321
|
+
function formatTokens(n) {
|
|
1322
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
1323
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
1324
|
+
return String(n);
|
|
509
1325
|
}
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
1326
|
+
function formatCost(c) {
|
|
1327
|
+
if (c < 0.01) return "$0.00";
|
|
1328
|
+
return `$${c.toFixed(2)}`;
|
|
513
1329
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
1330
|
+
function StatusBar(props) {
|
|
1331
|
+
const { stdout } = useStdout();
|
|
1332
|
+
const width = stdout?.columns || 80;
|
|
1333
|
+
const barLen = 8;
|
|
1334
|
+
const usage = Math.min(1, props.maxContext > 0 ? props.tokens / props.maxContext : 0);
|
|
1335
|
+
const filled = Math.round(usage * barLen);
|
|
1336
|
+
const empty = barLen - filled;
|
|
1337
|
+
const barColor = usage < 0.5 ? theme.success : usage < 0.8 ? theme.warning : theme.error;
|
|
1338
|
+
let modeColor;
|
|
1339
|
+
switch (props.mode) {
|
|
1340
|
+
case "PLAN":
|
|
1341
|
+
modeColor = theme.bashBorder;
|
|
1342
|
+
break;
|
|
1343
|
+
case "trust all":
|
|
1344
|
+
modeColor = theme.error;
|
|
1345
|
+
break;
|
|
1346
|
+
default:
|
|
1347
|
+
modeColor = theme.success;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
const bgColor = theme.subtleDark;
|
|
1351
|
+
const modelStr = ` ${props.provider}/${props.model} `;
|
|
1352
|
+
const tokenStr = ` ${formatTokens(props.tokens)} `;
|
|
1353
|
+
const costStr = formatCost(props.cost);
|
|
1354
|
+
const branchStr = props.gitBranch ? ` git:(${props.gitBranch})` : "";
|
|
1355
|
+
const cwdStr = props.cwd ? ` ${props.cwd}` : "";
|
|
1356
|
+
const rightStr = `${cwdStr}${branchStr} | ${costStr} `;
|
|
1357
|
+
const fixedLen = modelStr.length + barLen + tokenStr.length + rightStr.length + props.mode.length + 2;
|
|
1358
|
+
const padding = Math.max(0, width - fixedLen);
|
|
1359
|
+
return /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
|
|
1360
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: theme.text, children: modelStr }),
|
|
1361
|
+
/* @__PURE__ */ jsxs4(Text4, { backgroundColor: bgColor, color: barColor, children: [
|
|
1362
|
+
"\u2588".repeat(filled),
|
|
1363
|
+
"\u2591".repeat(empty)
|
|
1364
|
+
] }),
|
|
1365
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: theme.text, children: tokenStr }),
|
|
1366
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, children: " ".repeat(padding) }),
|
|
1367
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: theme.text, children: cwdStr }),
|
|
1368
|
+
props.gitBranch ? /* @__PURE__ */ jsxs4(Text4, { backgroundColor: bgColor, children: [
|
|
1369
|
+
/* @__PURE__ */ jsx4(Text4, { color: theme.text, children: " git:(" }),
|
|
1370
|
+
/* @__PURE__ */ jsx4(Text4, { color: theme.success, children: props.gitBranch }),
|
|
1371
|
+
/* @__PURE__ */ jsx4(Text4, { color: theme.text, children: ")" })
|
|
1372
|
+
] }) : null,
|
|
1373
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: theme.subtle, children: " | " }),
|
|
1374
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: theme.text, children: costStr }),
|
|
1375
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: theme.text, children: " " }),
|
|
1376
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, color: modeColor, bold: true, children: props.mode }),
|
|
1377
|
+
/* @__PURE__ */ jsx4(Text4, { backgroundColor: bgColor, children: " " })
|
|
1378
|
+
] });
|
|
516
1379
|
}
|
|
517
|
-
|
|
518
|
-
|
|
1380
|
+
|
|
1381
|
+
// src/ui/Input.tsx
|
|
1382
|
+
import { useState as useState4 } from "react";
|
|
1383
|
+
import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
|
|
1384
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1385
|
+
function Input({ onSubmit, isActive, history }) {
|
|
1386
|
+
const [value, setValue] = useState4("");
|
|
1387
|
+
const [historyIndex, setHistoryIndex] = useState4(-1);
|
|
1388
|
+
useInput2(
|
|
1389
|
+
(input, key) => {
|
|
1390
|
+
if (!isActive) return;
|
|
1391
|
+
if (key.return) {
|
|
1392
|
+
const trimmed = value.trim();
|
|
1393
|
+
if (trimmed) {
|
|
1394
|
+
onSubmit(trimmed);
|
|
1395
|
+
setValue("");
|
|
1396
|
+
setHistoryIndex(-1);
|
|
1397
|
+
}
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (key.upArrow) {
|
|
1401
|
+
const newIdx = Math.min(historyIndex + 1, history.length - 1);
|
|
1402
|
+
if (newIdx >= 0 && history[newIdx]) {
|
|
1403
|
+
setHistoryIndex(newIdx);
|
|
1404
|
+
setValue(history[newIdx]);
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (key.downArrow) {
|
|
1409
|
+
const newIdx = historyIndex - 1;
|
|
1410
|
+
setHistoryIndex(newIdx);
|
|
1411
|
+
if (newIdx >= 0 && history[newIdx]) {
|
|
1412
|
+
setValue(history[newIdx]);
|
|
1413
|
+
} else {
|
|
1414
|
+
setValue("");
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (key.backspace || key.delete) {
|
|
1419
|
+
setValue((v) => v.slice(0, -1));
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (key.ctrl && input === "u") {
|
|
1423
|
+
setValue("");
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (key.ctrl && input === "w") {
|
|
1427
|
+
setValue((v) => v.replace(/\S+\s*$/, ""));
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1431
|
+
setValue((v) => v + input);
|
|
1432
|
+
}
|
|
1433
|
+
},
|
|
1434
|
+
{ isActive }
|
|
1435
|
+
);
|
|
1436
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1437
|
+
/* @__PURE__ */ jsx5(Text5, { color: isActive ? theme.brand : theme.inactive, bold: true, children: isActive ? "\u25C6 " : "\u25C7 " }),
|
|
1438
|
+
/* @__PURE__ */ jsx5(Text5, { color: theme.text, children: value }),
|
|
1439
|
+
isActive && /* @__PURE__ */ jsx5(Text5, { inverse: true, children: " " })
|
|
1440
|
+
] });
|
|
519
1441
|
}
|
|
520
|
-
|
|
521
|
-
|
|
1442
|
+
|
|
1443
|
+
// src/ui/App.tsx
|
|
1444
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1445
|
+
function OrchestratorConfirm({ request }) {
|
|
1446
|
+
const [answered, setAnswered] = React3.useState(false);
|
|
1447
|
+
useInput3((input) => {
|
|
1448
|
+
if (answered) return;
|
|
1449
|
+
if (input === "y" || input === "Y") {
|
|
1450
|
+
setAnswered(true);
|
|
1451
|
+
request.resolve(true);
|
|
1452
|
+
}
|
|
1453
|
+
if (input === "n" || input === "N") {
|
|
1454
|
+
setAnswered(true);
|
|
1455
|
+
request.resolve(false);
|
|
1456
|
+
}
|
|
1457
|
+
}, { isActive: !answered });
|
|
1458
|
+
return /* @__PURE__ */ jsxs6(Box6, { marginLeft: 2, marginY: 1, children: [
|
|
1459
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.permission, children: [
|
|
1460
|
+
request.prompt,
|
|
1461
|
+
" "
|
|
1462
|
+
] }),
|
|
1463
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "(y/n)" })
|
|
1464
|
+
] });
|
|
522
1465
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
1466
|
+
function App(props) {
|
|
1467
|
+
const { exit } = useApp();
|
|
1468
|
+
const { stdout } = useStdout2();
|
|
1469
|
+
const lastCtrlCRef = useRef2(0);
|
|
1470
|
+
const width = stdout?.columns || 80;
|
|
1471
|
+
useInput3((input, key) => {
|
|
1472
|
+
if (key.ctrl && input === "c") {
|
|
1473
|
+
const now = Date.now();
|
|
1474
|
+
if (props.status === "idle" && now - lastCtrlCRef.current < 500) {
|
|
1475
|
+
exit();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
lastCtrlCRef.current = now;
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
const mode = props.planMode ? "PLAN" : props.trustAll ? "trust all" : "ask";
|
|
1482
|
+
const headerInner = Math.min(width - 4, 50);
|
|
1483
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", width: "100%", children: [
|
|
1484
|
+
/* @__PURE__ */ jsx6(Static, { items: [{ id: "__header__" }], children: () => /* @__PURE__ */ jsxs6(
|
|
1485
|
+
Box6,
|
|
1486
|
+
{
|
|
1487
|
+
flexDirection: "column",
|
|
1488
|
+
borderStyle: "round",
|
|
1489
|
+
borderColor: theme.subtleDark,
|
|
1490
|
+
paddingX: 1,
|
|
1491
|
+
width: headerInner,
|
|
1492
|
+
children: [
|
|
1493
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1494
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.brand, children: "\u25C6 " }),
|
|
1495
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.text, bold: true, children: "WorkerMill" })
|
|
1496
|
+
] }),
|
|
1497
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
1498
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.subtle, children: [
|
|
1499
|
+
" ",
|
|
1500
|
+
props.provider,
|
|
1501
|
+
"/",
|
|
1502
|
+
props.model
|
|
1503
|
+
] }),
|
|
1504
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.subtle, children: [
|
|
1505
|
+
" ",
|
|
1506
|
+
"cwd: ",
|
|
1507
|
+
props.workingDir
|
|
1508
|
+
] }),
|
|
1509
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.subtle, children: [
|
|
1510
|
+
" ",
|
|
1511
|
+
"Type ",
|
|
1512
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.text, children: "/help" }),
|
|
1513
|
+
" for commands"
|
|
1514
|
+
] })
|
|
1515
|
+
]
|
|
1516
|
+
},
|
|
1517
|
+
"__header__"
|
|
1518
|
+
) }),
|
|
1519
|
+
/* @__PURE__ */ jsx6(Static, { items: props.messages, children: (message) => /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginTop: 1, children: message.role === "user" ? /* @__PURE__ */ jsxs6(Box6, { marginLeft: 1, children: [
|
|
1520
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.brand, bold: true, children: "\u2771 " }),
|
|
1521
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.text, children: message.content })
|
|
1522
|
+
] }) : /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginLeft: 2, children: [
|
|
1523
|
+
message.toolCalls?.map((tc) => /* @__PURE__ */ jsx6(ToolCallDisplay, { tool: tc }, tc.id)),
|
|
1524
|
+
message.content ? /* @__PURE__ */ jsx6(Markdown, { content: message.content }) : null
|
|
1525
|
+
] }) }, message.id) }),
|
|
1526
|
+
/* @__PURE__ */ jsx6(Box6, { marginLeft: 2, height: 1, children: props.orchestratorStatus ? /* @__PURE__ */ jsxs6(Text6, { color: theme.warning, children: [
|
|
1527
|
+
"\u25CF ",
|
|
1528
|
+
props.orchestratorStatus
|
|
1529
|
+
] }) : props.status === "thinking" ? /* @__PURE__ */ jsx6(Text6, { color: theme.subtle, children: "\u25CF Thinking..." }) : props.status === "streaming" ? /* @__PURE__ */ jsx6(Text6, { color: theme.brand, children: "\u25CF Streaming response..." }) : props.status === "tool_running" ? /* @__PURE__ */ jsx6(Text6, { color: theme.warning, children: "\u25CF Running tool..." }) : props.status === "permission" ? /* @__PURE__ */ jsx6(Text6, { color: theme.permission, children: "\u25CF Waiting for permission..." }) : /* @__PURE__ */ jsx6(Text6, { children: " " }) }),
|
|
1530
|
+
props.permissionRequest ? /* @__PURE__ */ jsx6(PermissionPrompt, { request: props.permissionRequest }) : props.orchestratorConfirm ? /* @__PURE__ */ jsx6(OrchestratorConfirm, { request: props.orchestratorConfirm }) : /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1531
|
+
/* @__PURE__ */ jsx6(
|
|
1532
|
+
StatusBar,
|
|
1533
|
+
{
|
|
1534
|
+
model: props.model,
|
|
1535
|
+
provider: props.provider,
|
|
1536
|
+
tokens: props.tokens,
|
|
1537
|
+
maxContext: props.maxContext,
|
|
1538
|
+
cost: props.cost,
|
|
1539
|
+
mode,
|
|
1540
|
+
gitBranch: props.gitBranch,
|
|
1541
|
+
cwd: props.workingDir.split("/").pop() || ""
|
|
1542
|
+
}
|
|
1543
|
+
),
|
|
1544
|
+
/* @__PURE__ */ jsx6(
|
|
1545
|
+
Input,
|
|
1546
|
+
{
|
|
1547
|
+
onSubmit: props.onSubmit,
|
|
1548
|
+
isActive: props.status === "idle" && !props.orchestratorStatus,
|
|
1549
|
+
history: props.inputHistory
|
|
1550
|
+
}
|
|
1551
|
+
)
|
|
1552
|
+
] })
|
|
1553
|
+
] });
|
|
528
1554
|
}
|
|
529
1555
|
|
|
530
|
-
// src/
|
|
531
|
-
|
|
1556
|
+
// src/ui/Root.tsx
|
|
1557
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1558
|
+
var HISTORY_DIR = path2.join(os.homedir(), ".workermill");
|
|
1559
|
+
var HISTORY_FILE = path2.join(HISTORY_DIR, "history");
|
|
532
1560
|
var MAX_HISTORY = 1e3;
|
|
533
1561
|
function loadHistory() {
|
|
534
1562
|
try {
|
|
535
|
-
if (
|
|
536
|
-
const
|
|
537
|
-
|
|
1563
|
+
if (fs2.existsSync(HISTORY_FILE)) {
|
|
1564
|
+
const raw = fs2.readFileSync(HISTORY_FILE, "utf-8").trim();
|
|
1565
|
+
if (!raw) return [];
|
|
1566
|
+
return raw.split("\n").slice(-MAX_HISTORY);
|
|
538
1567
|
}
|
|
539
1568
|
} catch {
|
|
540
1569
|
}
|
|
@@ -542,408 +1571,389 @@ function loadHistory() {
|
|
|
542
1571
|
}
|
|
543
1572
|
function appendHistory(line) {
|
|
544
1573
|
try {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1574
|
+
if (!fs2.existsSync(HISTORY_DIR)) {
|
|
1575
|
+
fs2.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
1576
|
+
}
|
|
1577
|
+
fs2.appendFileSync(HISTORY_FILE, line + "\n", "utf-8");
|
|
549
1578
|
} catch {
|
|
550
1579
|
}
|
|
551
1580
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
"/git",
|
|
561
|
-
"/editor",
|
|
562
|
-
"/plan",
|
|
563
|
-
"/sessions",
|
|
564
|
-
"/exit"
|
|
565
|
-
];
|
|
566
|
-
function completer(line) {
|
|
567
|
-
if (line.startsWith("/")) {
|
|
568
|
-
const hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
569
|
-
return [hits.length ? hits : SLASH_COMMANDS, line];
|
|
1581
|
+
function getGitBranch() {
|
|
1582
|
+
try {
|
|
1583
|
+
return execSync2("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
|
|
1584
|
+
encoding: "utf-8",
|
|
1585
|
+
timeout: 2e3
|
|
1586
|
+
}).trim();
|
|
1587
|
+
} catch {
|
|
1588
|
+
return "";
|
|
570
1589
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
process.env[envVar] = apiKey;
|
|
584
|
-
}
|
|
1590
|
+
}
|
|
1591
|
+
function getGitStatus(cwd) {
|
|
1592
|
+
let branch = "(unknown)";
|
|
1593
|
+
let status = "(unable to read)";
|
|
1594
|
+
try {
|
|
1595
|
+
branch = execSync2("git branch --show-current 2>/dev/null", {
|
|
1596
|
+
cwd,
|
|
1597
|
+
encoding: "utf-8",
|
|
1598
|
+
timeout: 5e3
|
|
1599
|
+
}).trim() || "(detached HEAD)";
|
|
1600
|
+
} catch {
|
|
1601
|
+
branch = "(not a git repo)";
|
|
585
1602
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (loaded) {
|
|
596
|
-
session = loaded;
|
|
597
|
-
console.log(chalk3.green(` Resumed session ${session.id.slice(0, 8)}... (${session.messages.length} messages)
|
|
598
|
-
`));
|
|
599
|
-
} else {
|
|
600
|
-
session = createSession(provider, modelName);
|
|
601
|
-
console.log(chalk3.dim(" No previous session found, starting fresh.\n"));
|
|
602
|
-
}
|
|
603
|
-
} else {
|
|
604
|
-
session = createSession(provider, modelName);
|
|
1603
|
+
try {
|
|
1604
|
+
const raw = execSync2("git status --short 2>/dev/null", {
|
|
1605
|
+
cwd,
|
|
1606
|
+
encoding: "utf-8",
|
|
1607
|
+
timeout: 5e3
|
|
1608
|
+
}).trim();
|
|
1609
|
+
status = raw || "(clean)";
|
|
1610
|
+
} catch {
|
|
1611
|
+
status = "(not a git repo)";
|
|
605
1612
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1613
|
+
return `**Git branch:** ${branch}
|
|
1614
|
+
|
|
1615
|
+
\`\`\`
|
|
1616
|
+
${status}
|
|
1617
|
+
\`\`\``;
|
|
1618
|
+
}
|
|
1619
|
+
var HELP_TEXT = `**WorkerMill Commands**
|
|
1620
|
+
|
|
1621
|
+
| Command | Description |
|
|
1622
|
+
|---|---|
|
|
1623
|
+
| \`/help\` | Show this help |
|
|
1624
|
+
| \`/model\` | Show current model info |
|
|
1625
|
+
| \`/cost\` | Show session cost breakdown |
|
|
1626
|
+
| \`/status\` | Show session status |
|
|
1627
|
+
| \`/plan\` | Toggle plan mode (read-only tools) |
|
|
1628
|
+
| \`/trust\` | Trust all tool calls for this session |
|
|
1629
|
+
| \`/build <task>\` | Multi-expert orchestration |
|
|
1630
|
+
| \`/git\` | Show git branch and status |
|
|
1631
|
+
| \`/sessions\` | List recent sessions |
|
|
1632
|
+
| \`/editor\` | Open \\$EDITOR, submit contents |
|
|
1633
|
+
| \`/compact\` | Trigger context compaction |
|
|
1634
|
+
| \`/clear\` | Clear screen (limited in Ink) |
|
|
1635
|
+
| \`/quit\` | Exit WorkerMill |
|
|
1636
|
+
| \`/exit\` | Exit WorkerMill |
|
|
1637
|
+
|
|
1638
|
+
**Shortcuts**
|
|
1639
|
+
|
|
1640
|
+
- \`!command\` -- Run a shell command directly and display output
|
|
1641
|
+
- \`Ctrl+C\` -- Cancel current operation
|
|
1642
|
+
- \`Ctrl+C Ctrl+C\` -- Exit when idle
|
|
1643
|
+
|
|
1644
|
+
**Notes**
|
|
1645
|
+
|
|
1646
|
+
- Multiline input is not currently supported. Paste single-line prompts or use \`/editor\` to compose longer messages.`;
|
|
1647
|
+
function Root(props) {
|
|
1648
|
+
const { exit } = useApp2();
|
|
1649
|
+
const agent = useAgent(props);
|
|
1650
|
+
const addOrchestratorMessage = useCallback3(
|
|
1651
|
+
(content, role) => {
|
|
1652
|
+
if (role === "user") {
|
|
1653
|
+
agent.addUserMessage(content);
|
|
1654
|
+
} else {
|
|
1655
|
+
agent.addSystemMessage(content);
|
|
619
1656
|
}
|
|
1657
|
+
},
|
|
1658
|
+
[agent]
|
|
1659
|
+
);
|
|
1660
|
+
const orchestrator = useOrchestrator(addOrchestratorMessage);
|
|
1661
|
+
const [inputHistory, setInputHistory] = useState5(() => loadHistory());
|
|
1662
|
+
const [gitBranch, setGitBranch] = useState5(() => getGitBranch());
|
|
1663
|
+
const lastBranchCheck = useRef3(Date.now());
|
|
1664
|
+
const refreshGitBranch = useCallback3(() => {
|
|
1665
|
+
const now = Date.now();
|
|
1666
|
+
if (now - lastBranchCheck.current > 1e4) {
|
|
1667
|
+
lastBranchCheck.current = now;
|
|
1668
|
+
setGitBranch(getGitBranch());
|
|
620
1669
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1670
|
+
}, []);
|
|
1671
|
+
const pushHistory = useCallback3((line) => {
|
|
1672
|
+
setInputHistory((prev) => {
|
|
1673
|
+
const next = [...prev, line].slice(-MAX_HISTORY);
|
|
1674
|
+
return next;
|
|
1675
|
+
});
|
|
1676
|
+
appendHistory(line);
|
|
1677
|
+
}, []);
|
|
1678
|
+
const handleSlashCommand = useCallback3(
|
|
1679
|
+
(input) => {
|
|
1680
|
+
const trimmed = input.trim();
|
|
1681
|
+
if (!trimmed.startsWith("/")) return false;
|
|
1682
|
+
const spaceIdx = trimmed.indexOf(" ", 1);
|
|
1683
|
+
const cmd = spaceIdx === -1 ? trimmed.slice(1).toLowerCase() : trimmed.slice(1, spaceIdx).toLowerCase();
|
|
1684
|
+
const arg = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1).trim();
|
|
1685
|
+
switch (cmd) {
|
|
1686
|
+
// ---- /help ----
|
|
1687
|
+
case "help":
|
|
1688
|
+
case "h":
|
|
1689
|
+
case "?": {
|
|
1690
|
+
agent.addSystemMessage(HELP_TEXT);
|
|
1691
|
+
break;
|
|
636
1692
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
agentState = "streaming";
|
|
642
|
-
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
643
|
-
debug("Tool result", { tool: name, result: resultStr.slice(0, 200) });
|
|
644
|
-
printToolResult(name, resultStr);
|
|
645
|
-
return result;
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
info("Session started", { provider, model: modelName, workingDir, trustAll });
|
|
650
|
-
initTerminal();
|
|
651
|
-
printHeader("0.1.0", provider, modelName, workingDir);
|
|
652
|
-
const statusText = printStatusBar(provider, modelName, 0, planMode ? "PLAN" : trustAll ? "trust all" : "ask", 0);
|
|
653
|
-
setStatusBar(statusText);
|
|
654
|
-
const rl = readline2.createInterface({
|
|
655
|
-
input: process.stdin,
|
|
656
|
-
output: process.stdout,
|
|
657
|
-
prompt: chalk3.cyan("\u276F "),
|
|
658
|
-
completer
|
|
659
|
-
});
|
|
660
|
-
function promptWithStatus() {
|
|
661
|
-
showStatusBar();
|
|
662
|
-
rl.prompt();
|
|
663
|
-
}
|
|
664
|
-
const history = loadHistory();
|
|
665
|
-
rl.history = history.reverse();
|
|
666
|
-
permissions.setReadline(rl);
|
|
667
|
-
process.on("SIGINT", () => {
|
|
668
|
-
if (agentState === "streaming") {
|
|
669
|
-
if (currentAbortController)
|
|
670
|
-
currentAbortController.abort();
|
|
671
|
-
console.log(chalk3.yellow("\n [cancelled]"));
|
|
672
|
-
agentState = "idle";
|
|
673
|
-
currentAbortController = null;
|
|
674
|
-
processing = false;
|
|
675
|
-
rl.resume();
|
|
676
|
-
promptWithStatus();
|
|
677
|
-
} else if (agentState === "tool_executing") {
|
|
678
|
-
console.log(chalk3.yellow("\n [cancelling...]"));
|
|
679
|
-
killActiveProcess();
|
|
680
|
-
if (currentAbortController)
|
|
681
|
-
currentAbortController.abort();
|
|
682
|
-
agentState = "idle";
|
|
683
|
-
currentAbortController = null;
|
|
684
|
-
processing = false;
|
|
685
|
-
rl.resume();
|
|
686
|
-
promptWithStatus();
|
|
687
|
-
} else if (agentState === "permission_waiting") {
|
|
688
|
-
permissions.cancelPrompt();
|
|
689
|
-
console.log(chalk3.yellow("\n [cancelled]"));
|
|
690
|
-
agentState = "idle";
|
|
691
|
-
currentAbortController = null;
|
|
692
|
-
processing = false;
|
|
693
|
-
rl.resume();
|
|
694
|
-
promptWithStatus();
|
|
695
|
-
} else {
|
|
696
|
-
const now = Date.now();
|
|
697
|
-
if (now - lastCtrlCTime < 500) {
|
|
698
|
-
saveSession(session);
|
|
699
|
-
exitTerminal();
|
|
700
|
-
console.log(chalk3.dim(" Goodbye!"));
|
|
701
|
-
process.exit(0);
|
|
702
|
-
}
|
|
703
|
-
lastCtrlCTime = now;
|
|
704
|
-
console.log(chalk3.dim("\n Press Ctrl+C again to exit."));
|
|
705
|
-
promptWithStatus();
|
|
706
|
-
}
|
|
707
|
-
});
|
|
708
|
-
const systemPrompt = `You are WorkerMill, an AI coding agent running in the user's terminal.
|
|
709
|
-
You have access to tools for reading, writing, and editing files, running bash commands, searching code, and fetching web content.
|
|
1693
|
+
// ---- /model ----
|
|
1694
|
+
case "model": {
|
|
1695
|
+
agent.addSystemMessage(
|
|
1696
|
+
`**Current model:** ${props.provider}/${props.model}
|
|
710
1697
|
|
|
711
|
-
|
|
1698
|
+
**Supported model families:**
|
|
1699
|
+
- Anthropic: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5
|
|
1700
|
+
- OpenAI: gpt-5.4, gpt-5.4-mini
|
|
1701
|
+
- Google: gemini-3.1-pro, gemini-3.1-flash-lite
|
|
1702
|
+
- Ollama: any locally-hosted model
|
|
712
1703
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
- When editing files, read them first to understand context
|
|
717
|
-
- Prefer editing existing files over creating new ones
|
|
718
|
-
- Run tests after making changes when test infrastructure exists
|
|
719
|
-
- Use glob and grep to find relevant files before reading them
|
|
720
|
-
|
|
721
|
-
Focus on writing clean, production-ready code.`;
|
|
722
|
-
promptWithStatus();
|
|
723
|
-
let processing = false;
|
|
724
|
-
let multilineBacktick = false;
|
|
725
|
-
let multilineBackslash = false;
|
|
726
|
-
let multilineBuffer = [];
|
|
727
|
-
const cmdCtx = {
|
|
728
|
-
config,
|
|
729
|
-
session,
|
|
730
|
-
costTracker,
|
|
731
|
-
workingDir,
|
|
732
|
-
planMode,
|
|
733
|
-
setPlanMode: (mode) => {
|
|
734
|
-
planMode = mode;
|
|
735
|
-
cmdCtx.planMode = mode;
|
|
736
|
-
},
|
|
737
|
-
processInput
|
|
738
|
-
};
|
|
739
|
-
function processInput(input) {
|
|
740
|
-
processing = true;
|
|
741
|
-
rl.pause();
|
|
742
|
-
(async () => {
|
|
743
|
-
try {
|
|
744
|
-
addMessage(session, "user", input);
|
|
745
|
-
appendHistory(input);
|
|
746
|
-
if (!session.name) {
|
|
747
|
-
session.name = input.slice(0, 50).replace(/\n/g, " ");
|
|
1704
|
+
To change: edit \`~/.workermill/cli.json\` or restart with \`--provider\` / \`--model\` flags.`
|
|
1705
|
+
);
|
|
1706
|
+
break;
|
|
748
1707
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1708
|
+
// ---- /cost ----
|
|
1709
|
+
case "cost": {
|
|
1710
|
+
const costUsd = agent.cost;
|
|
1711
|
+
const totalTokens = agent.tokens;
|
|
1712
|
+
const sessionMessages = agent.session.messages.length;
|
|
1713
|
+
agent.addSystemMessage(
|
|
1714
|
+
`**Session Cost Summary**
|
|
1715
|
+
|
|
1716
|
+
| Metric | Value |
|
|
1717
|
+
|---|---|
|
|
1718
|
+
| Model | ${props.provider}/${props.model} |
|
|
1719
|
+
| Total cost | $${costUsd.toFixed(4)} |
|
|
1720
|
+
| Last input tokens | ${totalTokens.toLocaleString()} |
|
|
1721
|
+
| Session tokens | ${agent.session.totalTokens.toLocaleString()} |
|
|
1722
|
+
| Messages | ${sessionMessages} |`
|
|
1723
|
+
);
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
// ---- /status ----
|
|
1727
|
+
case "status": {
|
|
1728
|
+
const session = agent.session;
|
|
1729
|
+
const msgCount = session.messages.length;
|
|
1730
|
+
const mode = props.planMode ? "PLAN (read-only)" : props.trustAll ? "TRUST ALL" : "ask";
|
|
1731
|
+
agent.addSystemMessage(
|
|
1732
|
+
`**Session Status**
|
|
1733
|
+
|
|
1734
|
+
| Field | Value |
|
|
1735
|
+
|---|---|
|
|
1736
|
+
| Session ID | \`${session.id.slice(0, 8)}...\` |
|
|
1737
|
+
| Provider / Model | ${props.provider}/${props.model} |
|
|
1738
|
+
| Messages | ${msgCount} |
|
|
1739
|
+
| Session tokens | ${session.totalTokens.toLocaleString()} |
|
|
1740
|
+
| Cost | $${agent.cost.toFixed(4)} |
|
|
1741
|
+
| Mode | ${mode} |
|
|
1742
|
+
| Working dir | \`${props.workingDir}\` |
|
|
1743
|
+
| Started | ${session.startedAt} |`
|
|
1744
|
+
);
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
// ---- /plan ----
|
|
1748
|
+
case "plan": {
|
|
1749
|
+
const newPlan = !props.planMode;
|
|
1750
|
+
agent.setPlanMode(newPlan);
|
|
1751
|
+
agent.addSystemMessage(
|
|
1752
|
+
newPlan ? "**Plan mode ON.** Only read-only tools (read_file, glob, grep, ls, sub_agent) are available. Write operations are blocked." : "**Plan mode OFF.** All tools are now available."
|
|
1753
|
+
);
|
|
1754
|
+
break;
|
|
1755
|
+
}
|
|
1756
|
+
// ---- /trust ----
|
|
1757
|
+
case "trust": {
|
|
1758
|
+
agent.setTrustAll(true);
|
|
1759
|
+
agent.addSystemMessage(
|
|
1760
|
+
"**Trust mode ON.** All non-dangerous tool calls will be auto-approved for this session. Dangerous operations (force push, rm -rf, etc.) still require confirmation."
|
|
1761
|
+
);
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
// ---- /build <task> ----
|
|
1765
|
+
case "build": {
|
|
1766
|
+
if (!arg) {
|
|
1767
|
+
agent.addSystemMessage(
|
|
1768
|
+
"**Usage:** `/build <task description>`\n\nRuns WorkerMill multi-expert orchestration: classifies complexity, plans stories, executes per-persona with tool calls, reviews, and revision loops."
|
|
1769
|
+
);
|
|
1770
|
+
} else if (orchestrator.running) {
|
|
1771
|
+
agent.addSystemMessage("Orchestration is already running. Wait for it to complete.");
|
|
764
1772
|
} else {
|
|
765
|
-
|
|
766
|
-
|
|
1773
|
+
agent.addUserMessage(`/build ${arg}`);
|
|
1774
|
+
orchestrator.start(arg, props.trustAll, props.sandboxed);
|
|
767
1775
|
}
|
|
1776
|
+
break;
|
|
768
1777
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
for await (const chunk of stream.textStream) {
|
|
788
|
-
if (!spinnerStopped) {
|
|
789
|
-
thinkingSpinner.stop();
|
|
790
|
-
spinnerStopped = true;
|
|
791
|
-
process.stdout.write("\n");
|
|
1778
|
+
// ---- /clear ----
|
|
1779
|
+
case "clear": {
|
|
1780
|
+
agent.addSystemMessage(
|
|
1781
|
+
"Screen clearing is not fully supported in the Ink terminal framework. Previous messages rendered via `Static` cannot be removed. The conversation continues below."
|
|
1782
|
+
);
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
// ---- /compact ----
|
|
1786
|
+
case "compact": {
|
|
1787
|
+
const inputTokens = agent.tokens;
|
|
1788
|
+
if (inputTokens > 0) {
|
|
1789
|
+
agent.addSystemMessage(
|
|
1790
|
+
`Compaction is triggered automatically when context usage exceeds 80%. Current last-observed input tokens: ${inputTokens.toLocaleString()}. To force compaction, send a message and the agent will evaluate context pressure.`
|
|
1791
|
+
);
|
|
1792
|
+
} else {
|
|
1793
|
+
agent.addSystemMessage(
|
|
1794
|
+
"No token usage recorded yet. Compaction happens automatically when context usage exceeds 80% of the model limit."
|
|
1795
|
+
);
|
|
792
1796
|
}
|
|
793
|
-
|
|
794
|
-
fullText += chunk;
|
|
1797
|
+
break;
|
|
795
1798
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
process.stdout.write("\n");
|
|
1799
|
+
// ---- /git ----
|
|
1800
|
+
case "git": {
|
|
1801
|
+
const gitInfo = getGitStatus(props.workingDir);
|
|
1802
|
+
agent.addSystemMessage(gitInfo);
|
|
1803
|
+
break;
|
|
802
1804
|
}
|
|
803
|
-
|
|
804
|
-
|
|
1805
|
+
// ---- /editor ----
|
|
1806
|
+
case "editor": {
|
|
1807
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
1808
|
+
const tmpFile = path2.join(os.tmpdir(), `workermill-${Date.now()}.md`);
|
|
1809
|
+
try {
|
|
1810
|
+
fs2.writeFileSync(tmpFile, "", "utf-8");
|
|
1811
|
+
execSync2(`${editor} ${tmpFile}`, {
|
|
1812
|
+
cwd: props.workingDir,
|
|
1813
|
+
stdio: "inherit",
|
|
1814
|
+
timeout: 5 * 60 * 1e3
|
|
1815
|
+
});
|
|
1816
|
+
const contents = fs2.readFileSync(tmpFile, "utf-8").trim();
|
|
1817
|
+
if (contents) {
|
|
1818
|
+
agent.addUserMessage(contents);
|
|
1819
|
+
agent.submit(contents);
|
|
1820
|
+
} else {
|
|
1821
|
+
agent.addSystemMessage("Editor closed with no content. Nothing submitted.");
|
|
1822
|
+
}
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1825
|
+
agent.addSystemMessage(`Failed to open editor (\`${editor}\`): ${errMsg}`);
|
|
1826
|
+
} finally {
|
|
1827
|
+
try {
|
|
1828
|
+
fs2.unlinkSync(tmpFile);
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
break;
|
|
805
1833
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1834
|
+
// ---- /sessions ----
|
|
1835
|
+
case "sessions": {
|
|
1836
|
+
const sessions = listSessions(20);
|
|
1837
|
+
if (sessions.length === 0) {
|
|
1838
|
+
agent.addSystemMessage("No saved sessions found.");
|
|
1839
|
+
} else {
|
|
1840
|
+
const rows = sessions.map((s) => {
|
|
1841
|
+
const date = new Date(s.startedAt).toLocaleString();
|
|
1842
|
+
const name = s.name || s.preview;
|
|
1843
|
+
return `| \`${s.id.slice(0, 8)}\` | ${name} | ${s.messageCount} msgs | ${s.totalTokens.toLocaleString()} tokens | ${date} |`;
|
|
1844
|
+
});
|
|
1845
|
+
agent.addSystemMessage(
|
|
1846
|
+
`**Recent Sessions** (${sessions.length})
|
|
1847
|
+
|
|
1848
|
+
| ID | Name | Messages | Tokens | Date |
|
|
1849
|
+
|---|---|---|---|---|
|
|
1850
|
+
` + rows.join("\n")
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
break;
|
|
826
1854
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
1855
|
+
// ---- /quit, /exit ----
|
|
1856
|
+
case "quit":
|
|
1857
|
+
case "exit":
|
|
1858
|
+
case "q": {
|
|
1859
|
+
exit();
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
// ---- Unknown slash command ----
|
|
1863
|
+
default: {
|
|
1864
|
+
agent.addSystemMessage(
|
|
1865
|
+
`Unknown command: \`/${cmd}\`
|
|
1866
|
+
|
|
1867
|
+
Type \`/help\` to see all available commands.`
|
|
1868
|
+
);
|
|
1869
|
+
break;
|
|
836
1870
|
}
|
|
837
1871
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
process.stdout.write(chalk3.dim(" ... "));
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
if (input.trim() === "```" && multilineBacktick) {
|
|
853
|
-
multilineBacktick = false;
|
|
854
|
-
const fullInput = multilineBuffer.join("\n");
|
|
855
|
-
multilineBuffer = [];
|
|
856
|
-
if (!fullInput.trim() || processing) {
|
|
857
|
-
rl.prompt();
|
|
858
|
-
return;
|
|
1872
|
+
return true;
|
|
1873
|
+
},
|
|
1874
|
+
[agent, props, exit]
|
|
1875
|
+
);
|
|
1876
|
+
const handleShellEscape = useCallback3(
|
|
1877
|
+
(input) => {
|
|
1878
|
+
if (!input.startsWith("!")) return false;
|
|
1879
|
+
const bashCmd = input.slice(1).trim();
|
|
1880
|
+
if (!bashCmd) {
|
|
1881
|
+
agent.addSystemMessage("Usage: `!<command>` -- run a shell command and display the output.");
|
|
1882
|
+
return true;
|
|
859
1883
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1884
|
+
let output;
|
|
1885
|
+
let exitCode = 0;
|
|
1886
|
+
try {
|
|
1887
|
+
output = execSync2(bashCmd, {
|
|
1888
|
+
cwd: props.workingDir,
|
|
1889
|
+
encoding: "utf-8",
|
|
1890
|
+
timeout: 3e4,
|
|
1891
|
+
maxBuffer: 1024 * 1024
|
|
1892
|
+
});
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
const execErr = err;
|
|
1895
|
+
output = (execErr.stdout || "") + (execErr.stderr || "");
|
|
1896
|
+
exitCode = execErr.status ?? 1;
|
|
1897
|
+
}
|
|
1898
|
+
const trimmedOutput = output.trim();
|
|
1899
|
+
const header = exitCode !== 0 ? `\`$ ${bashCmd}\` (exit ${exitCode})` : `\`$ ${bashCmd}\``;
|
|
1900
|
+
if (trimmedOutput) {
|
|
1901
|
+
agent.addSystemMessage(`${header}
|
|
1902
|
+
|
|
1903
|
+
\`\`\`
|
|
1904
|
+
${trimmedOutput}
|
|
1905
|
+
\`\`\``);
|
|
872
1906
|
} else {
|
|
873
|
-
|
|
1907
|
+
agent.addSystemMessage(`${header}
|
|
1908
|
+
|
|
1909
|
+
(no output)`);
|
|
874
1910
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1911
|
+
return true;
|
|
1912
|
+
},
|
|
1913
|
+
[agent, props.workingDir]
|
|
1914
|
+
);
|
|
1915
|
+
const handleSubmit = useCallback3(
|
|
1916
|
+
(input) => {
|
|
1917
|
+
const trimmed = input.trim();
|
|
1918
|
+
if (!trimmed) return;
|
|
1919
|
+
pushHistory(trimmed);
|
|
1920
|
+
if (handleSlashCommand(trimmed)) {
|
|
885
1921
|
return;
|
|
886
1922
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
const trimmed = input.trim();
|
|
891
|
-
if (!trimmed || processing) {
|
|
892
|
-
if (!processing)
|
|
893
|
-
rl.prompt();
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
if (trimmed.startsWith("!")) {
|
|
897
|
-
const cmd = trimmed.slice(1).trim();
|
|
898
|
-
if (cmd) {
|
|
899
|
-
try {
|
|
900
|
-
const output = execSync3(cmd, { cwd: workingDir, encoding: "utf-8", timeout: 3e4 });
|
|
901
|
-
if (output.trim())
|
|
902
|
-
console.log(output);
|
|
903
|
-
} catch (err) {
|
|
904
|
-
console.log(chalk3.red(err.stderr || err.message));
|
|
905
|
-
}
|
|
1923
|
+
if (handleShellEscape(trimmed)) {
|
|
1924
|
+
return;
|
|
906
1925
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (processing) {
|
|
932
|
-
const checkDone = setInterval(() => {
|
|
933
|
-
if (!processing) {
|
|
934
|
-
clearInterval(checkDone);
|
|
935
|
-
cleanup();
|
|
936
|
-
}
|
|
937
|
-
}, 500);
|
|
938
|
-
} else {
|
|
939
|
-
cleanup();
|
|
1926
|
+
refreshGitBranch();
|
|
1927
|
+
agent.submit(trimmed);
|
|
1928
|
+
},
|
|
1929
|
+
[agent, pushHistory, handleSlashCommand, handleShellEscape, refreshGitBranch]
|
|
1930
|
+
);
|
|
1931
|
+
return /* @__PURE__ */ jsx7(
|
|
1932
|
+
App,
|
|
1933
|
+
{
|
|
1934
|
+
provider: props.provider,
|
|
1935
|
+
model: props.model,
|
|
1936
|
+
workingDir: props.workingDir,
|
|
1937
|
+
maxContext: props.contextLength || 128e3,
|
|
1938
|
+
trustAll: props.trustAll,
|
|
1939
|
+
planMode: props.planMode,
|
|
1940
|
+
onSubmit: handleSubmit,
|
|
1941
|
+
messages: agent.messages,
|
|
1942
|
+
status: orchestrator.running ? "tool_running" : agent.status,
|
|
1943
|
+
permissionRequest: agent.permissionRequest,
|
|
1944
|
+
orchestratorConfirm: orchestrator.confirmRequest,
|
|
1945
|
+
orchestratorStatus: orchestrator.statusMessage,
|
|
1946
|
+
tokens: agent.tokens,
|
|
1947
|
+
cost: agent.cost,
|
|
1948
|
+
gitBranch,
|
|
1949
|
+
inputHistory
|
|
940
1950
|
}
|
|
941
|
-
|
|
1951
|
+
);
|
|
942
1952
|
}
|
|
943
1953
|
|
|
944
1954
|
// src/index.ts
|
|
945
|
-
var VERSION = "0.
|
|
946
|
-
var program = new Command().name("workermill").description("AI coding agent with multi-
|
|
1955
|
+
var VERSION = "0.2.0";
|
|
1956
|
+
var program = new Command().name("workermill").description("AI coding agent with multi-provider support").version(VERSION).option("--provider <provider>", "Override default provider").option("--model <model>", "Override model").option("--trust", "Skip all tool permission prompts").option("--resume", "Resume the last conversation").option("--plan", "Start in plan mode (read-only tools)").option("--full-disk", "Allow tools to access files outside working directory").action(async (options) => {
|
|
947
1957
|
let config = loadConfig();
|
|
948
1958
|
if (!config) {
|
|
949
1959
|
config = await runSetup();
|
|
@@ -957,15 +1967,22 @@ var program = new Command().name("workermill").description("AI coding agent with
|
|
|
957
1967
|
providerConfig.model = options.model;
|
|
958
1968
|
}
|
|
959
1969
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1970
|
+
const { provider, model, apiKey, host, contextLength } = getProviderForPersona(config);
|
|
1971
|
+
const workingDir = process.cwd();
|
|
1972
|
+
const { waitUntilExit } = render(
|
|
1973
|
+
React5.createElement(Root, {
|
|
1974
|
+
provider,
|
|
1975
|
+
model,
|
|
1976
|
+
apiKey,
|
|
1977
|
+
host,
|
|
1978
|
+
contextLength,
|
|
1979
|
+
trustAll: options.trust || false,
|
|
1980
|
+
planMode: options.plan || false,
|
|
1981
|
+
sandboxed: !options.fullDisk,
|
|
1982
|
+
resume: options.resume || false,
|
|
1983
|
+
workingDir
|
|
1984
|
+
})
|
|
1985
|
+
);
|
|
1986
|
+
await waitUntilExit();
|
|
970
1987
|
});
|
|
971
1988
|
program.parse();
|