miii-agent 0.1.11 → 0.1.13

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 +123 -72
  2. package/dist/cli.js +151 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,26 +1,38 @@
1
- # miii
1
+ <h1 align="center">miii</h1>
2
2
 
3
- > small · simple · smart · strategic · semantic
4
- >
5
- > Your code never leaves your machine. No API keys. No cloud. No bullshit.
3
+ <p align="center">
4
+ <strong>Small. Simple. Smart. Strategic. Semantic.</strong>
5
+ </p>
6
6
 
7
- **miii** is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com), it reads your code, writes features, runs tests, and fixes bugs — entirely on your hardware, at native speed.
7
+ <p align="center">
8
+ A local-first AI coding agent that lives in your terminal.<br>
9
+ Your code never leaves your machine. No API keys. No cloud. No bullshit.
10
+ </p>
8
11
 
9
- [![npm](https://img.shields.io/npm/v/miii-agent)](https://www.npmjs.com/package/miii-agent)
10
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
- [![node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/miii-agent"><img src="https://img.shields.io/npm/v/miii-agent" alt="npm version"></a>
14
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license"></a>
15
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="node version"></a>
16
+ <a href="https://ollama.com"><img src="https://img.shields.io/badge/powered%20by-Ollama-black" alt="powered by Ollama"></a>
17
+ </p>
12
18
 
13
- ---
19
+ miii is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com), it reads your code, writes features, runs tests, and fixes bugs — entirely on your hardware, at native speed.
14
20
 
15
- ## The name
21
+ ---
16
22
 
17
- **miii** stands for five principles it's built around:
23
+ ## Contents
18
24
 
19
- - **small** — tight codebase, no bloat. You can read the whole thing.
20
- - **simple** no API keys, no accounts, no config ceremony. Just run it.
21
- - **smart** — decomposes problems and verifies its own work like an engineer.
22
- - **strategic** — plans before it acts; tools are gated, paths are confined.
23
- - **semantic** — works from the meaning of your code, not blind text matching.
25
+ - [Demo](#demo)
26
+ - [The Local-First Advantage](#the-local-first-advantage)
27
+ - [Core Philosophy](#core-philosophy)
28
+ - [Quick Start](#quick-start)
29
+ - [Interaction Guide](#interaction-guide)
30
+ - [Technical Deep Dive](#technical-deep-dive)
31
+ - [Configuration](#configuration)
32
+ - [System Architecture](#system-architecture)
33
+ - [Development](#development)
34
+ - [Project Status](#project-status)
35
+ - [License](#license)
24
36
 
25
37
  ---
26
38
 
@@ -30,25 +42,45 @@
30
42
 
31
43
  ---
32
44
 
33
- ## Why miii?
45
+ ## The Local-First Advantage
46
+
47
+ Most AI coding tools are wrappers around cloud APIs. They are slow, expensive, and require you to trust your private codebase to a third-party server.
34
48
 
35
- Most AI coding tools are wrappers around cloud APIs. They're slow, expensive, and send your private code to someone else's server.
49
+ miii flips the script:
36
50
 
37
- miii is different:
51
+ - **Absolute Privacy** — Powered by Ollama. Your code stays on your disk, period.
52
+ - **Zero Friction** — No API keys, no billing, no accounts. Just `miii`.
53
+ - **True Agency** — miii doesn't just chat; it decomposes problems, invokes tools, and verifies results like a senior engineer.
54
+ - **Native Performance** — No network round-trips. Latency is limited by your GPU, not a CDN.
38
55
 
39
- - **Local-first** Powered by Ollama. Your code stays on your disk, period.
40
- - **Zero ceremony** — No API keys. No billing. No accounts. Just `miii`.
41
- - **Actually agentic** miii doesn't just chat. It decomposes problems, calls tools, and verifies results like an engineer would.
42
- - **Fast** — No network round-trips. Response time is limited by your GPU, not a CDN.
56
+ | | Cloud AI agents | **miii** |
57
+ |----------------|--------------------------|------------------------------|
58
+ | Your code | Sent to a third party | Never leaves your machine |
59
+ | Cost | Per-token billing | Free runs on your hardware |
60
+ | Setup | API keys, accounts | `npm i -g miii-agent` |
61
+ | Offline | No | Yes |
62
+ | Latency | Network + queue | Your GPU only |
43
63
 
44
64
  ---
45
65
 
46
- ## Installation
66
+ ## Core Philosophy
67
+
68
+ miii is built on five foundational principles:
69
+
70
+ - **small** — A tight, bloat-free codebase. You can read the entire project in an afternoon.
71
+ - **simple** — No configuration ceremony. Install and run.
72
+ - **smart** — Decomposes complex tasks and verifies its own work.
73
+ - **strategic** — Plans before acting; tools are gated and paths are confined.
74
+ - **semantic** — Operates on the meaning of your code, not blind text matching.
75
+
76
+ ---
77
+
78
+ ## Quick Start
47
79
 
48
80
  ### Prerequisites
49
81
 
50
82
  - **Node.js** ≥ 18
51
- - **Ollama** running locally — [install here](https://ollama.com/download)
83
+ - **Ollama** running locally — [Download here](https://ollama.com/download)
52
84
  - A coding model pulled locally:
53
85
 
54
86
  ```bash
@@ -57,25 +89,18 @@ ollama pull qwen2.5-coder:14b
57
89
  ollama pull deepseek-coder-v2
58
90
  ```
59
91
 
60
- ### Install miii
92
+ ### Installation & Launch
61
93
 
62
94
  ```bash
63
95
  npm install -g miii-agent
64
- ```
65
-
66
- ### Launch
67
-
68
- ```bash
69
96
  miii
70
97
  ```
71
98
 
72
- That's it.
73
-
74
99
  ---
75
100
 
76
- ## Usage
101
+ ## Interaction Guide
77
102
 
78
- Once inside the TUI, just type naturally:
103
+ Inside the TUI, interact naturally:
79
104
 
80
105
  ```
81
106
  > refactor the auth module to use async/await
@@ -92,58 +117,53 @@ Once inside the TUI, just type naturally:
92
117
  | `/models` | Switch active Ollama model |
93
118
  | `/clear` | Reset conversation history |
94
119
  | `Esc` | Stop current generation or tool run |
120
+ | `Ctrl+O` | Toggle full tool output view |
95
121
  | `Ctrl+C` | Quit |
96
122
 
97
123
  ---
98
124
 
99
- ## Configuration
100
-
101
- Settings live in `~/.miii/config.json` and are created on first run.
102
-
103
- ```json
104
- {
105
- "model": "qwen2.5-coder:14b",
106
- "ollamaHost": "http://localhost:11434",
107
- "effort": "medium"
108
- }
109
- ```
110
-
111
- | Field | Description | Values |
112
- |-------|-------------|--------|
113
- | `model` | Default Ollama model | any `ollama list` model |
114
- | `ollamaHost` | Ollama API endpoint | URL string |
115
- | `effort` | Controls temperature & limits | `low` \| `medium` \| `high` |
116
-
117
- ---
125
+ ## Technical Deep Dive
118
126
 
119
- ## Capabilities
127
+ ### Capabilities
120
128
 
121
- miii ships with a built-in tool suite the agent can invoke autonomously:
129
+ miii ships with a built-in tool suite that the agent invokes autonomously:
122
130
 
123
- | Tool | What it does |
124
- |------|-------------|
131
+ | Tool | Function |
132
+ |------|----------|
125
133
  | `read_file` | Read any file in your workspace |
126
134
  | `write_file` | Create new files |
127
- | `edit_file` | Precise string-level edits (no rewrites) |
135
+ | `edit_file` | Precise string-level edits with whitespace tolerance (no rewrites) |
128
136
  | `glob` | Pattern-match files across the project |
129
137
  | `grep` | Regex search across files |
130
138
  | `run_bash` | Execute shell commands |
131
139
 
132
- Every sensitive operation is gated by a permission system you approve what the agent can touch, and "always" approvals persist to `~/.miii/permissions.json` so you're never asked twice. File tools are confined to your working directory; `../` traversal and absolute paths outside it are rejected.
140
+ **Security & Safety:** Every sensitive operation is gated by a permission system. You approve what the agent can touch, and "always" approvals persist to `~/.miii/permissions.json`. File tools are strictly confined to your working directory; `../` traversal and absolute paths outside the workspace are rejected.
133
141
 
134
- ---
142
+ ### Lossless Output Spill
135
143
 
136
- ## Checking your setup
144
+ Big tool outputs (like 50K-line test logs) usually get truncated, leaving the model to guess. miii doesn't truncate; it **spills**.
145
+
146
+ 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, followed by a pointer:
147
+
148
+ ```
149
+ [command output truncated: 5184 lines / 412900 bytes.
150
+ Full output at ~/.miii/output/9f3a1c.txt — read it with
151
+ read_file offset/limit to see the elided middle.]
152
+ ```
137
153
 
138
- 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.
154
+ If the model needs the elided middle, it pages through it using `read_file` ranged reads. Nothing is ever lost. Spill files are garbage-collected after 24 hours.
155
+
156
+ ### The Model Doctor
157
+
158
+ Not every local model can drive an agent. A model that cannot emit clean tool calls will simply chat at you instead of editing files. `miii doctor` validates your installed models against concrete engineering tasks.
139
159
 
140
160
  ```bash
141
161
  miii doctor # check every local model (from `ollama list`)
142
162
  miii doctor qwen2.5-coder:7b # check one model
143
- miii doctor gemma4:e4b grep # one model, only scenarios matching "grep"
163
+ miii doctor gemma4:e4b grep # check one model against "grep" scenarios
144
164
  ```
145
165
 
146
- It runs the real agent against a handful of concrete tasks (edit a file, read-and-answer, create a file, locate a definition) and checks the *outcome* — did the file actually change, was the answer right — then prints a verdict per model:
166
+ It verifies outcomes (did the file actually change?) and prints a verdict:
147
167
 
148
168
  ```
149
169
  === qwen3-coder ===
@@ -157,11 +177,29 @@ PASS grep-locate ...
157
177
  → gemma4:e4b: 1/4 — not recommended — weak tool-calling
158
178
  ```
159
179
 
160
- With more than one model it also prints a compatibility matrix (`+` pass, `.` fail). Cloud models are skipped by default; name one explicitly to include it. If a model comes back `marginal` or `not recommended`, pull a stronger coding model and try again.
180
+ ---
181
+
182
+ ## Configuration
183
+
184
+ Settings live in `~/.miii/config.json` and are created on first run.
185
+
186
+ ```json
187
+ {
188
+ "model": "qwen2.5-coder:14b",
189
+ "ollamaHost": "http://localhost:11434",
190
+ "effort": "medium"
191
+ }
192
+ ```
193
+
194
+ | Field | Description | Values |
195
+ |-------|-------------|--------|
196
+ | `model` | Default Ollama model | any `ollama list` model |
197
+ | `ollamaHost` | Ollama API endpoint | URL string |
198
+ | `effort` | Controls temperature & limits | `low` \| `medium` \| `high` |
161
199
 
162
200
  ---
163
201
 
164
- ## Architecture
202
+ ## System Architecture
165
203
 
166
204
  ```mermaid
167
205
  graph TD
@@ -195,6 +233,10 @@ graph TD
195
233
  ReadFile -.-> Confine["Path Confinement\n(tools/paths.ts)"]
196
234
  WriteFile -.-> Confine
197
235
  EditFile -.-> Confine
236
+ RunBash -->|"large output"| SpillMod["Output Spill\n(tools/spill.ts)"]
237
+ Grep -->|"large output"| SpillMod
238
+ Glob -->|"large output"| SpillMod
239
+ SpillMod -.->|"head+tail preview\n+ read pointer"| ToolRegistry
198
240
  end
199
241
 
200
242
  Adapter -->|"HTTP streaming"| Ollama["Ollama\n(local LLM server)"]
@@ -206,16 +248,21 @@ graph TD
206
248
  subgraph Storage ["Local Storage"]
207
249
  Config["~/.miii/config.json\n(model, host, effort)"]
208
250
  Rules["~/.miii/permissions.json\n(saved allow rules)"]
251
+ Spill["~/.miii/output/\n(spilled tool output)"]
209
252
  end
210
253
 
211
254
  App -.->|"reads"| Config
212
255
  Policy -.->|"reads / persists 'always'"| Rules
256
+ SpillMod -.->|"writes full output"| Spill
257
+ ReadFile -.->|"pages elided middle\n(offset/limit)"| Spill
213
258
  ```
214
259
 
215
260
  ---
216
261
 
217
262
  ## Development
218
263
 
264
+ ### Setup
265
+
219
266
  ```bash
220
267
  git clone https://github.com/maruakshay/miii-cli.git
221
268
  cd miii-cli
@@ -223,6 +270,8 @@ npm install
223
270
  npm run dev
224
271
  ```
225
272
 
273
+ ### Build & Test
274
+
226
275
  ```bash
227
276
  npm run build # production build
228
277
  npm run start # run built output
@@ -230,11 +279,11 @@ npm run typecheck # type-check src + eval
230
279
  npm run eval # run the eval harness as a CI / regression gate
231
280
  ```
232
281
 
233
- The eval harness lives in `eval/` and powers `miii doctor`. As `npm run eval` it doubles as a regression gate it exits non-zero if any model fails any scenario, so a prompt or tool change that regresses a baseline model is caught in CI. Same engine, two doors: `miii doctor` for users checking their setup, `npm run eval` for maintainers gating changes.
282
+ The eval harness in `eval/` powers `miii doctor` and serves as a regression gate. If `npm run eval` exits non-zero, a prompt or tool change has regressed a baseline model.
234
283
 
235
- ### Testing the `miii` command against your local changes
284
+ ### Testing Local Changes
236
285
 
237
- The global `miii` command points at whatever was last installed with `npm install -g miii-agent` **not** your working tree. After editing source, the global binary is stale, so `miii` (and `miii doctor`) will run the old code and may appear to ignore your changes (e.g. printing the wrong model). Two ways to run your local build:
286
+ The global `miii` command points to the last installed version of `miii-agent`. To run your local working tree:
238
287
 
239
288
  ```bash
240
289
  node dist/cli.js doctor <model> # run the freshly built output directly
@@ -242,11 +291,13 @@ node dist/cli.js doctor <model> # run the freshly built output directly
242
291
  npm run build && npm link # point the global `miii` at this repo
243
292
  ```
244
293
 
245
- `npm link` symlinks the global `miii` to `dist/cli.js` in this repo, so each `npm run build` is picked up automatically. Restore the published version later with `npm install -g miii-agent`. Note: `npm run dev` / `npm run start` always run the current source and never have this staleness problem.
294
+ `npm link` symlinks the global `miii` to `dist/cli.js` in this repo. Restore the published version later with `npm install -g miii-agent`.
295
+
296
+ ---
246
297
 
247
298
  ## Project Status
248
299
 
249
- MVP. Core agent loop works. Actively refining tool execution, streaming, and the permission model. PRs welcome — fork it, break it, improve it.
300
+ **MVP.** Core agent loop is stable. Actively refining tool execution, streaming, and the permission model. PRs are welcome — fork it, break it, improve it.
250
301
 
251
302
  ---
252
303
 
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,24 @@ 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 - 20;
1987
+ const raw = `${ln.sign} ${ln.text}`;
1988
+ const content = raw.length > width ? raw.slice(0, width) : raw.padEnd(width);
1989
+ return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(
1990
+ Text9,
1991
+ {
1992
+ wrap: "truncate",
1993
+ backgroundColor: ln.sign === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
1994
+ dimColor: ln.sign === " ",
1995
+ children: content
1996
+ }
1997
+ ) }, i);
1998
+ }),
1898
1999
  extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1899
2000
  "\u2026 ",
1900
2001
  extra,
1901
- " more lines"
2002
+ " more lines \xB7 ctrl+o to expand"
1902
2003
  ] }) })
1903
2004
  ] });
1904
2005
  }
@@ -1964,6 +2065,7 @@ function summarizeResult(res, toolName) {
1964
2065
  return extra > 0 ? `${head} (+${extra} lines)` : head;
1965
2066
  }
1966
2067
  function ToolResultBlock({ result, toolName }) {
2068
+ const expanded = useToolExpanded();
1967
2069
  const content = result.content ?? "";
1968
2070
  const lines = content.split("\n");
1969
2071
  const showMulti = (toolName === "run_bash" || toolName === "grep" || toolName === "glob" || result.is_error) && lines.length > 1;
@@ -1973,9 +2075,9 @@ function ToolResultBlock({ result, toolName }) {
1973
2075
  summarizeResult(result, toolName)
1974
2076
  ] }) });
1975
2077
  }
1976
- const MAX_LINES = 10;
1977
2078
  const MAX_LINE_WIDTH = 200;
1978
- const shown = lines.slice(0, MAX_LINES).map((l) => truncate(l, MAX_LINE_WIDTH));
2079
+ const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
2080
+ const shown = visible.map((l) => truncate(l, MAX_LINE_WIDTH));
1979
2081
  const extra = lines.length - shown.length;
1980
2082
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
1981
2083
  /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
@@ -1986,7 +2088,7 @@ function ToolResultBlock({ result, toolName }) {
1986
2088
  extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1987
2089
  "\u2026 ",
1988
2090
  extra,
1989
- " more lines"
2091
+ " more lines \xB7 ctrl+o to expand"
1990
2092
  ] }) })
1991
2093
  ] });
1992
2094
  }
@@ -2127,22 +2229,22 @@ function ChatView({
2127
2229
 
2128
2230
  // src/ui/hooks/useAgentRunner.ts
2129
2231
  init_loop();
2130
- import { useState as useState3, useRef } from "react";
2232
+ import { useState as useState4, useRef } from "react";
2131
2233
  var FLUSH_MS = 100;
2132
2234
  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([]);
2235
+ const [messages, setMessages] = useState4([]);
2236
+ const [thinking, setThinking] = useState4(false);
2237
+ const [thinkingContent, setThinkingContent] = useState4("");
2238
+ const [streaming, setStreaming] = useState4(false);
2239
+ const [streamingContent, setStreamingContent] = useState4("");
2240
+ const [error, setError] = useState4(null);
2241
+ const [busy, setBusy] = useState4(false);
2242
+ const [processingLabel, setProcessingLabel] = useState4(void 0);
2243
+ const [agentHistory, setAgentHistory] = useState4([]);
2244
+ const [pendingPermission, setPendingPermission] = useState4(null);
2245
+ const [permissionCursor, setPermissionCursor] = useState4(0);
2246
+ const [activeToolUses, setActiveToolUses] = useState4([]);
2247
+ const [activeToolResults, setActiveToolResults] = useState4([]);
2146
2248
  const busyRef = useRef(false);
2147
2249
  const abortRef = useRef(null);
2148
2250
  const pendingPermissionRef = useRef(null);
@@ -2413,6 +2515,10 @@ function useKeyboard(opts) {
2413
2515
  toggleThinkingVisible();
2414
2516
  return;
2415
2517
  }
2518
+ if (key.ctrl && char === "o") {
2519
+ toggleToolExpanded();
2520
+ return;
2521
+ }
2416
2522
  if (key.escape && busyRef.current && abortRef.current) {
2417
2523
  abortRef.current.abort();
2418
2524
  return;
@@ -2638,30 +2744,30 @@ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-r
2638
2744
  function App() {
2639
2745
  const { exit } = useApp();
2640
2746
  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);
2747
+ const [cfg, setCfg] = useState5(loadConfig());
2748
+ const [models, setModels] = useState5([]);
2749
+ const [contexts, setContexts] = useState5({});
2750
+ const [activeCtx, setActiveCtx] = useState5(null);
2751
+ const [state, setState] = useState5("loading");
2752
+ const [cursor, setCursor] = useState5(0);
2753
+ const [updateAvailable, setUpdateAvailable] = useState5(null);
2754
+ const [ollamaDown, setOllamaDown] = useState5(false);
2755
+ const [sessionId, setSessionId] = useState5(() => newSessionId());
2756
+ const [sessions, setSessions] = useState5([]);
2757
+ const [notice, setNotice] = useState5(null);
2758
+ const [input, setInput] = useState5("");
2759
+ const [paletteCursor, setPaletteCursor] = useState5(0);
2760
+ const [filePickerCursor, setFilePickerCursor] = useState5(0);
2655
2761
  const agent = useAgentRunner(cfg.model, activeCtx);
2656
- useEffect3(() => {
2762
+ useEffect4(() => {
2657
2763
  checkForUpdate().then((v) => {
2658
2764
  if (v) setUpdateAvailable(v);
2659
2765
  });
2660
2766
  }, []);
2661
- useEffect3(() => {
2767
+ useEffect4(() => {
2662
2768
  if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
2663
2769
  }, [agent.agentHistory, sessionId]);
2664
- useEffect3(() => {
2770
+ useEffect4(() => {
2665
2771
  listModels().then((m) => {
2666
2772
  setModels(m);
2667
2773
  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.13",
4
4
  "description": "Terminal AI coding agent powered by Ollama",
5
5
  "type": "module",
6
6
  "bin": {