miii-agent 0.1.11 → 0.1.12

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 (3) hide show
  1. package/README.md +27 -1
  2. package/dist/cli.js +149 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -92,6 +92,7 @@ Once inside the TUI, just type naturally:
92
92
  | `/models` | Switch active Ollama model |
93
93
  | `/clear` | Reset conversation history |
94
94
  | `Esc` | Stop current generation or tool run |
95
+ | `Ctrl+O` | Toggle full tool output view |
95
96
  | `Ctrl+C` | Quit |
96
97
 
97
98
  ---
@@ -124,7 +125,7 @@ miii ships with a built-in tool suite the agent can invoke autonomously:
124
125
  |------|-------------|
125
126
  | `read_file` | Read any file in your workspace |
126
127
  | `write_file` | Create new files |
127
- | `edit_file` | Precise string-level edits (no rewrites) |
128
+ | `edit_file` | Precise string-level edits with whitespace tolerance (no rewrites) |
128
129
  | `glob` | Pattern-match files across the project |
129
130
  | `grep` | Regex search across files |
130
131
  | `run_bash` | Execute shell commands |
@@ -133,6 +134,24 @@ Every sensitive operation is gated by a permission system — you approve what t
133
134
 
134
135
  ---
135
136
 
137
+ ## Lossless output spill
138
+
139
+ Big tool outputs used to get truncated — a 50K-line test log chopped to 32K, the middle gone, the model guessing at what it missed. miii doesn't truncate. It **spills**.
140
+
141
+ When a tool result exceeds the inline budget (~10K bytes), the full output is written to `~/.miii/output/<id>.txt`. Only a head + tail **preview** is inlined into the conversation, followed by a pointer:
142
+
143
+ ```
144
+ [command output truncated: 5184 lines / 412900 bytes.
145
+ Full output at ~/.miii/output/9f3a1c.txt — read it with
146
+ read_file offset/limit to see the elided middle.]
147
+ ```
148
+
149
+ The head shows where output started; the tail catches the errors and summaries that live at the bottom. If the model needs the elided middle, it pages through it with `read_file` ranged reads — nothing is ever lost. The inline budget becomes "how much to show," not "how much exists."
150
+
151
+ Spill files are confined to the app-owned `~/.miii/output` dir and garbage-collected after 24h. If the spill write fails (e.g. read-only home), miii falls back to a lossy head+tail and says so explicitly, so the context window is never blown.
152
+
153
+ ---
154
+
136
155
  ## Checking your setup
137
156
 
138
157
  miii is model-agnostic — but not every local model can actually drive an agent. A model that can't emit clean tool calls will chat at you instead of editing files. `miii doctor` tells you which of *your* installed models are up to the job, before you waste time wondering why nothing happens.
@@ -195,6 +214,10 @@ graph TD
195
214
  ReadFile -.-> Confine["Path Confinement\n(tools/paths.ts)"]
196
215
  WriteFile -.-> Confine
197
216
  EditFile -.-> Confine
217
+ RunBash -->|"large output"| SpillMod["Output Spill\n(tools/spill.ts)"]
218
+ Grep -->|"large output"| SpillMod
219
+ Glob -->|"large output"| SpillMod
220
+ SpillMod -.->|"head+tail preview\n+ read pointer"| ToolRegistry
198
221
  end
199
222
 
200
223
  Adapter -->|"HTTP streaming"| Ollama["Ollama\n(local LLM server)"]
@@ -206,10 +229,13 @@ graph TD
206
229
  subgraph Storage ["Local Storage"]
207
230
  Config["~/.miii/config.json\n(model, host, effort)"]
208
231
  Rules["~/.miii/permissions.json\n(saved allow rules)"]
232
+ Spill["~/.miii/output/\n(spilled tool output)"]
209
233
  end
210
234
 
211
235
  App -.->|"reads"| Config
212
236
  Policy -.->|"reads / persists 'always'"| Rules
237
+ SpillMod -.->|"writes full output"| Spill
238
+ ReadFile -.->|"pages elided middle\n(offset/limit)"| Spill
213
239
  ```
214
240
 
215
241
  ---
package/dist/cli.js CHANGED
@@ -170,6 +170,39 @@ var init_paths = __esm({
170
170
  }
171
171
  });
172
172
 
173
+ // src/tools/verifyHint.ts
174
+ function verifyHint(path) {
175
+ const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
176
+ const cmds = {
177
+ ts: `npx tsc --noEmit`,
178
+ tsx: `npx tsc --noEmit`,
179
+ js: `node --check ${path}`,
180
+ jsx: `node --check ${path}`,
181
+ mjs: `node --check ${path}`,
182
+ cjs: `node --check ${path}`,
183
+ py: `python -m py_compile ${path}`,
184
+ go: `go build ./...`,
185
+ rs: `cargo check`,
186
+ rb: `ruby -c ${path}`,
187
+ php: `php -l ${path}`,
188
+ sh: `bash -n ${path}`,
189
+ bash: `bash -n ${path}`,
190
+ c: `gcc -fsyntax-only ${path}`,
191
+ h: `gcc -fsyntax-only ${path}`,
192
+ cpp: `g++ -fsyntax-only ${path}`,
193
+ cc: `g++ -fsyntax-only ${path}`,
194
+ java: `javac -d /tmp ${path}`
195
+ };
196
+ const cmd2 = cmds[ext];
197
+ if (!cmd2) return "";
198
+ return ` Now verify via run_bash: ${cmd2} \u2014 fix any errors it reports before continuing.`;
199
+ }
200
+ var init_verifyHint = __esm({
201
+ "src/tools/verifyHint.ts"() {
202
+ "use strict";
203
+ }
204
+ });
205
+
173
206
  // src/tools/edit_file.ts
174
207
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
175
208
  function similarity(a, b) {
@@ -182,6 +215,35 @@ function similarity(a, b) {
182
215
  for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] === y[i]) same++;
183
216
  return same / len;
184
217
  }
218
+ function fuzzyRange(src, old_str) {
219
+ const srcLines = src.split("\n");
220
+ const oldLines = old_str.split("\n");
221
+ const norm = (l) => l.trim();
222
+ const oldNorm = oldLines.map(norm);
223
+ const offsets = new Array(srcLines.length);
224
+ let acc = 0;
225
+ for (let i = 0; i < srcLines.length; i++) {
226
+ offsets[i] = acc;
227
+ acc += srcLines[i].length + 1;
228
+ }
229
+ const matches2 = [];
230
+ const window = oldLines.length;
231
+ for (let i = 0; i + window <= srcLines.length; i++) {
232
+ let ok = true;
233
+ for (let j = 0; j < window; j++) {
234
+ if (norm(srcLines[i + j]) !== oldNorm[j]) {
235
+ ok = false;
236
+ break;
237
+ }
238
+ }
239
+ if (!ok) continue;
240
+ const start = offsets[i];
241
+ const last = i + window - 1;
242
+ const end = offsets[last] + srcLines[last].length;
243
+ matches2.push([start, end]);
244
+ }
245
+ return matches2.length === 1 ? matches2[0] : null;
246
+ }
185
247
  function nearMiss(src, old_str) {
186
248
  const srcLines = src.split("\n");
187
249
  const needle = old_str.split("\n").find((l) => l.trim()) ?? old_str;
@@ -208,6 +270,7 @@ var init_edit_file = __esm({
208
270
  "src/tools/edit_file.ts"() {
209
271
  "use strict";
210
272
  init_paths();
273
+ init_verifyHint();
211
274
  edit_file = {
212
275
  name: "edit_file",
213
276
  description: "Replace an exact string in a file. old_str must be unique unless replace_all is set. On no match, returns the closest text in the file.",
@@ -230,6 +293,15 @@ var init_edit_file = __esm({
230
293
  const src = readFileSync3(abs, "utf-8");
231
294
  const first = src.indexOf(old_str);
232
295
  if (first === -1) {
296
+ if (replace_all !== true) {
297
+ const fuzzy = fuzzyRange(src, old_str);
298
+ if (fuzzy) {
299
+ const [s, e] = fuzzy;
300
+ const out2 = src.slice(0, s) + new_str + src.slice(e);
301
+ writeFileSync3(abs, out2, "utf-8");
302
+ return { content: `Edited ${path} (whitespace-tolerant match).${verifyHint(path)}` };
303
+ }
304
+ }
233
305
  return { content: `old_str not found in ${path}.${nearMiss(src, old_str)}`, is_error: true };
234
306
  }
235
307
  const all = replace_all === true;
@@ -242,7 +314,7 @@ var init_edit_file = __esm({
242
314
  const out = all ? src.split(old_str).join(new_str) : src.slice(0, first) + new_str + src.slice(first + old_str.length);
243
315
  const n = all ? src.split(old_str).length - 1 : 1;
244
316
  writeFileSync3(abs, out, "utf-8");
245
- return { content: `Edited ${path}${all ? ` (${n} occurrences)` : ""}` };
317
+ return { content: `Edited ${path}${all ? ` (${n} occurrences)` : ""}.${verifyHint(path)}` };
246
318
  } catch (err) {
247
319
  return { content: err instanceof Error ? err.message : String(err), is_error: true };
248
320
  }
@@ -315,6 +387,7 @@ var init_write_file = __esm({
315
387
  "src/tools/write_file.ts"() {
316
388
  "use strict";
317
389
  init_paths();
390
+ init_verifyHint();
318
391
  write_file = {
319
392
  name: "write_file",
320
393
  description: "Create or overwrite a file with the given content. Parent dirs auto-created.",
@@ -331,7 +404,7 @@ var init_write_file = __esm({
331
404
  const abs = confinePath(path);
332
405
  mkdirSync3(dirname(abs), { recursive: true });
333
406
  writeFileSync4(abs, content, "utf-8");
334
- return { content: `Wrote ${path} (${content.length} bytes)` };
407
+ return { content: `Wrote ${path} (${content.length} bytes).${verifyHint(path)}` };
335
408
  } catch (err) {
336
409
  return { content: err instanceof Error ? err.message : String(err), is_error: true };
337
410
  }
@@ -1348,7 +1421,7 @@ import { createElement } from "react";
1348
1421
 
1349
1422
  // src/ui/App.tsx
1350
1423
  init_client();
1351
- import { useState as useState4, useEffect as useEffect3 } from "react";
1424
+ import { useState as useState5, useEffect as useEffect4 } from "react";
1352
1425
  import { Box as Box10, Text as Text10, useApp } from "ink";
1353
1426
  import { homedir as homedir6 } from "os";
1354
1427
  import { sep as sep2 } from "path";
@@ -1790,6 +1863,7 @@ function FilePicker({ matches: matches2, cursor }) {
1790
1863
  }
1791
1864
 
1792
1865
  // src/ui/ChatView.tsx
1866
+ import { useState as useState3, useEffect as useEffect3 } from "react";
1793
1867
  import { Box as Box9, Text as Text9 } from "ink";
1794
1868
 
1795
1869
  // src/ui/ThinkingBlock.tsx
@@ -1850,6 +1924,24 @@ var EMPTY_STATE_TITLE = "Ask anything, or try:";
1850
1924
 
1851
1925
  // src/ui/ChatView.tsx
1852
1926
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1927
+ var COLLAPSED_LINES = 3;
1928
+ var globalToolExpanded = false;
1929
+ var toolExpandListeners = /* @__PURE__ */ new Set();
1930
+ function toggleToolExpanded() {
1931
+ globalToolExpanded = !globalToolExpanded;
1932
+ toolExpandListeners.forEach((fn) => fn());
1933
+ }
1934
+ function useToolExpanded() {
1935
+ const [expanded, setExpanded] = useState3(globalToolExpanded);
1936
+ useEffect3(() => {
1937
+ const handler = () => setExpanded(globalToolExpanded);
1938
+ toolExpandListeners.add(handler);
1939
+ return () => {
1940
+ toolExpandListeners.delete(handler);
1941
+ };
1942
+ }, []);
1943
+ return expanded;
1944
+ }
1853
1945
  function formatTokens(n) {
1854
1946
  if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
1855
1947
  return String(n);
@@ -1872,8 +1964,8 @@ function FileEditBlock({
1872
1964
  removed,
1873
1965
  previewLines
1874
1966
  }) {
1875
- const MAX = 16;
1876
- const shown = previewLines.slice(0, MAX);
1967
+ const expanded = useToolExpanded();
1968
+ const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
1877
1969
  const extra = previewLines.length - shown.length;
1878
1970
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
1879
1971
  /* @__PURE__ */ jsxs9(Box9, { children: [
@@ -1890,15 +1982,22 @@ function FileEditBlock({
1890
1982
  "\u23BF ",
1891
1983
  removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
1892
1984
  ] }) }),
1893
- shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { color: ln.sign === "+" ? "green" : ln.sign === "-" ? "red" : void 0, dimColor: ln.sign === " ", children: [
1894
- ln.sign,
1895
- " ",
1896
- ln.text
1897
- ] }) }, i)),
1985
+ shown.map((ln, i) => {
1986
+ const width = (process.stdout.columns ?? 80) - 6;
1987
+ const content = `${ln.sign} ${ln.text}`.padEnd(width);
1988
+ return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(
1989
+ Text9,
1990
+ {
1991
+ backgroundColor: ln.sign === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
1992
+ dimColor: ln.sign === " ",
1993
+ children: content
1994
+ }
1995
+ ) }, i);
1996
+ }),
1898
1997
  extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1899
1998
  "\u2026 ",
1900
1999
  extra,
1901
- " more lines"
2000
+ " more lines \xB7 ctrl+o to expand"
1902
2001
  ] }) })
1903
2002
  ] });
1904
2003
  }
@@ -1964,6 +2063,7 @@ function summarizeResult(res, toolName) {
1964
2063
  return extra > 0 ? `${head} (+${extra} lines)` : head;
1965
2064
  }
1966
2065
  function ToolResultBlock({ result, toolName }) {
2066
+ const expanded = useToolExpanded();
1967
2067
  const content = result.content ?? "";
1968
2068
  const lines = content.split("\n");
1969
2069
  const showMulti = (toolName === "run_bash" || toolName === "grep" || toolName === "glob" || result.is_error) && lines.length > 1;
@@ -1973,9 +2073,9 @@ function ToolResultBlock({ result, toolName }) {
1973
2073
  summarizeResult(result, toolName)
1974
2074
  ] }) });
1975
2075
  }
1976
- const MAX_LINES = 10;
1977
2076
  const MAX_LINE_WIDTH = 200;
1978
- const shown = lines.slice(0, MAX_LINES).map((l) => truncate(l, MAX_LINE_WIDTH));
2077
+ const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
2078
+ const shown = visible.map((l) => truncate(l, MAX_LINE_WIDTH));
1979
2079
  const extra = lines.length - shown.length;
1980
2080
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
1981
2081
  /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
@@ -1986,7 +2086,7 @@ function ToolResultBlock({ result, toolName }) {
1986
2086
  extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1987
2087
  "\u2026 ",
1988
2088
  extra,
1989
- " more lines"
2089
+ " more lines \xB7 ctrl+o to expand"
1990
2090
  ] }) })
1991
2091
  ] });
1992
2092
  }
@@ -2127,22 +2227,22 @@ function ChatView({
2127
2227
 
2128
2228
  // src/ui/hooks/useAgentRunner.ts
2129
2229
  init_loop();
2130
- import { useState as useState3, useRef } from "react";
2230
+ import { useState as useState4, useRef } from "react";
2131
2231
  var FLUSH_MS = 100;
2132
2232
  function useAgentRunner(model, activeCtx) {
2133
- const [messages, setMessages] = useState3([]);
2134
- const [thinking, setThinking] = useState3(false);
2135
- const [thinkingContent, setThinkingContent] = useState3("");
2136
- const [streaming, setStreaming] = useState3(false);
2137
- const [streamingContent, setStreamingContent] = useState3("");
2138
- const [error, setError] = useState3(null);
2139
- const [busy, setBusy] = useState3(false);
2140
- const [processingLabel, setProcessingLabel] = useState3(void 0);
2141
- const [agentHistory, setAgentHistory] = useState3([]);
2142
- const [pendingPermission, setPendingPermission] = useState3(null);
2143
- const [permissionCursor, setPermissionCursor] = useState3(0);
2144
- const [activeToolUses, setActiveToolUses] = useState3([]);
2145
- const [activeToolResults, setActiveToolResults] = useState3([]);
2233
+ const [messages, setMessages] = useState4([]);
2234
+ const [thinking, setThinking] = useState4(false);
2235
+ const [thinkingContent, setThinkingContent] = useState4("");
2236
+ const [streaming, setStreaming] = useState4(false);
2237
+ const [streamingContent, setStreamingContent] = useState4("");
2238
+ const [error, setError] = useState4(null);
2239
+ const [busy, setBusy] = useState4(false);
2240
+ const [processingLabel, setProcessingLabel] = useState4(void 0);
2241
+ const [agentHistory, setAgentHistory] = useState4([]);
2242
+ const [pendingPermission, setPendingPermission] = useState4(null);
2243
+ const [permissionCursor, setPermissionCursor] = useState4(0);
2244
+ const [activeToolUses, setActiveToolUses] = useState4([]);
2245
+ const [activeToolResults, setActiveToolResults] = useState4([]);
2146
2246
  const busyRef = useRef(false);
2147
2247
  const abortRef = useRef(null);
2148
2248
  const pendingPermissionRef = useRef(null);
@@ -2413,6 +2513,10 @@ function useKeyboard(opts) {
2413
2513
  toggleThinkingVisible();
2414
2514
  return;
2415
2515
  }
2516
+ if (key.ctrl && char === "o") {
2517
+ toggleToolExpanded();
2518
+ return;
2519
+ }
2416
2520
  if (key.escape && busyRef.current && abortRef.current) {
2417
2521
  abortRef.current.abort();
2418
2522
  return;
@@ -2638,30 +2742,30 @@ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-r
2638
2742
  function App() {
2639
2743
  const { exit } = useApp();
2640
2744
  const cwd = process.cwd().replace(homedir6(), "~").split(sep2).join("/");
2641
- const [cfg, setCfg] = useState4(loadConfig());
2642
- const [models, setModels] = useState4([]);
2643
- const [contexts, setContexts] = useState4({});
2644
- const [activeCtx, setActiveCtx] = useState4(null);
2645
- const [state, setState] = useState4("loading");
2646
- const [cursor, setCursor] = useState4(0);
2647
- const [updateAvailable, setUpdateAvailable] = useState4(null);
2648
- const [ollamaDown, setOllamaDown] = useState4(false);
2649
- const [sessionId, setSessionId] = useState4(() => newSessionId());
2650
- const [sessions, setSessions] = useState4([]);
2651
- const [notice, setNotice] = useState4(null);
2652
- const [input, setInput] = useState4("");
2653
- const [paletteCursor, setPaletteCursor] = useState4(0);
2654
- const [filePickerCursor, setFilePickerCursor] = useState4(0);
2745
+ const [cfg, setCfg] = useState5(loadConfig());
2746
+ const [models, setModels] = useState5([]);
2747
+ const [contexts, setContexts] = useState5({});
2748
+ const [activeCtx, setActiveCtx] = useState5(null);
2749
+ const [state, setState] = useState5("loading");
2750
+ const [cursor, setCursor] = useState5(0);
2751
+ const [updateAvailable, setUpdateAvailable] = useState5(null);
2752
+ const [ollamaDown, setOllamaDown] = useState5(false);
2753
+ const [sessionId, setSessionId] = useState5(() => newSessionId());
2754
+ const [sessions, setSessions] = useState5([]);
2755
+ const [notice, setNotice] = useState5(null);
2756
+ const [input, setInput] = useState5("");
2757
+ const [paletteCursor, setPaletteCursor] = useState5(0);
2758
+ const [filePickerCursor, setFilePickerCursor] = useState5(0);
2655
2759
  const agent = useAgentRunner(cfg.model, activeCtx);
2656
- useEffect3(() => {
2760
+ useEffect4(() => {
2657
2761
  checkForUpdate().then((v) => {
2658
2762
  if (v) setUpdateAvailable(v);
2659
2763
  });
2660
2764
  }, []);
2661
- useEffect3(() => {
2765
+ useEffect4(() => {
2662
2766
  if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
2663
2767
  }, [agent.agentHistory, sessionId]);
2664
- useEffect3(() => {
2768
+ useEffect4(() => {
2665
2769
  listModels().then((m) => {
2666
2770
  setModels(m);
2667
2771
  setState(cfg.model ? "ready" : "select-model");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Terminal AI coding agent powered by Ollama",
5
5
  "type": "module",
6
6
  "bin": {