miii-agent 0.1.10 → 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.
- package/README.md +27 -1
- package/dist/cli.js +346 -100
- 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
|
@@ -145,57 +145,176 @@ var init_client = __esm({
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
// src/tools/paths.ts
|
|
148
|
-
import { resolve, relative as relative2, isAbsolute, sep } from "path";
|
|
148
|
+
import { resolve, relative as relative2, isAbsolute, sep, join as join4 } from "path";
|
|
149
|
+
import { homedir as homedir3 } from "os";
|
|
150
|
+
function isUnder(parent, child) {
|
|
151
|
+
const rel = relative2(parent, child);
|
|
152
|
+
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute(rel);
|
|
153
|
+
}
|
|
149
154
|
function confinePath(p) {
|
|
150
155
|
if (typeof p !== "string" || p.length === 0) {
|
|
151
156
|
throw new Error("Path is required.");
|
|
152
157
|
}
|
|
153
158
|
const root = process.cwd();
|
|
154
159
|
const abs = resolve(root, p);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
|
|
160
|
+
if (isUnder(root, abs) || isUnder(SPILL_DIR, abs)) {
|
|
161
|
+
return abs;
|
|
158
162
|
}
|
|
159
|
-
|
|
163
|
+
throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
|
|
160
164
|
}
|
|
165
|
+
var SPILL_DIR;
|
|
161
166
|
var init_paths = __esm({
|
|
162
167
|
"src/tools/paths.ts"() {
|
|
163
168
|
"use strict";
|
|
169
|
+
SPILL_DIR = resolve(join4(homedir3(), ".miii", "output"));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
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";
|
|
164
203
|
}
|
|
165
204
|
});
|
|
166
205
|
|
|
167
206
|
// src/tools/edit_file.ts
|
|
168
207
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
208
|
+
function similarity(a, b) {
|
|
209
|
+
const x = a.trim();
|
|
210
|
+
const y = b.trim();
|
|
211
|
+
if (!x && !y) return 1;
|
|
212
|
+
const len = Math.max(x.length, y.length);
|
|
213
|
+
if (len === 0) return 0;
|
|
214
|
+
let same = 0;
|
|
215
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] === y[i]) same++;
|
|
216
|
+
return same / len;
|
|
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
|
+
}
|
|
247
|
+
function nearMiss(src, old_str) {
|
|
248
|
+
const srcLines = src.split("\n");
|
|
249
|
+
const needle = old_str.split("\n").find((l) => l.trim()) ?? old_str;
|
|
250
|
+
let bestIdx = -1;
|
|
251
|
+
let bestScore = 0;
|
|
252
|
+
for (let i = 0; i < srcLines.length; i++) {
|
|
253
|
+
const s = similarity(srcLines[i], needle);
|
|
254
|
+
if (s > bestScore) {
|
|
255
|
+
bestScore = s;
|
|
256
|
+
bestIdx = i;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (bestIdx === -1 || bestScore < 0.4) return "";
|
|
260
|
+
const from = Math.max(0, bestIdx - 3);
|
|
261
|
+
const to = Math.min(srcLines.length, bestIdx + 4);
|
|
262
|
+
const width = String(to).length;
|
|
263
|
+
const ctx = srcLines.slice(from, to).map((l, i) => `${String(from + i + 1).padStart(width, " ")} ${l}`).join("\n");
|
|
264
|
+
return `
|
|
265
|
+
Closest text in file (lines ${from + 1}-${to}):
|
|
266
|
+
${ctx}`;
|
|
267
|
+
}
|
|
169
268
|
var edit_file;
|
|
170
269
|
var init_edit_file = __esm({
|
|
171
270
|
"src/tools/edit_file.ts"() {
|
|
172
271
|
"use strict";
|
|
173
272
|
init_paths();
|
|
273
|
+
init_verifyHint();
|
|
174
274
|
edit_file = {
|
|
175
275
|
name: "edit_file",
|
|
176
|
-
description: "Replace an exact string in a file. old_str must be unique.",
|
|
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.",
|
|
177
277
|
input_schema: {
|
|
178
278
|
type: "object",
|
|
179
279
|
properties: {
|
|
180
280
|
path: { type: "string", description: "File path" },
|
|
181
|
-
old_str: { type: "string", description: "Exact text to replace" },
|
|
182
|
-
new_str: { type: "string", description: "Replacement text" }
|
|
281
|
+
old_str: { type: "string", description: "Exact text to replace (whitespace-sensitive)" },
|
|
282
|
+
new_str: { type: "string", description: "Replacement text" },
|
|
283
|
+
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring uniqueness" }
|
|
183
284
|
},
|
|
184
285
|
required: ["path", "old_str", "new_str"]
|
|
185
286
|
},
|
|
186
|
-
handler: ({ path, old_str, new_str }) => {
|
|
287
|
+
handler: ({ path, old_str, new_str, replace_all }) => {
|
|
187
288
|
try {
|
|
289
|
+
if (old_str === new_str) {
|
|
290
|
+
return { content: `old_str and new_str are identical \u2014 nothing to change in ${path}.`, is_error: true };
|
|
291
|
+
}
|
|
188
292
|
const abs = confinePath(path);
|
|
189
293
|
const src = readFileSync3(abs, "utf-8");
|
|
190
294
|
const first = src.indexOf(old_str);
|
|
191
295
|
if (first === -1) {
|
|
192
|
-
|
|
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
|
+
}
|
|
305
|
+
return { content: `old_str not found in ${path}.${nearMiss(src, old_str)}`, is_error: true };
|
|
193
306
|
}
|
|
194
|
-
|
|
195
|
-
|
|
307
|
+
const all = replace_all === true;
|
|
308
|
+
if (!all && src.indexOf(old_str, first + 1) !== -1) {
|
|
309
|
+
return {
|
|
310
|
+
content: `old_str not unique in ${path}. Add surrounding context to disambiguate, or set replace_all.`,
|
|
311
|
+
is_error: true
|
|
312
|
+
};
|
|
196
313
|
}
|
|
197
|
-
|
|
198
|
-
|
|
314
|
+
const out = all ? src.split(old_str).join(new_str) : src.slice(0, first) + new_str + src.slice(first + old_str.length);
|
|
315
|
+
const n = all ? src.split(old_str).length - 1 : 1;
|
|
316
|
+
writeFileSync3(abs, out, "utf-8");
|
|
317
|
+
return { content: `Edited ${path}${all ? ` (${n} occurrences)` : ""}.${verifyHint(path)}` };
|
|
199
318
|
} catch (err) {
|
|
200
319
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
201
320
|
}
|
|
@@ -206,6 +325,10 @@ var init_edit_file = __esm({
|
|
|
206
325
|
|
|
207
326
|
// src/tools/read_file.ts
|
|
208
327
|
import { readFileSync as readFileSync4 } from "fs";
|
|
328
|
+
function numbered(lines, start) {
|
|
329
|
+
const width = String(start + lines.length - 1).length;
|
|
330
|
+
return lines.map((l, i) => `${String(start + i).padStart(width, " ")} ${l}`).join("\n");
|
|
331
|
+
}
|
|
209
332
|
var read_file;
|
|
210
333
|
var init_read_file = __esm({
|
|
211
334
|
"src/tools/read_file.ts"() {
|
|
@@ -213,21 +336,40 @@ var init_read_file = __esm({
|
|
|
213
336
|
init_paths();
|
|
214
337
|
read_file = {
|
|
215
338
|
name: "read_file",
|
|
216
|
-
description: "Read
|
|
339
|
+
description: "Read file contents as UTF-8 text with line numbers. Use offset/limit to read a range of a large file instead of the whole thing.",
|
|
217
340
|
input_schema: {
|
|
218
341
|
type: "object",
|
|
219
342
|
properties: {
|
|
220
|
-
path: { type: "string", description: "File path" }
|
|
343
|
+
path: { type: "string", description: "File path" },
|
|
344
|
+
offset: { type: "number", description: "1-based line to start from (default 1)" },
|
|
345
|
+
limit: { type: "number", description: "Max lines to return (default all / capped)" }
|
|
221
346
|
},
|
|
222
347
|
required: ["path"]
|
|
223
348
|
},
|
|
224
|
-
handler: ({ path }) => {
|
|
349
|
+
handler: ({ path, offset, limit }) => {
|
|
225
350
|
try {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
351
|
+
const MAX_CHARS = 2e5;
|
|
352
|
+
const buf = readFileSync4(confinePath(path));
|
|
353
|
+
if (buf.subarray(0, 8e3).includes(0)) {
|
|
354
|
+
return { content: `${path} looks binary (${buf.length} bytes); not reading as text.`, is_error: true };
|
|
355
|
+
}
|
|
356
|
+
const raw = buf.toString("utf-8").replace(/\r\n/g, "\n");
|
|
357
|
+
const allLines = raw.split("\n");
|
|
358
|
+
const total = allLines.length;
|
|
359
|
+
const start = Math.max(1, Math.floor(offset ?? 1));
|
|
360
|
+
const ranged = offset != null || limit != null;
|
|
361
|
+
const count = limit != null ? Math.max(0, Math.floor(limit)) : total;
|
|
362
|
+
const slice = allLines.slice(start - 1, start - 1 + count);
|
|
363
|
+
let body = numbered(slice, start);
|
|
364
|
+
if (body.length > MAX_CHARS) {
|
|
365
|
+
body = body.slice(0, MAX_CHARS) + `
|
|
366
|
+
[truncated: output exceeded ${MAX_CHARS} chars \u2014 use offset/limit]`;
|
|
367
|
+
}
|
|
368
|
+
if (ranged) {
|
|
369
|
+
const end = start - 1 + slice.length;
|
|
370
|
+
body += `
|
|
371
|
+
[showing lines ${start}-${end} of ${total}]`;
|
|
372
|
+
}
|
|
231
373
|
return { content: body };
|
|
232
374
|
} catch (err) {
|
|
233
375
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
@@ -245,6 +387,7 @@ var init_write_file = __esm({
|
|
|
245
387
|
"src/tools/write_file.ts"() {
|
|
246
388
|
"use strict";
|
|
247
389
|
init_paths();
|
|
390
|
+
init_verifyHint();
|
|
248
391
|
write_file = {
|
|
249
392
|
name: "write_file",
|
|
250
393
|
description: "Create or overwrite a file with the given content. Parent dirs auto-created.",
|
|
@@ -261,7 +404,7 @@ var init_write_file = __esm({
|
|
|
261
404
|
const abs = confinePath(path);
|
|
262
405
|
mkdirSync3(dirname(abs), { recursive: true });
|
|
263
406
|
writeFileSync4(abs, content, "utf-8");
|
|
264
|
-
return { content: `Wrote ${path} (${content.length} bytes)` };
|
|
407
|
+
return { content: `Wrote ${path} (${content.length} bytes).${verifyHint(path)}` };
|
|
265
408
|
} catch (err) {
|
|
266
409
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
267
410
|
}
|
|
@@ -270,12 +413,63 @@ var init_write_file = __esm({
|
|
|
270
413
|
}
|
|
271
414
|
});
|
|
272
415
|
|
|
416
|
+
// src/tools/spill.ts
|
|
417
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, rmSync as rmSync2, readdirSync as readdirSync3, statSync } from "fs";
|
|
418
|
+
import { join as join5 } from "path";
|
|
419
|
+
import { homedir as homedir4 } from "os";
|
|
420
|
+
import { randomBytes } from "crypto";
|
|
421
|
+
function ensureDir() {
|
|
422
|
+
mkdirSync4(OUTPUT_DIR, { recursive: true });
|
|
423
|
+
return OUTPUT_DIR;
|
|
424
|
+
}
|
|
425
|
+
function spillIfLarge(full, label = "output", budget = INLINE_BUDGET) {
|
|
426
|
+
if (full.length <= budget) return full;
|
|
427
|
+
const id = randomBytes(6).toString("hex");
|
|
428
|
+
const file = join5(ensureDir(), `${id}.txt`);
|
|
429
|
+
let path = file;
|
|
430
|
+
try {
|
|
431
|
+
writeFileSync5(file, full, "utf-8");
|
|
432
|
+
} catch {
|
|
433
|
+
path = "";
|
|
434
|
+
}
|
|
435
|
+
const head = Math.floor(budget * HEAD_FRACTION);
|
|
436
|
+
const tail = budget - head;
|
|
437
|
+
const totalLines = full.split("\n").length;
|
|
438
|
+
const preview = full.slice(0, head) + "\n\u2026\n" + full.slice(-tail);
|
|
439
|
+
const notice = path ? `[${label} truncated: ${totalLines} lines / ${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.]`;
|
|
440
|
+
return `${preview}
|
|
441
|
+
${notice}`;
|
|
442
|
+
}
|
|
443
|
+
function cleanupSpill(maxAgeMs = 24 * 60 * 60 * 1e3) {
|
|
444
|
+
try {
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
for (const name of readdirSync3(OUTPUT_DIR)) {
|
|
447
|
+
const f = join5(OUTPUT_DIR, name);
|
|
448
|
+
try {
|
|
449
|
+
if (now - statSync(f).mtimeMs > maxAgeMs) rmSync2(f, { force: true });
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
var OUTPUT_DIR, INLINE_BUDGET, HEAD_FRACTION;
|
|
457
|
+
var init_spill = __esm({
|
|
458
|
+
"src/tools/spill.ts"() {
|
|
459
|
+
"use strict";
|
|
460
|
+
OUTPUT_DIR = join5(homedir4(), ".miii", "output");
|
|
461
|
+
INLINE_BUDGET = 1e4;
|
|
462
|
+
HEAD_FRACTION = 0.3;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
273
466
|
// src/tools/run_bash.ts
|
|
274
467
|
import { execa } from "execa";
|
|
275
468
|
var run_bash;
|
|
276
469
|
var init_run_bash = __esm({
|
|
277
470
|
"src/tools/run_bash.ts"() {
|
|
278
471
|
"use strict";
|
|
472
|
+
init_spill();
|
|
279
473
|
run_bash = {
|
|
280
474
|
name: "run_bash",
|
|
281
475
|
description: "Execute a shell command (bash on Unix, cmd on Windows). Returns stdout+stderr. Non-interactive only.",
|
|
@@ -300,10 +494,10 @@ var init_run_bash = __esm({
|
|
|
300
494
|
const out = [stdout, stderr].filter(Boolean).join("\n");
|
|
301
495
|
const is_error = exitCode !== 0;
|
|
302
496
|
const body = out || (is_error ? `(no output)` : "");
|
|
303
|
-
const content = `${body}
|
|
497
|
+
const content = `${spillIfLarge(body, "command output")}
|
|
304
498
|
[exit ${exitCode}]`;
|
|
305
499
|
return {
|
|
306
|
-
content
|
|
500
|
+
content,
|
|
307
501
|
is_error
|
|
308
502
|
};
|
|
309
503
|
} catch (err) {
|
|
@@ -329,7 +523,7 @@ var init_grep = __esm({
|
|
|
329
523
|
pattern: { type: "string", description: "Regex pattern" },
|
|
330
524
|
path: { type: "string", description: "Root path to search (default cwd)" },
|
|
331
525
|
glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
|
|
332
|
-
case_insensitive: { type: "
|
|
526
|
+
case_insensitive: { type: "boolean", description: "Case-insensitive match" },
|
|
333
527
|
max_results: { type: "number", description: "Max matching lines (default 200)" }
|
|
334
528
|
},
|
|
335
529
|
required: ["pattern"]
|
|
@@ -377,8 +571,25 @@ var init_grep = __esm({
|
|
|
377
571
|
|
|
378
572
|
// src/tools/glob.ts
|
|
379
573
|
import { execa as execa3 } from "execa";
|
|
380
|
-
|
|
381
|
-
|
|
574
|
+
import { statSync as statSync2 } from "fs";
|
|
575
|
+
function byMtimeDesc(paths) {
|
|
576
|
+
const mtime = /* @__PURE__ */ new Map();
|
|
577
|
+
for (const p of paths) {
|
|
578
|
+
try {
|
|
579
|
+
mtime.set(p, statSync2(p).mtimeMs);
|
|
580
|
+
} catch {
|
|
581
|
+
mtime.set(p, 0);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return [...paths].sort((a, b) => (mtime.get(b) ?? 0) - (mtime.get(a) ?? 0));
|
|
585
|
+
}
|
|
586
|
+
function globToFindArgs(root, glob2) {
|
|
587
|
+
const stripped = glob2.replace(/^\*\*\//, "");
|
|
588
|
+
if (!stripped.includes("/")) {
|
|
589
|
+
return [root, "-type", "f", "-name", stripped];
|
|
590
|
+
}
|
|
591
|
+
const pathPat = "*/" + glob2.replace(/\*\*/g, "*");
|
|
592
|
+
return [root, "-type", "f", "-path", pathPat];
|
|
382
593
|
}
|
|
383
594
|
var glob;
|
|
384
595
|
var init_glob = __esm({
|
|
@@ -403,13 +614,10 @@ var init_glob = __esm({
|
|
|
403
614
|
reject: false,
|
|
404
615
|
timeout: 2e4
|
|
405
616
|
});
|
|
406
|
-
const tryFind = () => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
timeout: 2e4
|
|
411
|
-
});
|
|
412
|
-
};
|
|
617
|
+
const tryFind = () => execa3("find", globToFindArgs(root, pattern), {
|
|
618
|
+
reject: false,
|
|
619
|
+
timeout: 2e4
|
|
620
|
+
});
|
|
413
621
|
try {
|
|
414
622
|
let res;
|
|
415
623
|
try {
|
|
@@ -420,8 +628,9 @@ var init_glob = __esm({
|
|
|
420
628
|
} catch {
|
|
421
629
|
res = await tryFind();
|
|
422
630
|
}
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
631
|
+
const all = (res.stdout ?? "").split("\n").filter(Boolean);
|
|
632
|
+
if (all.length === 0) return { content: "No files matched." };
|
|
633
|
+
const lines = byMtimeDesc(all).slice(0, limit);
|
|
425
634
|
return { content: lines.join("\n") };
|
|
426
635
|
} catch (err) {
|
|
427
636
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
@@ -576,10 +785,14 @@ ${toolLines}
|
|
|
576
785
|
# Rules
|
|
577
786
|
- Always read a file before updating it. Never edit, overwrite, or create-over a file you have not read first this turn.
|
|
578
787
|
- Prefer editing existing files over creating new ones.
|
|
579
|
-
- For edit_file,
|
|
788
|
+
- For edit_file, make old_str unique by including surrounding context, or set replace_all to change every occurrence.
|
|
580
789
|
- Never invent file paths. Read, glob, or grep before editing.
|
|
581
790
|
- No filler, no pleasantries, no apologies.
|
|
582
791
|
|
|
792
|
+
# Context discipline
|
|
793
|
+
- read_file returns line numbers and accepts offset/limit. For large files, grep or glob to the relevant region first, then read only that range with offset/limit. Do not read a whole large file when you need a few functions \u2014 it wastes the context window.
|
|
794
|
+
- Reference code by the line numbers read_file returns.
|
|
795
|
+
|
|
583
796
|
# Testing and verification
|
|
584
797
|
- Always test the code after a change. Run the project's tests (e.g. npm test, pytest, go test) or the relevant script via run_bash before declaring a task done.
|
|
585
798
|
- If no test exists for the change, run the affected entry point via run_bash to verify it behaves correctly.
|
|
@@ -597,9 +810,9 @@ var init_system = __esm({
|
|
|
597
810
|
});
|
|
598
811
|
|
|
599
812
|
// src/permissions/policy.ts
|
|
600
|
-
import { readFileSync as readFileSync5, writeFileSync as
|
|
601
|
-
import { join as
|
|
602
|
-
import { homedir as
|
|
813
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync3, renameSync } from "fs";
|
|
814
|
+
import { join as join6 } from "path";
|
|
815
|
+
import { homedir as homedir5 } from "os";
|
|
603
816
|
function loadRules() {
|
|
604
817
|
if (!existsSync3(RULES_PATH)) return [];
|
|
605
818
|
try {
|
|
@@ -610,9 +823,9 @@ function loadRules() {
|
|
|
610
823
|
}
|
|
611
824
|
}
|
|
612
825
|
function saveRules(rules) {
|
|
613
|
-
|
|
826
|
+
mkdirSync5(RULES_DIR, { recursive: true });
|
|
614
827
|
const tmp = RULES_PATH + ".tmp";
|
|
615
|
-
|
|
828
|
+
writeFileSync6(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
|
|
616
829
|
renameSync(tmp, RULES_PATH);
|
|
617
830
|
}
|
|
618
831
|
function addRule(tool, pattern) {
|
|
@@ -654,8 +867,8 @@ var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
|
|
|
654
867
|
var init_policy = __esm({
|
|
655
868
|
"src/permissions/policy.ts"() {
|
|
656
869
|
"use strict";
|
|
657
|
-
RULES_DIR =
|
|
658
|
-
RULES_PATH =
|
|
870
|
+
RULES_DIR = join6(homedir5(), ".miii");
|
|
871
|
+
RULES_PATH = join6(RULES_DIR, "permissions.json");
|
|
659
872
|
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
660
873
|
}
|
|
661
874
|
});
|
|
@@ -985,16 +1198,16 @@ var init_loop = __esm({
|
|
|
985
1198
|
});
|
|
986
1199
|
|
|
987
1200
|
// eval/runner.ts
|
|
988
|
-
import { mkdtempSync, writeFileSync as
|
|
989
|
-
import { dirname as dirname2, join as
|
|
1201
|
+
import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
|
|
1202
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
990
1203
|
import { tmpdir } from "os";
|
|
991
1204
|
async function runScenario(model, s) {
|
|
992
|
-
const dir = mkdtempSync(
|
|
1205
|
+
const dir = mkdtempSync(join7(tmpdir(), "miii-eval-"));
|
|
993
1206
|
const prevCwd = process.cwd();
|
|
994
1207
|
for (const [rel, content] of Object.entries(s.files ?? {})) {
|
|
995
|
-
const abs =
|
|
996
|
-
|
|
997
|
-
|
|
1208
|
+
const abs = join7(dir, rel);
|
|
1209
|
+
mkdirSync6(dirname2(abs), { recursive: true });
|
|
1210
|
+
writeFileSync7(abs, content, "utf-8");
|
|
998
1211
|
}
|
|
999
1212
|
const r = {
|
|
1000
1213
|
name: s.name,
|
|
@@ -1032,7 +1245,7 @@ async function runScenario(model, s) {
|
|
|
1032
1245
|
r.durationMs = Date.now() - start;
|
|
1033
1246
|
if (r.error) {
|
|
1034
1247
|
r.reason = `loop error: ${r.error}`;
|
|
1035
|
-
|
|
1248
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
1036
1249
|
return r;
|
|
1037
1250
|
}
|
|
1038
1251
|
try {
|
|
@@ -1042,7 +1255,7 @@ async function runScenario(model, s) {
|
|
|
1042
1255
|
} catch (err) {
|
|
1043
1256
|
r.reason = `check threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
1044
1257
|
}
|
|
1045
|
-
|
|
1258
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
1046
1259
|
return r;
|
|
1047
1260
|
}
|
|
1048
1261
|
var autoYes;
|
|
@@ -1056,12 +1269,12 @@ var init_runner = __esm({
|
|
|
1056
1269
|
|
|
1057
1270
|
// eval/scenarios.ts
|
|
1058
1271
|
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1059
|
-
import { join as
|
|
1272
|
+
import { join as join8 } from "path";
|
|
1060
1273
|
var read, scenarios;
|
|
1061
1274
|
var init_scenarios = __esm({
|
|
1062
1275
|
"eval/scenarios.ts"() {
|
|
1063
1276
|
"use strict";
|
|
1064
|
-
read = (dir, f) => existsSync4(
|
|
1277
|
+
read = (dir, f) => existsSync4(join8(dir, f)) ? readFileSync6(join8(dir, f), "utf-8") : null;
|
|
1065
1278
|
scenarios = [
|
|
1066
1279
|
{
|
|
1067
1280
|
name: "edit-exact-string",
|
|
@@ -1208,9 +1421,9 @@ import { createElement } from "react";
|
|
|
1208
1421
|
|
|
1209
1422
|
// src/ui/App.tsx
|
|
1210
1423
|
init_client();
|
|
1211
|
-
import { useState as
|
|
1424
|
+
import { useState as useState5, useEffect as useEffect4 } from "react";
|
|
1212
1425
|
import { Box as Box10, Text as Text10, useApp } from "ink";
|
|
1213
|
-
import { homedir as
|
|
1426
|
+
import { homedir as homedir6 } from "os";
|
|
1214
1427
|
import { sep as sep2 } from "path";
|
|
1215
1428
|
|
|
1216
1429
|
// src/config.ts
|
|
@@ -1650,6 +1863,7 @@ function FilePicker({ matches: matches2, cursor }) {
|
|
|
1650
1863
|
}
|
|
1651
1864
|
|
|
1652
1865
|
// src/ui/ChatView.tsx
|
|
1866
|
+
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
1653
1867
|
import { Box as Box9, Text as Text9 } from "ink";
|
|
1654
1868
|
|
|
1655
1869
|
// src/ui/ThinkingBlock.tsx
|
|
@@ -1710,6 +1924,24 @@ var EMPTY_STATE_TITLE = "Ask anything, or try:";
|
|
|
1710
1924
|
|
|
1711
1925
|
// src/ui/ChatView.tsx
|
|
1712
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
|
+
}
|
|
1713
1945
|
function formatTokens(n) {
|
|
1714
1946
|
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
|
|
1715
1947
|
return String(n);
|
|
@@ -1732,8 +1964,8 @@ function FileEditBlock({
|
|
|
1732
1964
|
removed,
|
|
1733
1965
|
previewLines
|
|
1734
1966
|
}) {
|
|
1735
|
-
const
|
|
1736
|
-
const shown = previewLines.slice(0,
|
|
1967
|
+
const expanded = useToolExpanded();
|
|
1968
|
+
const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
|
|
1737
1969
|
const extra = previewLines.length - shown.length;
|
|
1738
1970
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
1739
1971
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
@@ -1750,15 +1982,22 @@ function FileEditBlock({
|
|
|
1750
1982
|
"\u23BF ",
|
|
1751
1983
|
removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
|
|
1752
1984
|
] }) }),
|
|
1753
|
-
shown.map((ln, i) =>
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
+
}),
|
|
1758
1997
|
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1759
1998
|
"\u2026 ",
|
|
1760
1999
|
extra,
|
|
1761
|
-
" more lines"
|
|
2000
|
+
" more lines \xB7 ctrl+o to expand"
|
|
1762
2001
|
] }) })
|
|
1763
2002
|
] });
|
|
1764
2003
|
}
|
|
@@ -1824,6 +2063,7 @@ function summarizeResult(res, toolName) {
|
|
|
1824
2063
|
return extra > 0 ? `${head} (+${extra} lines)` : head;
|
|
1825
2064
|
}
|
|
1826
2065
|
function ToolResultBlock({ result, toolName }) {
|
|
2066
|
+
const expanded = useToolExpanded();
|
|
1827
2067
|
const content = result.content ?? "";
|
|
1828
2068
|
const lines = content.split("\n");
|
|
1829
2069
|
const showMulti = (toolName === "run_bash" || toolName === "grep" || toolName === "glob" || result.is_error) && lines.length > 1;
|
|
@@ -1833,9 +2073,9 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
1833
2073
|
summarizeResult(result, toolName)
|
|
1834
2074
|
] }) });
|
|
1835
2075
|
}
|
|
1836
|
-
const MAX_LINES = 10;
|
|
1837
2076
|
const MAX_LINE_WIDTH = 200;
|
|
1838
|
-
const
|
|
2077
|
+
const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
2078
|
+
const shown = visible.map((l) => truncate(l, MAX_LINE_WIDTH));
|
|
1839
2079
|
const extra = lines.length - shown.length;
|
|
1840
2080
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
1841
2081
|
/* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
@@ -1846,7 +2086,7 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
1846
2086
|
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
1847
2087
|
"\u2026 ",
|
|
1848
2088
|
extra,
|
|
1849
|
-
" more lines"
|
|
2089
|
+
" more lines \xB7 ctrl+o to expand"
|
|
1850
2090
|
] }) })
|
|
1851
2091
|
] });
|
|
1852
2092
|
}
|
|
@@ -1987,22 +2227,22 @@ function ChatView({
|
|
|
1987
2227
|
|
|
1988
2228
|
// src/ui/hooks/useAgentRunner.ts
|
|
1989
2229
|
init_loop();
|
|
1990
|
-
import { useState as
|
|
2230
|
+
import { useState as useState4, useRef } from "react";
|
|
1991
2231
|
var FLUSH_MS = 100;
|
|
1992
2232
|
function useAgentRunner(model, activeCtx) {
|
|
1993
|
-
const [messages, setMessages] =
|
|
1994
|
-
const [thinking, setThinking] =
|
|
1995
|
-
const [thinkingContent, setThinkingContent] =
|
|
1996
|
-
const [streaming, setStreaming] =
|
|
1997
|
-
const [streamingContent, setStreamingContent] =
|
|
1998
|
-
const [error, setError] =
|
|
1999
|
-
const [busy, setBusy] =
|
|
2000
|
-
const [processingLabel, setProcessingLabel] =
|
|
2001
|
-
const [agentHistory, setAgentHistory] =
|
|
2002
|
-
const [pendingPermission, setPendingPermission] =
|
|
2003
|
-
const [permissionCursor, setPermissionCursor] =
|
|
2004
|
-
const [activeToolUses, setActiveToolUses] =
|
|
2005
|
-
const [activeToolResults, setActiveToolResults] =
|
|
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([]);
|
|
2006
2246
|
const busyRef = useRef(false);
|
|
2007
2247
|
const abortRef = useRef(null);
|
|
2008
2248
|
const pendingPermissionRef = useRef(null);
|
|
@@ -2273,6 +2513,10 @@ function useKeyboard(opts) {
|
|
|
2273
2513
|
toggleThinkingVisible();
|
|
2274
2514
|
return;
|
|
2275
2515
|
}
|
|
2516
|
+
if (key.ctrl && char === "o") {
|
|
2517
|
+
toggleToolExpanded();
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2276
2520
|
if (key.escape && busyRef.current && abortRef.current) {
|
|
2277
2521
|
abortRef.current.abort();
|
|
2278
2522
|
return;
|
|
@@ -2497,31 +2741,31 @@ async function checkForUpdate() {
|
|
|
2497
2741
|
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2498
2742
|
function App() {
|
|
2499
2743
|
const { exit } = useApp();
|
|
2500
|
-
const cwd = process.cwd().replace(
|
|
2501
|
-
const [cfg, setCfg] =
|
|
2502
|
-
const [models, setModels] =
|
|
2503
|
-
const [contexts, setContexts] =
|
|
2504
|
-
const [activeCtx, setActiveCtx] =
|
|
2505
|
-
const [state, setState] =
|
|
2506
|
-
const [cursor, setCursor] =
|
|
2507
|
-
const [updateAvailable, setUpdateAvailable] =
|
|
2508
|
-
const [ollamaDown, setOllamaDown] =
|
|
2509
|
-
const [sessionId, setSessionId] =
|
|
2510
|
-
const [sessions, setSessions] =
|
|
2511
|
-
const [notice, setNotice] =
|
|
2512
|
-
const [input, setInput] =
|
|
2513
|
-
const [paletteCursor, setPaletteCursor] =
|
|
2514
|
-
const [filePickerCursor, setFilePickerCursor] =
|
|
2744
|
+
const cwd = process.cwd().replace(homedir6(), "~").split(sep2).join("/");
|
|
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);
|
|
2515
2759
|
const agent = useAgentRunner(cfg.model, activeCtx);
|
|
2516
|
-
|
|
2760
|
+
useEffect4(() => {
|
|
2517
2761
|
checkForUpdate().then((v) => {
|
|
2518
2762
|
if (v) setUpdateAvailable(v);
|
|
2519
2763
|
});
|
|
2520
2764
|
}, []);
|
|
2521
|
-
|
|
2765
|
+
useEffect4(() => {
|
|
2522
2766
|
if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
|
|
2523
2767
|
}, [agent.agentHistory, sessionId]);
|
|
2524
|
-
|
|
2768
|
+
useEffect4(() => {
|
|
2525
2769
|
listModels().then((m) => {
|
|
2526
2770
|
setModels(m);
|
|
2527
2771
|
setState(cfg.model ? "ready" : "select-model");
|
|
@@ -2635,6 +2879,8 @@ function App() {
|
|
|
2635
2879
|
}
|
|
2636
2880
|
|
|
2637
2881
|
// src/cli.tsx
|
|
2882
|
+
init_spill();
|
|
2883
|
+
cleanupSpill();
|
|
2638
2884
|
var [, , cmd, ...rest] = process.argv;
|
|
2639
2885
|
if (cmd === "update" || cmd === "--update" || cmd === "-u") {
|
|
2640
2886
|
const { spawnSync } = await import("child_process");
|