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/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
- PermissionManager,
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-3KIFXIBC.js";
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-4o", envVar: "OPENAI_API_KEY" },
36
- { name: "google", display: "Google (Gemini)", needsKey: true, defaultModel: "gemini-2.5-pro", envVar: "GOOGLE_API_KEY" }
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/agent.js
130
- import readline2 from "readline";
131
- import fs4 from "fs";
132
- import pathModule from "path";
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 { execSync as execSync2 } from "child_process";
127
+ import os from "os";
145
128
 
146
- // src/compaction.js
147
- import { generateText } from "ai";
148
- var CONTEXT_LIMITS = {
149
- // Anthropic
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
- function loadSessionById(id) {
275
- ensureSessionsDir();
276
- try {
277
- const filePath = path.join(SESSIONS_DIR, `${id}.json`);
278
- if (fs.existsSync(filePath)) {
279
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
280
- }
281
- const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.startsWith(id));
282
- if (files.length === 1) {
283
- return JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, files[0]), "utf-8"));
284
- }
285
- } catch {
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 null;
229
+ return CONTEXT_LIMITS["default"];
288
230
  }
289
- function deleteSession(id) {
290
- ensureSessionsDir();
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 filePath = path.join(SESSIONS_DIR, `${id}.json`);
293
- if (fs.existsSync(filePath)) {
294
- fs.unlinkSync(filePath);
295
- return true;
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
- return false;
300
- }
301
-
302
- // src/commands.js
303
- async function handleCommand(cmd, ctx) {
304
- const parts = cmd.slice(1).split(" ");
305
- const command = parts[0];
306
- switch (command) {
307
- case "help":
308
- console.log();
309
- console.log(chalk2.bold(" Commands:"));
310
- console.log(chalk2.dim(" /help ") + "Show this help");
311
- console.log(chalk2.dim(" /clear ") + "Clear conversation history");
312
- console.log(chalk2.dim(" /compact ") + "Manually compact conversation");
313
- console.log(chalk2.dim(" /status ") + "Show session status");
314
- console.log(chalk2.dim(" /model ") + "Show current model");
315
- console.log(chalk2.dim(" /cost ") + "Show token cost breakdown");
316
- console.log(chalk2.dim(" /git ") + "Show git status");
317
- console.log(chalk2.dim(" /sessions ") + "List/switch sessions");
318
- console.log(chalk2.dim(" /editor ") + "Open $EDITOR for input");
319
- console.log(chalk2.dim(" /plan ") + "Toggle plan mode (read-only)");
320
- console.log(chalk2.dim(" /quit ") + "Exit");
321
- console.log();
322
- console.log(chalk2.dim(" Prefix ! for bash: ") + chalk2.white("!git status"));
323
- console.log(chalk2.dim(" Multiline: ``` to start/end, or \\ at end of line"));
324
- console.log();
325
- break;
326
- case "clear":
327
- ctx.session.messages = [];
328
- saveSession(ctx.session);
329
- console.log(chalk2.green("\n Conversation cleared\n"));
330
- break;
331
- case "compact": {
332
- const { provider, model: modelName, host } = getProviderForPersona(ctx.config);
333
- const model = createModel(provider, modelName, host);
334
- const plainMessages = ctx.session.messages.map((m) => ({ role: m.role, content: m.content }));
335
- if (plainMessages.length <= 4) {
336
- console.log(chalk2.dim("\n Not enough messages to compact.\n"));
337
- break;
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
- case "status":
352
- console.log();
353
- console.log(chalk2.bold(" Session Status:"));
354
- console.log(chalk2.dim(" Messages: ") + ctx.session.messages.length);
355
- console.log(chalk2.dim(" Tokens: ") + ctx.session.totalTokens.toLocaleString());
356
- console.log(chalk2.dim(" Cost: ") + `$${ctx.costTracker.getTotalCost().toFixed(4)}`);
357
- console.log(chalk2.dim(" Mode: ") + (ctx.planMode ? chalk2.cyan("PLAN (read-only)") : "normal"));
358
- console.log();
359
- break;
360
- case "model": {
361
- const { provider, model } = getProviderForPersona(ctx.config);
362
- console.log(chalk2.dim(`
363
- ${provider}/${model}
364
- `));
365
- break;
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
- case "cost":
368
- console.log(chalk2.dim(`
369
- ${ctx.costTracker.getSummary()}
370
- `));
371
- break;
372
- case "git": {
373
- try {
374
- const status = execSync2("git status --short", { cwd: ctx.workingDir, encoding: "utf-8" });
375
- const branch = execSync2("git branch --show-current", { cwd: ctx.workingDir, encoding: "utf-8" }).trim();
376
- console.log(chalk2.dim(`
377
- Branch: `) + chalk2.white(branch));
378
- if (status.trim()) {
379
- console.log(chalk2.dim(" Changes:"));
380
- for (const line of status.trim().split("\n").slice(0, 15)) {
381
- console.log(chalk2.dim(` ${line}`));
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
- console.log();
387
- } catch {
388
- console.log(chalk2.dim("\n Not a git repository.\n"));
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
- case "sessions": {
393
- const args = parts.slice(1).join(" ").trim();
394
- if (args.startsWith("delete ")) {
395
- const deleteId = args.slice(7).trim();
396
- if (deleteSession(deleteId)) {
397
- console.log(chalk2.green(`
398
- Session ${deleteId.slice(0, 8)} deleted.
399
- `));
400
- } else {
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
- break;
406
- }
407
- if (args) {
408
- const targetId = args;
409
- let loaded = loadSessionById(targetId);
410
- if (!loaded) {
411
- const sessions2 = listSessions();
412
- const idx = parseInt(targetId, 10) - 1;
413
- if (idx >= 0 && idx < sessions2.length) {
414
- loaded = loadSessionById(sessions2[idx].id);
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
- if (loaded) {
418
- Object.assign(ctx.session, loaded);
419
- console.log(chalk2.green(`
420
- Switched to session ${loaded.id.slice(0, 8)} (${loaded.messages.length} messages)
421
- `));
422
- } else {
423
- console.log(chalk2.red(`
424
- Session not found: ${targetId}
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
- break;
428
- }
429
- const sessions = listSessions();
430
- if (sessions.length === 0) {
431
- console.log(chalk2.dim("\n No saved sessions.\n"));
432
- break;
433
- }
434
- console.log(chalk2.bold("\n Recent Sessions:"));
435
- sessions.forEach((s, i) => {
436
- const date = new Date(s.startedAt).toLocaleDateString();
437
- const current = s.id === ctx.session.id ? chalk2.green(" <- current") : "";
438
- console.log(chalk2.dim(` ${i + 1}. `) + chalk2.white(`${s.name || s.preview}`) + chalk2.dim(` (${s.messageCount} msgs, ${date})`) + current);
439
- });
440
- console.log(chalk2.dim(`
441
- Use /sessions <n> to switch, /sessions delete <id> to delete.
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
- case "editor": {
446
- const editor = process.env.EDITOR || process.env.VISUAL || "vi";
447
- const tmpFile = path2.join(os.tmpdir(), `workermill-${Date.now()}.md`);
448
- fs2.writeFileSync(tmpFile, "", "utf-8");
449
- try {
450
- execSync2(`${editor} ${tmpFile}`, { stdio: "inherit" });
451
- const content = fs2.readFileSync(tmpFile, "utf-8").trim();
452
- fs2.unlinkSync(tmpFile);
453
- if (content) {
454
- ctx.processInput(content);
455
- } else {
456
- console.log(chalk2.dim("\n Empty input, cancelled.\n"));
457
- }
458
- } catch {
459
- console.log(chalk2.red("\n Editor failed.\n"));
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
- case "plan": {
464
- const arg = parts[1];
465
- if (arg === "off" || ctx.planMode) {
466
- ctx.setPlanMode(false);
467
- console.log(chalk2.green("\n Plan mode off. Full tools available.\n"));
468
- } else {
469
- ctx.setPlanMode(true);
470
- console.log(chalk2.cyan("\n [PLAN MODE] Read-only tools only. Use /plan off to exit.\n"));
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
- break;
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
- case "quit":
475
- case "exit": {
476
- const { exitTerminal: exitTerminal2 } = await import("./terminal-ILMO7Z3P.js");
477
- exitTerminal2();
478
- console.log(chalk2.dim(" Goodbye!"));
479
- process.exit(0);
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
- console.log(chalk2.yellow(`
484
- Unknown command: /${command}. Type /help for available commands.
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/logger.js
490
- import fs3 from "fs";
491
- import path3 from "path";
492
- var LOG_DIR = path3.join(process.cwd(), ".workermill");
493
- var LOG_FILE = path3.join(LOG_DIR, "cli.log");
494
- var logStream = null;
495
- function ensureLogDir() {
496
- if (!fs3.existsSync(LOG_DIR)) {
497
- fs3.mkdirSync(LOG_DIR, { recursive: true });
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 getStream() {
501
- if (!logStream) {
502
- ensureLogDir();
503
- logStream = fs3.createWriteStream(LOG_FILE, { flags: "a" });
504
- }
505
- return logStream;
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
- function timestamp() {
508
- return (/* @__PURE__ */ new Date()).toISOString();
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 log(level, message, data) {
511
- const entry = data ? `[${timestamp()}] ${level}: ${message} ${JSON.stringify(data)}` : `[${timestamp()}] ${level}: ${message}`;
512
- getStream().write(entry + "\n");
1326
+ function formatCost(c) {
1327
+ if (c < 0.01) return "$0.00";
1328
+ return `$${c.toFixed(2)}`;
513
1329
  }
514
- function info(message, data) {
515
- log("INFO", message, data);
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
- function error(message, data) {
518
- log("ERROR", message, data);
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
- function debug(message, data) {
521
- log("DEBUG", message, data);
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 flush() {
524
- if (logStream) {
525
- logStream.end();
526
- logStream = null;
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/agent.js
531
- var HISTORY_FILE = pathModule.join(process.env.HOME || "~", ".workermill", "history");
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 (fs4.existsSync(HISTORY_FILE)) {
536
- const lines = fs4.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean);
537
- return lines.slice(-MAX_HISTORY);
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
- const dir = pathModule.dirname(HISTORY_FILE);
546
- if (!fs4.existsSync(dir))
547
- fs4.mkdirSync(dir, { recursive: true });
548
- fs4.appendFileSync(HISTORY_FILE, line + "\n", "utf-8");
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
- var SLASH_COMMANDS = [
553
- "/help",
554
- "/clear",
555
- "/compact",
556
- "/status",
557
- "/model",
558
- "/quit",
559
- "/cost",
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
- return [[], line];
572
- }
573
- async function runAgent(config, trustAll, resume, startInPlanMode, fullDisk) {
574
- const { provider, model: modelName, apiKey, host } = getProviderForPersona(config);
575
- if (apiKey) {
576
- const envMap = {
577
- anthropic: "ANTHROPIC_API_KEY",
578
- openai: "OPENAI_API_KEY",
579
- google: "GOOGLE_API_KEY"
580
- };
581
- const envVar = envMap[provider];
582
- if (envVar && !process.env[envVar]) {
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
- const aiProvider = provider;
587
- const model = createModel(aiProvider, modelName, host);
588
- const workingDir = process.cwd();
589
- const sandboxed = !fullDisk;
590
- const tools = createToolDefinitions(workingDir, model, sandboxed);
591
- const permissions = new PermissionManager(trustAll);
592
- let session;
593
- if (resume) {
594
- const loaded = loadLatestSession();
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
- let agentState = "idle";
607
- let currentAbortController = null;
608
- let lastCtrlCTime = 0;
609
- let planMode = startInPlanMode || false;
610
- const costTracker = new CostTracker();
611
- const READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
612
- function getActiveTools() {
613
- if (!planMode)
614
- return permissionedTools;
615
- const filtered = {};
616
- for (const [name, def] of Object.entries(permissionedTools)) {
617
- if (READ_ONLY_TOOLS.has(name)) {
618
- filtered[name] = def;
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
- return filtered;
622
- }
623
- const permissionedTools = {};
624
- for (const [name, toolDef] of Object.entries(tools)) {
625
- const td = toolDef;
626
- permissionedTools[name] = {
627
- ...td,
628
- execute: async (input) => {
629
- const prevState = agentState;
630
- agentState = "permission_waiting";
631
- const allowed = await permissions.checkPermission(name, input);
632
- if (!allowed) {
633
- agentState = prevState;
634
- debug("Tool denied by user", { tool: name });
635
- return "Tool execution denied by user.";
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
- info("Tool call", { tool: name, input: JSON.stringify(input).slice(0, 200) });
638
- printToolCall(name, input);
639
- agentState = "tool_executing";
640
- const result = await td.execute(input);
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
- Working directory: ${workingDir}
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
- Guidelines:
714
- - Be concise and direct in your responses
715
- - Use tools proactively to explore the codebase before making changes
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
- info("User message", { length: input.length, preview: input.slice(0, 100) });
750
- if (session.messages.length <= 1) {
751
- const { classifyComplexity, runOrchestration } = await import("./orchestrator-NMTZUS23.js");
752
- console.log(chalk3.dim("\n Analyzing task complexity..."));
753
- info("Running complexity classifier");
754
- const classification = await classifyComplexity(config, input);
755
- info("Classification result", { isMulti: classification.isMulti, reason: classification.reason });
756
- if (classification.isMulti) {
757
- console.log(chalk3.dim(` \u2192 multi-expert (${classification.reason})
758
- `));
759
- await runOrchestration(config, input, trustAll, sandboxed, rl);
760
- processing = false;
761
- rl.resume();
762
- promptWithStatus();
763
- return;
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
- console.log(chalk3.dim(` \u2192 single agent (${classification.reason})
766
- `));
1773
+ agent.addUserMessage(`/build ${arg}`);
1774
+ orchestrator.start(arg, props.trustAll, props.sandboxed);
767
1775
  }
1776
+ break;
768
1777
  }
769
- info("Starting streamText", { model: modelName, messageCount: session.messages.length });
770
- const thinkingSpinner = ora2({ stream: process.stdout, text: chalk3.dim("Thinking..."), prefixText: " " }).start();
771
- currentAbortController = new AbortController();
772
- const timeoutId = setTimeout(() => currentAbortController?.abort(), 10 * 60 * 1e3);
773
- agentState = "streaming";
774
- const stream = streamText({
775
- model,
776
- system: systemPrompt,
777
- messages: session.messages.map((m) => ({
778
- role: m.role,
779
- content: m.content
780
- })),
781
- tools: getActiveTools(),
782
- stopWhen: stepCountIs(100),
783
- abortSignal: currentAbortController.signal
784
- });
785
- let fullText = "";
786
- let spinnerStopped = false;
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
- process.stdout.write(chalk3.white(chunk));
794
- fullText += chunk;
1797
+ break;
795
1798
  }
796
- clearTimeout(timeoutId);
797
- agentState = "idle";
798
- currentAbortController = null;
799
- if (!spinnerStopped) {
800
- thinkingSpinner.stop();
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
- if (fullText.trim()) {
804
- process.stdout.write("\n");
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
- const usage = await stream.totalUsage;
807
- const tokens = (usage?.inputTokens || 0) + (usage?.outputTokens || 0);
808
- session.totalTokens += tokens;
809
- costTracker.addUsage("agent", provider, modelName, usage?.inputTokens || 0, usage?.outputTokens || 0);
810
- const finalText = await stream.text;
811
- info("Response complete", { tokens, textLength: finalText.length });
812
- addMessage(session, "assistant", finalText);
813
- saveSession(session);
814
- const compactionLevel = shouldCompact(session.totalTokens, modelName);
815
- if (compactionLevel !== "none") {
816
- const spinner = ora2({ stream: process.stdout, text: `Compacting conversation (${compactionLevel})...`, prefixText: " " }).start();
817
- const plainMessages = session.messages.map((m) => ({ role: m.role, content: m.content }));
818
- const compacted = await compactMessages(model, plainMessages, compactionLevel);
819
- session.messages = compacted.map((m) => ({
820
- role: m.role,
821
- content: m.content,
822
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
823
- }));
824
- saveSession(session);
825
- spinner.succeed("Conversation compacted");
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
- const bar = printStatusBar(provider, modelName, session.totalTokens, planMode ? "PLAN" : trustAll ? "trust all" : "ask", costTracker.getTotalCost());
828
- setStatusBar(bar);
829
- } catch (err) {
830
- agentState = "idle";
831
- currentAbortController = null;
832
- if (err instanceof Error && err.name === "AbortError") {
833
- } else {
834
- error("Agent error", { error: err instanceof Error ? err.message : String(err) });
835
- printError(err instanceof Error ? err.message : String(err));
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
- processing = false;
839
- rl.resume();
840
- promptWithStatus();
841
- })();
842
- }
843
- rl.on("line", (input) => {
844
- if (permissions.questionActive)
845
- return;
846
- if (input.trim() === "```" && !multilineBacktick && !multilineBackslash) {
847
- multilineBacktick = true;
848
- multilineBuffer = [];
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
- processInput(fullInput);
861
- return;
862
- }
863
- if (multilineBacktick) {
864
- multilineBuffer.push(input);
865
- process.stdout.write(chalk3.dim(" ... "));
866
- return;
867
- }
868
- if (input.endsWith("\\") && !input.endsWith("\\\\")) {
869
- if (!multilineBackslash) {
870
- multilineBackslash = true;
871
- multilineBuffer = [input.slice(0, -1)];
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
- multilineBuffer.push(input.slice(0, -1));
1907
+ agent.addSystemMessage(`${header}
1908
+
1909
+ (no output)`);
874
1910
  }
875
- process.stdout.write(chalk3.dim(" ... "));
876
- return;
877
- }
878
- if (multilineBackslash) {
879
- multilineBackslash = false;
880
- multilineBuffer.push(input);
881
- const fullInput = multilineBuffer.join("\n");
882
- multilineBuffer = [];
883
- if (!fullInput.trim() || processing) {
884
- rl.prompt();
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
- processInput(fullInput);
888
- return;
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
- rl.prompt();
908
- return;
909
- }
910
- if (trimmed.startsWith("/")) {
911
- processing = true;
912
- rl.pause();
913
- handleCommand(trimmed, cmdCtx).then(() => {
914
- processing = false;
915
- rl.resume();
916
- promptWithStatus();
917
- });
918
- return;
919
- }
920
- processInput(trimmed);
921
- });
922
- rl.on("close", () => {
923
- info("Session ended", { totalTokens: session.totalTokens, messages: session.messages.length });
924
- flush();
925
- const cleanup = () => {
926
- saveSession(session);
927
- exitTerminal();
928
- console.log(chalk3.dim(" Goodbye!"));
929
- process.exit(0);
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.1.7";
946
- var program = new Command().name("workermill").description("AI coding agent with multi-expert orchestration").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("--auto-revise", "Auto-revise on failed reviews without prompting").option("--max-revisions <n>", "Max review\u2192revise cycles (default: 2)", parseInt).option("--critic", "Run separate critic pass on plan before execution").option("--full-disk", "Allow tools to access files outside working directory (default: restricted to cwd)").action(async (options) => {
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
- if (options.autoRevise || options.maxRevisions || options.critic) {
961
- config.review = {
962
- ...config.review,
963
- ...options.autoRevise ? { autoRevise: true } : {},
964
- ...options.maxRevisions ? { maxRevisions: options.maxRevisions } : {},
965
- ...options.critic ? { useCritic: true } : {}
966
- };
967
- }
968
- const fullDisk = options.fullDisk || false;
969
- await runAgent(config, options.trust || false, options.resume || false, options.plan || false, fullDisk);
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();