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.
- package/README.md +123 -72
- package/dist/cli.js +151 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">miii</h1>
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Small. Simple. Smart. Strategic. Semantic.</strong>
|
|
5
|
+
</p>
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
21
|
+
---
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
## Contents
|
|
18
24
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
49
|
+
miii flips the script:
|
|
36
50
|
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
##
|
|
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 — [
|
|
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
|
-
###
|
|
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
|
-
##
|
|
101
|
+
## Interaction Guide
|
|
77
102
|
|
|
78
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
127
|
+
### Capabilities
|
|
120
128
|
|
|
121
|
-
miii ships with a built-in tool suite the agent
|
|
129
|
+
miii ships with a built-in tool suite that the agent invokes autonomously:
|
|
122
130
|
|
|
123
|
-
| Tool |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
163
|
+
miii doctor gemma4:e4b grep # check one model against "grep" scenarios
|
|
144
164
|
```
|
|
145
165
|
|
|
146
|
-
It
|
|
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
|
-
|
|
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
|
|
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
|
|
284
|
+
### Testing Local Changes
|
|
236
285
|
|
|
237
|
-
The global `miii` command points
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1876
|
-
const shown = previewLines.slice(0,
|
|
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) =>
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
|
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
|
|
2232
|
+
import { useState as useState4, useRef } from "react";
|
|
2131
2233
|
var FLUSH_MS = 100;
|
|
2132
2234
|
function useAgentRunner(model, activeCtx) {
|
|
2133
|
-
const [messages, setMessages] =
|
|
2134
|
-
const [thinking, setThinking] =
|
|
2135
|
-
const [thinkingContent, setThinkingContent] =
|
|
2136
|
-
const [streaming, setStreaming] =
|
|
2137
|
-
const [streamingContent, setStreamingContent] =
|
|
2138
|
-
const [error, setError] =
|
|
2139
|
-
const [busy, setBusy] =
|
|
2140
|
-
const [processingLabel, setProcessingLabel] =
|
|
2141
|
-
const [agentHistory, setAgentHistory] =
|
|
2142
|
-
const [pendingPermission, setPendingPermission] =
|
|
2143
|
-
const [permissionCursor, setPermissionCursor] =
|
|
2144
|
-
const [activeToolUses, setActiveToolUses] =
|
|
2145
|
-
const [activeToolResults, setActiveToolResults] =
|
|
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] =
|
|
2642
|
-
const [models, setModels] =
|
|
2643
|
-
const [contexts, setContexts] =
|
|
2644
|
-
const [activeCtx, setActiveCtx] =
|
|
2645
|
-
const [state, setState] =
|
|
2646
|
-
const [cursor, setCursor] =
|
|
2647
|
-
const [updateAvailable, setUpdateAvailable] =
|
|
2648
|
-
const [ollamaDown, setOllamaDown] =
|
|
2649
|
-
const [sessionId, setSessionId] =
|
|
2650
|
-
const [sessions, setSessions] =
|
|
2651
|
-
const [notice, setNotice] =
|
|
2652
|
-
const [input, setInput] =
|
|
2653
|
-
const [paletteCursor, setPaletteCursor] =
|
|
2654
|
-
const [filePickerCursor, setFilePickerCursor] =
|
|
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
|
-
|
|
2762
|
+
useEffect4(() => {
|
|
2657
2763
|
checkForUpdate().then((v) => {
|
|
2658
2764
|
if (v) setUpdateAvailable(v);
|
|
2659
2765
|
});
|
|
2660
2766
|
}, []);
|
|
2661
|
-
|
|
2767
|
+
useEffect4(() => {
|
|
2662
2768
|
if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
|
|
2663
2769
|
}, [agent.agentHistory, sessionId]);
|
|
2664
|
-
|
|
2770
|
+
useEffect4(() => {
|
|
2665
2771
|
listModels().then((m) => {
|
|
2666
2772
|
setModels(m);
|
|
2667
2773
|
setState(cfg.model ? "ready" : "select-model");
|