miii-agent 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +158 -41
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -184,7 +184,7 @@ async function modelContext(entry, model) {
184
184
  }
185
185
  const msg = err instanceof Error ? err.message : String(err);
186
186
  if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
187
- throw new Error(`Model "${model}" not found. Run: ollama pull ${model}`);
187
+ throw new Error(`I don't see a model called "${model}" on Ollama. Pull it first with: ollama pull ${model}`);
188
188
  }
189
189
  throw err;
190
190
  }
@@ -240,7 +240,7 @@ async function* chat(entry, model, messages, tools, opts) {
240
240
  }
241
241
  const msg = err instanceof Error ? err.message : String(err);
242
242
  if (msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("unknown model")) {
243
- throw new Error(`Model "${model}" not found. Run: ollama pull ${model}`);
243
+ throw new Error(`I don't see a model called "${model}" on Ollama. Pull it first with: ollama pull ${model}`);
244
244
  }
245
245
  throw err;
246
246
  }
@@ -278,8 +278,8 @@ var NOT_INSTALLED, NOT_RUNNING, LOCAL_HOST_RE, HARMONY_RE, CHANNEL_LABEL_RE;
278
278
  var init_ollama = __esm({
279
279
  "src/llm/ollama.ts"() {
280
280
  "use strict";
281
- NOT_INSTALLED = "Ollama is not installed. Install it with: npm i -g ollama\nOr download from https://ollama.com/download";
282
- NOT_RUNNING = "Ollama is not running. Start it with: ollama serve";
281
+ NOT_INSTALLED = "I can't find Ollama on this machine. Install it with: npm i -g ollama\nOr download it from https://ollama.com/download";
282
+ NOT_RUNNING = "Ollama isn't running, so I can't reach it. Start it with: ollama serve";
283
283
  LOCAL_HOST_RE = /localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/;
284
284
  HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
285
285
  CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
@@ -288,7 +288,7 @@ var init_ollama = __esm({
288
288
 
289
289
  // src/llm/openai.ts
290
290
  function notAvailable(entry) {
291
- return `Cannot reach OpenAI-compatible provider at ${entry.baseUrl}. Make sure the server is running and the baseUrl is correct.`;
291
+ return `I couldn't reach the provider at ${entry.baseUrl}. Check that the server is running and the baseUrl is correct.`;
292
292
  }
293
293
  function headers(entry) {
294
294
  const h = { "Content-Type": "application/json" };
@@ -314,7 +314,7 @@ async function listModels2(entry) {
314
314
  detail = await res.text();
315
315
  } catch {
316
316
  }
317
- throw new Error(`Provider error (HTTP ${res.status}): ${detail || res.statusText}`);
317
+ throw new Error(`The provider came back with an error (HTTP ${res.status}): ${detail || res.statusText}`);
318
318
  }
319
319
  const body = await res.json();
320
320
  return body.data.map((m) => m.id);
@@ -406,12 +406,12 @@ async function* chat2(entry, model, messages, tools, opts) {
406
406
  } catch {
407
407
  }
408
408
  if (res.status === 404) {
409
- throw new Error(`Model "${model}" not found at ${entry.baseUrl}. Make sure it's available.`);
409
+ throw new Error(`The provider at ${entry.baseUrl} has no model called "${model}". Double-check the name, or pick another with /models.`);
410
410
  }
411
- throw new Error(`Provider error (HTTP ${res.status}): ${detail || res.statusText}`);
411
+ throw new Error(`The provider came back with an error (HTTP ${res.status}): ${detail || res.statusText}`);
412
412
  }
413
413
  const reader = res.body?.getReader();
414
- if (!reader) throw new Error("No response body");
414
+ if (!reader) throw new Error("The provider answered but sent no body to read.");
415
415
  const decoder = new TextDecoder();
416
416
  let buffer = "";
417
417
  try {
@@ -470,7 +470,7 @@ async function* chat2(entry, model, messages, tools, opts) {
470
470
  throw new Error(notAvailable(entry));
471
471
  }
472
472
  if (timeoutSignal.aborted && !opts?.signal?.aborted) {
473
- throw new Error(`Provider request timed out after ${TIMEOUT_MS / 1e3}s. The model may still be loading or thinking.`);
473
+ throw new Error(`The provider didn't respond within ${TIMEOUT_MS / 1e3}s, so I stopped waiting. The model may still be loading or thinking \u2014 give it another try.`);
474
474
  }
475
475
  throw err;
476
476
  }
@@ -678,14 +678,14 @@ function isUnder(parent, child) {
678
678
  }
679
679
  function confinePath(p) {
680
680
  if (typeof p !== "string" || p.length === 0) {
681
- throw new Error("Path is required.");
681
+ throw new Error("I need a file path here, but none was given.");
682
682
  }
683
683
  const root = process.cwd();
684
684
  const abs = resolve(root, p);
685
685
  if (isUnder(root, abs) || isUnder(SPILL_DIR, abs)) {
686
686
  return abs;
687
687
  }
688
- throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
688
+ throw new Error(`"${p}" sits outside the working directory (${root}), so I can't touch it. Stay within the project folder.`);
689
689
  }
690
690
  var SPILL_DIR;
691
691
  var init_paths = __esm({
@@ -787,38 +787,38 @@ function nearMiss(src, old_str) {
787
787
  const width = String(to).length;
788
788
  const ctx = srcLines.slice(from, to).map((l, i) => `${String(from + i + 1).padStart(width, " ")} ${l}`).join("\n");
789
789
  return `
790
- Closest text in file (lines ${from + 1}-${to}):
790
+ Here's the closest text I could find (lines ${from + 1}-${to}) \u2014 copy it exactly:
791
791
  ${ctx}`;
792
792
  }
793
793
  function locate(src, old_str) {
794
794
  const first = src.indexOf(old_str);
795
795
  if (first !== -1) {
796
796
  if (src.indexOf(old_str, first + 1) !== -1) {
797
- return { error: `old_str not unique \u2014 add surrounding context to disambiguate.` };
797
+ return { error: `That text shows up in more than one place, so I can't tell which one you mean. Add a line or two around it to make it unique.` };
798
798
  }
799
799
  return [first, first + old_str.length];
800
800
  }
801
801
  const fuzzy = fuzzyRange(src, old_str);
802
802
  if (fuzzy) return fuzzy;
803
- return { error: `old_str not found.${nearMiss(src, old_str)}` };
803
+ return { error: `I couldn't find that text \u2014 it may differ by whitespace or a stray character.${nearMiss(src, old_str)}` };
804
804
  }
805
805
  function applyBatch(src, edits) {
806
806
  const ranges = [];
807
807
  for (let i = 0; i < edits.length; i++) {
808
808
  const { old_str, new_str } = edits[i];
809
809
  if (typeof old_str !== "string" || typeof new_str !== "string") {
810
- return { error: `edits[${i}] must have string old_str and new_str.` };
810
+ return { error: `Edit #${i + 1} is missing text \u2014 each edit needs both an old_str and a new_str string.` };
811
811
  }
812
- if (old_str === "") return { error: `edits[${i}].old_str is empty.` };
813
- if (old_str === new_str) return { error: `edits[${i}] old_str and new_str are identical \u2014 nothing to change.` };
812
+ if (old_str === "") return { error: `Edit #${i + 1} has an empty old_str, so there's nothing to look for. Give it the exact text you want to replace.` };
813
+ if (old_str === new_str) return { error: `Edit #${i + 1} has the same old_str and new_str, so it wouldn't change anything.` };
814
814
  const r = locate(src, old_str);
815
- if (!Array.isArray(r)) return { error: `edits[${i}]: ${r.error}` };
815
+ if (!Array.isArray(r)) return { error: `Edit #${i + 1}: ${r.error}` };
816
816
  ranges.push({ start: r[0], end: r[1], new_str });
817
817
  }
818
818
  const sorted = [...ranges].sort((a, b) => a.start - b.start);
819
819
  for (let i = 1; i < sorted.length; i++) {
820
820
  if (sorted[i].start < sorted[i - 1].end) {
821
- return { error: `edits overlap in the file \u2014 split them into separate calls or widen the context.` };
821
+ return { error: `Two of these edits touch the same spot, so I can't apply them together. Split them into separate calls, or widen each old_str so they don't overlap.` };
822
822
  }
823
823
  }
824
824
  let out = src;
@@ -869,11 +869,11 @@ var init_edit_file = __esm({
869
869
  return { content: `Edited ${path} (${res.count} edits).${verifyHint(path)}` };
870
870
  }
871
871
  if (typeof old_str !== "string" || typeof new_str !== "string") {
872
- return { content: `edit_file needs old_str and new_str (or an edits[] array) for ${path}.`, is_error: true };
872
+ return { content: `To edit ${path} I need either an old_str/new_str pair or an edits[] array \u2014 neither came through.`, is_error: true };
873
873
  }
874
874
  if (old_str === new_str) {
875
875
  return {
876
- content: `old_str and new_str are identical \u2014 nothing to change in ${path}. If the file is already correct, do NOT edit again: finish with the respond action and tell the user it is done.`,
876
+ content: `old_str and new_str are the same, so there's nothing to change in ${path}. If the file is already correct, don't edit it again \u2014 finish with the respond action and tell the user it's done.`,
877
877
  is_error: true
878
878
  };
879
879
  }
@@ -890,12 +890,12 @@ var init_edit_file = __esm({
890
890
  return { content: `Edited ${path} (whitespace-tolerant match).${verifyHint(path)}` };
891
891
  }
892
892
  }
893
- return { content: `old_str not found in ${path}.${nearMiss(src, old_str)}`, is_error: true };
893
+ return { content: `I couldn't find that text in ${path} \u2014 it may differ by whitespace or a stray character.${nearMiss(src, old_str)}`, is_error: true };
894
894
  }
895
895
  const all = replace_all === true;
896
896
  if (!all && src.indexOf(old_str, first + 1) !== -1) {
897
897
  return {
898
- content: `old_str not unique in ${path}. Add surrounding context to disambiguate, or set replace_all.`,
898
+ content: `That text appears more than once in ${path}, so I don't know which one to change. Add a line or two around it to single it out, or set replace_all to change them all.`,
899
899
  is_error: true
900
900
  };
901
901
  }
@@ -953,7 +953,7 @@ var init_read_file = __esm({
953
953
  if (IMAGE_EXT.has(ext) || looksImage(buf)) {
954
954
  if (buf.length > MAX_IMAGE_BYTES) {
955
955
  return {
956
- content: `${path} is an image but too large to attach (${buf.length} bytes > ${MAX_IMAGE_BYTES}). Resize it first.`,
956
+ content: `${path} is an image, but at ${buf.length} bytes it's too big to attach (limit is ${MAX_IMAGE_BYTES}). Resize it and try again.`,
957
957
  is_error: true
958
958
  };
959
959
  }
@@ -963,7 +963,7 @@ var init_read_file = __esm({
963
963
  };
964
964
  }
965
965
  if (buf.subarray(0, 8e3).includes(0)) {
966
- return { content: `${path} looks binary (${buf.length} bytes); not reading as text.`, is_error: true };
966
+ return { content: `${path} looks like a binary file (${buf.length} bytes), so I'm not reading it as text.`, is_error: true };
967
967
  }
968
968
  const raw = buf.toString("utf-8").replace(/\r\n/g, "\n");
969
969
  const allLines = raw.split("\n");
@@ -975,7 +975,7 @@ var init_read_file = __esm({
975
975
  let body = numbered(slice, start);
976
976
  if (body.length > MAX_CHARS) {
977
977
  body = body.slice(0, MAX_CHARS) + `
978
- [truncated: output exceeded ${MAX_CHARS} chars \u2014 use offset/limit]`;
978
+ [There's more \u2014 this hit the ${MAX_CHARS}-char limit. Use offset/limit to read the rest.]`;
979
979
  }
980
980
  if (ranged) {
981
981
  const end = start - 1 + slice.length;
@@ -1047,7 +1047,7 @@ function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
1047
1047
  const head = Math.floor(budget * HEAD_FRACTION);
1048
1048
  const tail = budget - head;
1049
1049
  const preview = full.slice(0, head) + "\n\u2026\n" + full.slice(-tail);
1050
- const notice = path ? `[${label} truncated: ${full.length} bytes. Full output at ${path} \u2014 read it with read_file offset/limit to see the elided middle.]` : `[${label} truncated to ${budget} bytes; spill to disk failed, middle is lost.]`;
1050
+ const notice = path ? `[This ${label} was long (${full.length} bytes), so I'm showing the start and end. The full text is saved at ${path} \u2014 read it with read_file offset/limit to see the middle.]` : `[This ${label} was too long to show in full, and I couldn't save the rest to disk, so the middle is missing.]`;
1051
1051
  return `${preview}
1052
1052
  ${notice}`;
1053
1053
  }
@@ -1130,8 +1130,8 @@ var init_run_bash = __esm({
1130
1130
  const out = all ?? "";
1131
1131
  const is_error = aborted || timedOut || exitCode !== 0;
1132
1132
  const note = timedOut ? `
1133
- [timed out after ${timeout}ms \u2014 process tree killed]` : aborted ? `
1134
- [aborted \u2014 process tree killed]` : "";
1133
+ [Timed out after ${timeout}ms, so I stopped the command and everything it started. Raise timeout_ms for long builds or test suites.]` : aborted ? `
1134
+ [Cancelled \u2014 I stopped the command and everything it started.]` : "";
1135
1135
  const body = out || (is_error ? `(no output)` : "");
1136
1136
  const content = `${spillIfLarge(body, "command output")}
1137
1137
  [exit ${exitCode ?? "killed"}]${note}`;
@@ -1240,8 +1240,9 @@ var init_grep = __esm({
1240
1240
  const lines = (res.stdout ?? "").split("\n").slice(0, limit);
1241
1241
  const out = lines.join("\n");
1242
1242
  const code = res.exitCode ?? 0;
1243
- if (!out && code === 1) return { content: "No matches." };
1244
- return { content: out || res.stderr || "No matches.", is_error: code > 1 };
1243
+ const noMatch = `Nothing matched "${pattern}". Try a looser pattern, case_insensitive, or a wider path.`;
1244
+ if (!out && code === 1) return { content: noMatch };
1245
+ return { content: out || res.stderr || noMatch, is_error: code > 1 };
1245
1246
  } catch (err) {
1246
1247
  return { content: err instanceof Error ? err.message : String(err), is_error: true };
1247
1248
  }
@@ -1316,7 +1317,7 @@ var init_glob = __esm({
1316
1317
  res = await tryFind();
1317
1318
  }
1318
1319
  const all = (res.stdout ?? "").split("\n").filter(Boolean);
1319
- if (all.length === 0) return { content: "No files matched." };
1320
+ if (all.length === 0) return { content: `Nothing matched "${pattern}". Try a broader pattern or a different path.` };
1320
1321
  const lines = byMtimeDesc(all).slice(0, limit);
1321
1322
  return { content: lines.join("\n") };
1322
1323
  } catch (err) {
@@ -1327,6 +1328,70 @@ var init_glob = __esm({
1327
1328
  }
1328
1329
  });
1329
1330
 
1331
+ // src/tools/write_todos.ts
1332
+ function render(todos) {
1333
+ const done = todos.filter((t) => t.status === "completed").length;
1334
+ const header = `Todos (${done}/${todos.length} done):`;
1335
+ const lines = todos.map((t) => `${MARK[t.status]} ${t.content}`);
1336
+ return [header, ...lines].join("\n");
1337
+ }
1338
+ var STATUSES, MARK, write_todos;
1339
+ var init_write_todos = __esm({
1340
+ "src/tools/write_todos.ts"() {
1341
+ "use strict";
1342
+ STATUSES = ["pending", "in_progress", "completed"];
1343
+ MARK = {
1344
+ completed: "[x]",
1345
+ in_progress: "[~]",
1346
+ pending: "[ ]"
1347
+ };
1348
+ write_todos = {
1349
+ name: "write_todos",
1350
+ description: "Create and maintain a task list for the current session, shown to the user as a live checklist. Pass the FULL list every call \u2014 it replaces the previous one (all or nothing), so include finished items too. Each todo is {content, status} where status is pending | in_progress | completed. Keep exactly one item in_progress at a time and mark it completed before starting the next. Use for multi-step work (features, refactors, multi-file changes) so the user can see what is done, in progress, and left. Skip it for single trivial actions.",
1351
+ input_schema: {
1352
+ type: "object",
1353
+ properties: {
1354
+ todos: {
1355
+ type: "array",
1356
+ description: "The full, ordered task list. Replaces the prior list entirely.",
1357
+ items: {
1358
+ type: "object",
1359
+ properties: {
1360
+ content: { type: "string", description: 'Short task description (imperative, e.g. "Add webfetch tool").' },
1361
+ status: { type: "string", enum: STATUSES, description: "pending | in_progress | completed" }
1362
+ },
1363
+ required: ["content", "status"]
1364
+ }
1365
+ }
1366
+ },
1367
+ required: ["todos"]
1368
+ },
1369
+ handler: ({ todos }) => {
1370
+ if (!Array.isArray(todos)) {
1371
+ return { content: "I need a todos array here \u2014 pass the full list of tasks.", is_error: true };
1372
+ }
1373
+ if (todos.length === 0) {
1374
+ return { content: "The todo list is empty \u2014 give me at least one task.", is_error: true };
1375
+ }
1376
+ for (let i = 0; i < todos.length; i++) {
1377
+ const t = todos[i];
1378
+ if (!t || typeof t.content !== "string" || t.content.trim() === "") {
1379
+ return { content: `Task #${i + 1} needs some text \u2014 its content can't be empty.`, is_error: true };
1380
+ }
1381
+ if (!STATUSES.includes(t.status)) {
1382
+ return { content: `Task #${i + 1} has an unknown status. Use one of: ${STATUSES.join(", ")}.`, is_error: true };
1383
+ }
1384
+ }
1385
+ const active2 = todos.filter((t) => t.status === "in_progress").length;
1386
+ if (active2 > 1) {
1387
+ return { content: `Keep just one task in progress at a time \u2014 I see ${active2}. Finish one before starting the next.`, is_error: true };
1388
+ }
1389
+ return { content: render(todos) };
1390
+ }
1391
+ };
1392
+ }
1393
+ });
1394
+
1330
1395
  // src/tools/registry.ts
1331
1396
  function getTool(name) {
1332
1397
  return TOOLS.find((t) => t.name === name);
@@ -1355,13 +1420,15 @@ var init_registry = __esm({
1355
1420
  init_run_bash();
1356
1421
  init_grep();
1357
1422
  init_glob();
1423
+ init_write_todos();
1358
1424
  TOOLS = [
1359
1425
  edit_file,
1360
1426
  read_file,
1361
1427
  write_file,
1362
1428
  run_bash,
1363
1429
  grep,
1364
- glob
1430
+ glob,
1431
+ write_todos
1365
1432
  ];
1366
1433
  }
1367
1434
  });
@@ -1424,7 +1491,7 @@ function validateInput(schema, input) {
1424
1491
  const result = toZod(schema).safeParse(input ?? {});
1425
1492
  if (result.success) return null;
1426
1493
  const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
1427
- return `Invalid arguments: ${issues}`;
1494
+ return `Some arguments didn't look right: ${issues}`;
1428
1495
  }
1429
1496
  var init_validate = __esm({
1430
1497
  "src/tools/validate.ts"() {
@@ -1563,6 +1630,14 @@ Ask in a numbered list. One round of questions per turn. Then wait.
1563
1630
  You have access to the following tools. Call them via the function-calling interface.
1564
1631
  ${toolLines}
1565
1632
 
1633
+ # Task list (write_todos)
1634
+ For multi-step work \u2014 a feature, a refactor, anything touching multiple files or more than ~3 steps \u2014 track it with write_todos so the user sees a live checklist of what is done, in progress, and left.
1635
+ - Create the list right after your plan, before the first real action. Each item is {content, status}: short imperative content, status pending | in_progress | completed.
1636
+ - Pass the FULL list every call; it replaces the previous one. Include already-finished items so nothing disappears.
1637
+ - Mark exactly ONE item in_progress at a time. Set it in_progress when you start it, completed the moment it is done, then move the next to in_progress. Never leave a finished task pending or two tasks in_progress.
1638
+ - Keep items outcome-sized, not tool-sized \u2014 "Add webfetch tool and wire it in", not "call edit_file". Aim for a handful, not twenty.
1639
+ - Do NOT use write_todos for trivial single-step work \u2014 a lone read, one edit, a quick search. It is overhead there.
1640
+
1566
1641
  # Loop semantics
1567
1642
  - When you need to act on the filesystem or run a command, emit a tool call.
1568
1643
  - After each tool result, decide: more tool calls, or a final plain-text answer.
@@ -1858,7 +1933,7 @@ function readGuard(name, input, seen) {
1858
1933
  if (seen.has(abs)) return null;
1859
1934
  if (name === "write_file" && !existsSync5(abs)) return null;
1860
1935
  const verb = name === "edit_file" ? "edit" : "overwrite";
1861
- return `Refusing to ${verb} ${p}: you have not read it this turn. Call read_file on ${p} first, then retry the ${name}.`;
1936
+ return `I won't ${verb} ${p} without seeing it first \u2014 I don't want to clobber something. Read it with read_file, then retry the ${name}.`;
1862
1937
  }
1863
1938
  function unwrapEnvelope(name, input) {
1864
1939
  if (!("arguments" in input)) return input;
@@ -2040,7 +2115,7 @@ async function* runAgent(opts) {
2040
2115
  const r2 = {
2041
2116
  type: "tool_result",
2042
2117
  tool_use_id: use.id,
2043
- content: `Unknown tool: ${use.name}`,
2118
+ content: `Unknown tool: ${use.name}. There's no tool by that name \u2014 check the spelling against the tools you were given.`,
2044
2119
  is_error: true
2045
2120
  };
2046
2121
  results.push(r2);
@@ -2066,7 +2141,7 @@ async function* runAgent(opts) {
2066
2141
  const r2 = {
2067
2142
  type: "tool_result",
2068
2143
  tool_use_id: use.id,
2069
- content: `Permission denied for ${use.name}.`,
2144
+ content: `Permission denied \u2014 the user chose not to run ${use.name}. Try a different approach, or ask them what they'd prefer.`,
2070
2145
  is_error: true
2071
2146
  };
2072
2147
  results.push(r2);
@@ -2368,7 +2443,7 @@ var init_run = __esm({
2368
2443
  });
2369
2444
 
2370
2445
  // src/cli.tsx
2371
- import { render } from "ink";
2446
+ import { render as render2 } from "ink";
2372
2447
  import { createElement } from "react";
2373
2448
 
2374
2449
  // src/ui/App.tsx
@@ -3658,7 +3733,8 @@ var TOOL_LABEL = {
3658
3733
  read_file: "Read",
3659
3734
  run_bash: "Bash",
3660
3735
  glob: "Glob",
3661
- grep: "Grep"
3736
+ grep: "Grep",
3737
+ write_todos: "Todos"
3662
3738
  };
3663
3739
  var EXT_LANG = {
3664
3740
  ts: "typescript",
@@ -3760,6 +3836,43 @@ function FileEditBlock({
3760
3836
  ] }) })
3761
3837
  ] });
3762
3838
  }
3839
+ function TodoBlock({ todos }) {
3840
+ const done = todos.filter((t) => t.status === "completed").length;
3841
+ const doing = todos.filter((t) => t.status === "in_progress").length;
3842
+ const glyph = { completed: "\u2714", in_progress: "\u25B6", pending: "\u25CB" };
3843
+ const color = { completed: "green", in_progress: "yellow", pending: "gray" };
3844
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
3845
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3846
+ /* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
3847
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: "Todos " }),
3848
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
3849
+ "(",
3850
+ done,
3851
+ "/",
3852
+ todos.length,
3853
+ " done",
3854
+ doing > 0 ? `, ${doing} in progress` : "",
3855
+ ")"
3856
+ ] })
3857
+ ] }),
3858
+ todos.map((t, i) => /* @__PURE__ */ jsxs9(Box9, { marginLeft: 4, children: [
3859
+ /* @__PURE__ */ jsxs9(Text9, { color: color[t.status], children: [
3860
+ glyph[t.status],
3861
+ " "
3862
+ ] }),
3863
+ /* @__PURE__ */ jsx9(
3864
+ Text9,
3865
+ {
3866
+ color: t.status === "in_progress" ? "yellow" : void 0,
3867
+ dimColor: t.status !== "in_progress",
3868
+ strikethrough: t.status === "completed",
3869
+ bold: t.status === "in_progress",
3870
+ children: t.content
3871
+ }
3872
+ )
3873
+ ] }, i))
3874
+ ] });
3875
+ }
3763
3876
  function toolHeader(use) {
3764
3877
  const label = TOOL_LABEL[use.name] ?? use.name;
3765
3878
  const input = use.input ?? {};
@@ -3839,6 +3952,10 @@ function ToolResultBlock({ result, toolName }) {
3839
3952
  ] });
3840
3953
  }
3841
3954
  function ToolUseLine({ use, result }) {
3955
+ if (use.name === "write_todos" && !result?.is_error) {
3956
+ const todos = use.input.todos;
3957
+ if (Array.isArray(todos) && todos.length > 0) return /* @__PURE__ */ jsx9(TodoBlock, { todos });
3958
+ }
3842
3959
  if (use.name === "write_file" && !result?.is_error) {
3843
3960
  const input = use.input;
3844
3961
  const content = input.content ?? "";
@@ -5196,5 +5313,5 @@ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
5196
5313
  process.on("exit", () => {
5197
5314
  if (process.stdout.isTTY) process.stdout.write("\x1B]2;\x07");
5198
5315
  });
5199
- render(createElement(App));
5316
+ render2(createElement(App));
5200
5317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "Local AI coding agent for your terminal — an open-source, offline alternative to Claude Code, Cursor, and GitHub Copilot, powered by Ollama. Private by default, free forever.",
5
5
  "type": "module",
6
6
  "bin": {