miii-agent 0.1.14 → 0.1.16
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 +45 -3
- package/dist/cli.js +278 -61
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a href="https://ollama.com"><img src="https://img.shields.io/badge/powered%20by-Ollama-black" alt="powered by Ollama"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
miii is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com)
|
|
19
|
+
miii is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com) — or any OpenAI-compatible local server like [llama.cpp](https://github.com/ggml-org/llama.cpp) and [LM Studio](https://lmstudio.ai) — it reads your code, writes features, runs tests, and fixes bugs entirely on your hardware, at native speed.
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -48,7 +48,7 @@ Most AI coding tools are wrappers around cloud APIs. They are slow, expensive, a
|
|
|
48
48
|
|
|
49
49
|
miii flips the script:
|
|
50
50
|
|
|
51
|
-
- **Absolute Privacy** — Powered by Ollama. Your code stays on your disk, period.
|
|
51
|
+
- **Absolute Privacy** — Powered by Ollama, llama.cpp, or any local OpenAI-compatible server. Your code stays on your disk, period.
|
|
52
52
|
- **Zero Friction** — No API keys, no billing, no accounts. Just `miii`.
|
|
53
53
|
- **True Agency** — miii doesn't just chat; it decomposes problems, invokes tools, and verifies results like a senior engineer.
|
|
54
54
|
- **Native Performance** — No network round-trips. Latency is limited by your GPU, not a CDN.
|
|
@@ -120,6 +120,19 @@ Inside the TUI, interact naturally:
|
|
|
120
120
|
| `Ctrl+O` | Toggle full tool output view |
|
|
121
121
|
| `Ctrl+C` | Quit |
|
|
122
122
|
|
|
123
|
+
### Project Instructions (`MIII.md`)
|
|
124
|
+
|
|
125
|
+
Drop a `MIII.md` file in your project and miii reads it first, every turn — the same idea as `CLAUDE.md`. Use it to teach the agent your conventions, build/test commands, architecture, and do's and don'ts.
|
|
126
|
+
|
|
127
|
+
```markdown
|
|
128
|
+
# MIII.md
|
|
129
|
+
- Use tabs, not spaces.
|
|
130
|
+
- Run `npm test` before declaring any task done.
|
|
131
|
+
- The HTTP layer lives in src/server/ — never import it from src/core/.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
miii searches upward from the working directory to the repo root (the directory containing `.git`); the nearest `MIII.md` wins. It is treated as authoritative — higher priority than the agent's defaults — except it can never override the permission system or safety boundaries. Files over 32KB are truncated.
|
|
135
|
+
|
|
123
136
|
---
|
|
124
137
|
|
|
125
138
|
## Technical Deep Dive
|
|
@@ -137,7 +150,7 @@ miii ships with a built-in tool suite that the agent invokes autonomously:
|
|
|
137
150
|
| `grep` | Regex search across files |
|
|
138
151
|
| `run_bash` | Execute shell commands |
|
|
139
152
|
|
|
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`.
|
|
153
|
+
**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`. The file tools (`read_file`, `write_file`, `edit_file`) are strictly confined to your working directory; `../` traversal and absolute paths outside the workspace are rejected. `run_bash` runs arbitrary shell commands and is **not** path-confined — its only boundary is the permission prompt, so review commands before approving (especially "always").
|
|
141
154
|
|
|
142
155
|
### Lossless Output Spill
|
|
143
156
|
|
|
@@ -197,6 +210,35 @@ Settings live in `~/.miii/config.json` and are created on first run.
|
|
|
197
210
|
| `ollamaHost` | Ollama API endpoint | URL string |
|
|
198
211
|
| `effort` | Controls temperature & limits | `low` \| `medium` \| `high` |
|
|
199
212
|
|
|
213
|
+
### Other Local Backends
|
|
214
|
+
|
|
215
|
+
Ollama is the default, but miii talks to any **OpenAI-compatible** local server — so you can run [llama.cpp](https://github.com/ggml-org/llama.cpp) or [LM Studio](https://lmstudio.ai) instead. Your code still never leaves your machine.
|
|
216
|
+
|
|
217
|
+
Start `llama-server` (ships with llama.cpp), then point a named provider at it:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
llama-server -m ./qwen2.5-coder-14b.gguf --port 8080
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"model": "qwen2.5-coder-14b",
|
|
226
|
+
"provider": "llamacpp",
|
|
227
|
+
"providers": {
|
|
228
|
+
"llamacpp": { "type": "openai", "baseUrl": "http://localhost:8080" }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
| Field | Description | Values |
|
|
234
|
+
|-------|-------------|--------|
|
|
235
|
+
| `provider` | Active provider name (keys into `providers`) | e.g. `ollama`, `llamacpp` |
|
|
236
|
+
| `providers.<name>.type` | Wire protocol | `ollama` \| `openai` |
|
|
237
|
+
| `providers.<name>.baseUrl` | Server endpoint | URL string |
|
|
238
|
+
| `providers.<name>.apiKey` | Optional bearer token | string |
|
|
239
|
+
|
|
240
|
+
Switch the active provider at launch with `miii --provider llamacpp`. Any `openai`-type provider on a `localhost` URL counts as local — no key, no cloud.
|
|
241
|
+
|
|
200
242
|
---
|
|
201
243
|
|
|
202
244
|
## System Architecture
|
package/dist/cli.js
CHANGED
|
@@ -79,10 +79,15 @@ function setEffort(effort) {
|
|
|
79
79
|
function setProvider(provider) {
|
|
80
80
|
saveConfig({ ...readRawConfig(), provider });
|
|
81
81
|
}
|
|
82
|
-
var CONFIG_DIR, CONFIG_PATH;
|
|
82
|
+
var EFFORT_OPTIONS, CONFIG_DIR, CONFIG_PATH;
|
|
83
83
|
var init_config = __esm({
|
|
84
84
|
"src/config.ts"() {
|
|
85
85
|
"use strict";
|
|
86
|
+
EFFORT_OPTIONS = {
|
|
87
|
+
low: { temperature: 0.2, num_predict: 1024 },
|
|
88
|
+
medium: { temperature: 0.7, num_predict: 2048 },
|
|
89
|
+
high: { temperature: 1, num_predict: -1 }
|
|
90
|
+
};
|
|
86
91
|
CONFIG_DIR = join(homedir(), ".miii");
|
|
87
92
|
CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
88
93
|
}
|
|
@@ -850,6 +855,7 @@ var grep;
|
|
|
850
855
|
var init_grep = __esm({
|
|
851
856
|
"src/tools/grep.ts"() {
|
|
852
857
|
"use strict";
|
|
858
|
+
init_paths();
|
|
853
859
|
grep = {
|
|
854
860
|
name: "grep",
|
|
855
861
|
description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
|
|
@@ -865,7 +871,12 @@ var init_grep = __esm({
|
|
|
865
871
|
required: ["pattern"]
|
|
866
872
|
},
|
|
867
873
|
handler: async ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
|
|
868
|
-
|
|
874
|
+
let root;
|
|
875
|
+
try {
|
|
876
|
+
root = confinePath(path ?? ".");
|
|
877
|
+
} catch (err) {
|
|
878
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
879
|
+
}
|
|
869
880
|
const limit = max_results ?? 200;
|
|
870
881
|
const ci = case_insensitive === true || String(case_insensitive) === "true";
|
|
871
882
|
const tryRg = async () => {
|
|
@@ -931,6 +942,7 @@ var glob;
|
|
|
931
942
|
var init_glob = __esm({
|
|
932
943
|
"src/tools/glob.ts"() {
|
|
933
944
|
"use strict";
|
|
945
|
+
init_paths();
|
|
934
946
|
glob = {
|
|
935
947
|
name: "glob",
|
|
936
948
|
description: 'List files matching a glob pattern (e.g. "**/*.ts"). Uses ripgrep --files if available.',
|
|
@@ -944,7 +956,12 @@ var init_glob = __esm({
|
|
|
944
956
|
required: ["pattern"]
|
|
945
957
|
},
|
|
946
958
|
handler: async ({ pattern, path, max_results }) => {
|
|
947
|
-
|
|
959
|
+
let root;
|
|
960
|
+
try {
|
|
961
|
+
root = confinePath(path ?? ".");
|
|
962
|
+
} catch (err) {
|
|
963
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
964
|
+
}
|
|
948
965
|
const limit = max_results ?? 500;
|
|
949
966
|
const tryRg = () => execa3("rg", ["--files", "--hidden", "--glob", pattern, root], {
|
|
950
967
|
reject: false,
|
|
@@ -1056,12 +1073,61 @@ var init_validate = __esm({
|
|
|
1056
1073
|
}
|
|
1057
1074
|
});
|
|
1058
1075
|
|
|
1076
|
+
// src/prompt/context.ts
|
|
1077
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1078
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
1079
|
+
function findContextFile(cwd) {
|
|
1080
|
+
let dir = cwd;
|
|
1081
|
+
for (; ; ) {
|
|
1082
|
+
const candidate = join6(dir, CONTEXT_FILENAME);
|
|
1083
|
+
if (existsSync3(candidate)) return candidate;
|
|
1084
|
+
if (existsSync3(join6(dir, ".git"))) return null;
|
|
1085
|
+
const parent = dirname2(dir);
|
|
1086
|
+
if (parent === dir) return null;
|
|
1087
|
+
dir = parent;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function loadProjectContext(cwd) {
|
|
1091
|
+
const source = findContextFile(cwd);
|
|
1092
|
+
if (!source) return EMPTY;
|
|
1093
|
+
try {
|
|
1094
|
+
if (statSync3(source).size === 0) return { ...EMPTY, source };
|
|
1095
|
+
const raw = readFileSync5(source, "utf8");
|
|
1096
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_CONTEXT_BYTES) {
|
|
1097
|
+
const clipped = Buffer.from(raw, "utf8").subarray(0, MAX_CONTEXT_BYTES).toString("utf8");
|
|
1098
|
+
return { content: clipped, source, truncated: true };
|
|
1099
|
+
}
|
|
1100
|
+
return { content: raw, source, truncated: false };
|
|
1101
|
+
} catch {
|
|
1102
|
+
return { ...EMPTY, source };
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
var CONTEXT_FILENAME, MAX_CONTEXT_BYTES, EMPTY;
|
|
1106
|
+
var init_context = __esm({
|
|
1107
|
+
"src/prompt/context.ts"() {
|
|
1108
|
+
"use strict";
|
|
1109
|
+
CONTEXT_FILENAME = "MIII.md";
|
|
1110
|
+
MAX_CONTEXT_BYTES = 32 * 1024;
|
|
1111
|
+
EMPTY = { content: "", source: null, truncated: false };
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1059
1115
|
// src/prompt/system.ts
|
|
1060
|
-
function buildSystemPrompt(tools, cwd) {
|
|
1116
|
+
function buildSystemPrompt(tools, cwd, project) {
|
|
1061
1117
|
const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
1118
|
+
const projectSection = project && project.content.trim() ? `
|
|
1119
|
+
# ${CONTEXT_FILENAME} \u2014 project instructions (authoritative, read first)
|
|
1120
|
+
The user maintains ${CONTEXT_FILENAME} at ${project.source} to steer how you work in this project: conventions, commands, architecture, do's and don'ts. Treat it as direct instruction from the user, higher priority than your defaults. When it conflicts with a default rule below, ${CONTEXT_FILENAME} wins (except permissions and safety, which you never override).${project.truncated ? `
|
|
1121
|
+
(Note: file exceeded ${"32KB"} and was truncated.)` : ""}
|
|
1122
|
+
|
|
1123
|
+
--- BEGIN ${CONTEXT_FILENAME} ---
|
|
1124
|
+
${project.content.trim()}
|
|
1125
|
+
--- END ${CONTEXT_FILENAME} ---
|
|
1126
|
+
` : "";
|
|
1062
1127
|
return `You are miii, a senior software engineer running in a terminal.
|
|
1063
1128
|
|
|
1064
1129
|
Working directory: ${cwd}
|
|
1130
|
+
${projectSection}
|
|
1065
1131
|
|
|
1066
1132
|
# Goal Understanding (read this first, every turn)
|
|
1067
1133
|
Before acting on any request, extract and hold three things:
|
|
@@ -1142,17 +1208,18 @@ ${toolLines}
|
|
|
1142
1208
|
var init_system = __esm({
|
|
1143
1209
|
"src/prompt/system.ts"() {
|
|
1144
1210
|
"use strict";
|
|
1211
|
+
init_context();
|
|
1145
1212
|
}
|
|
1146
1213
|
});
|
|
1147
1214
|
|
|
1148
1215
|
// src/permissions/policy.ts
|
|
1149
|
-
import { readFileSync as
|
|
1150
|
-
import { join as
|
|
1216
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync4, renameSync } from "fs";
|
|
1217
|
+
import { join as join7 } from "path";
|
|
1151
1218
|
import { homedir as homedir5 } from "os";
|
|
1152
1219
|
function loadRules() {
|
|
1153
|
-
if (!
|
|
1220
|
+
if (!existsSync4(RULES_PATH)) return [];
|
|
1154
1221
|
try {
|
|
1155
|
-
const data = JSON.parse(
|
|
1222
|
+
const data = JSON.parse(readFileSync6(RULES_PATH, "utf-8"));
|
|
1156
1223
|
return Array.isArray(data.rules) ? data.rules : [];
|
|
1157
1224
|
} catch {
|
|
1158
1225
|
return [];
|
|
@@ -1203,8 +1270,8 @@ var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
|
|
|
1203
1270
|
var init_policy = __esm({
|
|
1204
1271
|
"src/permissions/policy.ts"() {
|
|
1205
1272
|
"use strict";
|
|
1206
|
-
RULES_DIR =
|
|
1207
|
-
RULES_PATH =
|
|
1273
|
+
RULES_DIR = join7(homedir5(), ".miii");
|
|
1274
|
+
RULES_PATH = join7(RULES_DIR, "permissions.json");
|
|
1208
1275
|
ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
|
|
1209
1276
|
}
|
|
1210
1277
|
});
|
|
@@ -1346,11 +1413,37 @@ var init_adapter = __esm({
|
|
|
1346
1413
|
});
|
|
1347
1414
|
|
|
1348
1415
|
// src/agent/loop.ts
|
|
1416
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1417
|
+
function readGuard(name, input, seen) {
|
|
1418
|
+
if (name !== "edit_file" && name !== "write_file") return null;
|
|
1419
|
+
const p = input.path;
|
|
1420
|
+
if (typeof p !== "string" || !p) return null;
|
|
1421
|
+
let abs;
|
|
1422
|
+
try {
|
|
1423
|
+
abs = confinePath(p);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
if (seen.has(abs)) return null;
|
|
1428
|
+
if (name === "write_file" && !existsSync5(abs)) return null;
|
|
1429
|
+
const verb = name === "edit_file" ? "edit" : "overwrite";
|
|
1430
|
+
return `Refusing to ${verb} ${p}: you have not read it this turn. Call read_file on ${p} first, then retry the ${name}.`;
|
|
1431
|
+
}
|
|
1432
|
+
function markSeen(name, input, seen) {
|
|
1433
|
+
if (name !== "read_file" && name !== "edit_file" && name !== "write_file") return;
|
|
1434
|
+
const p = input.path;
|
|
1435
|
+
if (typeof p !== "string" || !p) return;
|
|
1436
|
+
try {
|
|
1437
|
+
seen.add(confinePath(p));
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1349
1441
|
async function* runAgent(opts) {
|
|
1350
1442
|
const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
|
|
1351
1443
|
const startTime = Date.now();
|
|
1352
|
-
const system = buildSystemPrompt(TOOLS, cwd);
|
|
1444
|
+
const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd));
|
|
1353
1445
|
const ollamaTools = toOllamaTools(TOOLS);
|
|
1446
|
+
const effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
|
|
1354
1447
|
const history = [
|
|
1355
1448
|
...opts.history,
|
|
1356
1449
|
{ role: "user", content: opts.userText }
|
|
@@ -1359,6 +1452,7 @@ async function* runAgent(opts) {
|
|
|
1359
1452
|
let evalTokens = 0;
|
|
1360
1453
|
let lastAssistantSig = "";
|
|
1361
1454
|
let repeatCount = 0;
|
|
1455
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1362
1456
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
1363
1457
|
let text = "";
|
|
1364
1458
|
let tool_calls;
|
|
@@ -1369,7 +1463,7 @@ async function* runAgent(opts) {
|
|
|
1369
1463
|
const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
|
|
1370
1464
|
if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
|
|
1371
1465
|
try {
|
|
1372
|
-
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict:
|
|
1466
|
+
for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature })) {
|
|
1373
1467
|
if (signal?.aborted) break;
|
|
1374
1468
|
if (chunk.content) {
|
|
1375
1469
|
text += chunk.content;
|
|
@@ -1483,6 +1577,18 @@ async function* runAgent(opts) {
|
|
|
1483
1577
|
yield { type: "tool-result", block: r2 };
|
|
1484
1578
|
continue;
|
|
1485
1579
|
}
|
|
1580
|
+
const guard = readGuard(use.name, use.input, seenPaths);
|
|
1581
|
+
if (guard) {
|
|
1582
|
+
const r2 = {
|
|
1583
|
+
type: "tool_result",
|
|
1584
|
+
tool_use_id: use.id,
|
|
1585
|
+
content: guard,
|
|
1586
|
+
is_error: true
|
|
1587
|
+
};
|
|
1588
|
+
results.push(r2);
|
|
1589
|
+
yield { type: "tool-result", block: r2 };
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1486
1592
|
try {
|
|
1487
1593
|
await hooks?.firePre(use);
|
|
1488
1594
|
} catch {
|
|
@@ -1504,6 +1610,7 @@ async function* runAgent(opts) {
|
|
|
1504
1610
|
is_error: true
|
|
1505
1611
|
};
|
|
1506
1612
|
}
|
|
1613
|
+
if (!r.is_error) markSeen(use.name, use.input, seenPaths);
|
|
1507
1614
|
try {
|
|
1508
1615
|
await hooks?.firePost(use, r);
|
|
1509
1616
|
} catch {
|
|
@@ -1517,18 +1624,20 @@ async function* runAgent(opts) {
|
|
|
1517
1624
|
yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
|
|
1518
1625
|
return history;
|
|
1519
1626
|
}
|
|
1520
|
-
var MAX_TURNS,
|
|
1627
|
+
var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL;
|
|
1521
1628
|
var init_loop = __esm({
|
|
1522
1629
|
"src/agent/loop.ts"() {
|
|
1523
1630
|
"use strict";
|
|
1524
1631
|
init_client();
|
|
1632
|
+
init_paths();
|
|
1525
1633
|
init_registry();
|
|
1526
1634
|
init_validate();
|
|
1527
1635
|
init_system();
|
|
1636
|
+
init_context();
|
|
1528
1637
|
init_policy();
|
|
1638
|
+
init_config();
|
|
1529
1639
|
init_adapter();
|
|
1530
1640
|
MAX_TURNS = 25;
|
|
1531
|
-
NUM_PREDICT = 8192;
|
|
1532
1641
|
REPEAT_TAIL = 120;
|
|
1533
1642
|
REPEAT_KILL = 4;
|
|
1534
1643
|
}
|
|
@@ -1536,14 +1645,14 @@ var init_loop = __esm({
|
|
|
1536
1645
|
|
|
1537
1646
|
// eval/runner.ts
|
|
1538
1647
|
import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
|
|
1539
|
-
import { dirname as
|
|
1648
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1540
1649
|
import { tmpdir } from "os";
|
|
1541
1650
|
async function runScenario(model, s) {
|
|
1542
|
-
const dir = mkdtempSync(
|
|
1651
|
+
const dir = mkdtempSync(join8(tmpdir(), "miii-eval-"));
|
|
1543
1652
|
const prevCwd = process.cwd();
|
|
1544
1653
|
for (const [rel, content] of Object.entries(s.files ?? {})) {
|
|
1545
|
-
const abs =
|
|
1546
|
-
mkdirSync6(
|
|
1654
|
+
const abs = join8(dir, rel);
|
|
1655
|
+
mkdirSync6(dirname3(abs), { recursive: true });
|
|
1547
1656
|
writeFileSync7(abs, content, "utf-8");
|
|
1548
1657
|
}
|
|
1549
1658
|
const r = {
|
|
@@ -1605,13 +1714,13 @@ var init_runner = __esm({
|
|
|
1605
1714
|
});
|
|
1606
1715
|
|
|
1607
1716
|
// eval/scenarios.ts
|
|
1608
|
-
import { readFileSync as
|
|
1609
|
-
import { join as
|
|
1717
|
+
import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
|
|
1718
|
+
import { join as join9 } from "path";
|
|
1610
1719
|
var read, scenarios;
|
|
1611
1720
|
var init_scenarios = __esm({
|
|
1612
1721
|
"eval/scenarios.ts"() {
|
|
1613
1722
|
"use strict";
|
|
1614
|
-
read = (dir, f) =>
|
|
1723
|
+
read = (dir, f) => existsSync6(join9(dir, f)) ? readFileSync7(join9(dir, f), "utf-8") : null;
|
|
1615
1724
|
scenarios = [
|
|
1616
1725
|
{
|
|
1617
1726
|
name: "edit-exact-string",
|
|
@@ -1767,7 +1876,7 @@ import { sep as sep2 } from "path";
|
|
|
1767
1876
|
// src/ui/WelcomeBlock.tsx
|
|
1768
1877
|
import { Box, Text } from "ink";
|
|
1769
1878
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1770
|
-
function WelcomeBlock({ model, activeCtx, effort, cwd }) {
|
|
1879
|
+
function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
|
|
1771
1880
|
const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
|
|
1772
1881
|
return /* @__PURE__ */ jsxs(
|
|
1773
1882
|
Box,
|
|
@@ -1790,18 +1899,19 @@ function WelcomeBlock({ model, activeCtx, effort, cwd }) {
|
|
|
1790
1899
|
" effort"
|
|
1791
1900
|
] })
|
|
1792
1901
|
] }),
|
|
1793
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
|
|
1902
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd }),
|
|
1903
|
+
updateAvailable && /* @__PURE__ */ jsx(Text, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` })
|
|
1794
1904
|
]
|
|
1795
1905
|
}
|
|
1796
1906
|
);
|
|
1797
1907
|
}
|
|
1798
1908
|
|
|
1799
1909
|
// src/ui/InputBar.tsx
|
|
1800
|
-
import { useEffect, useState } from "react";
|
|
1910
|
+
import { memo, useEffect, useState } from "react";
|
|
1801
1911
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
1802
1912
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1803
1913
|
var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1804
|
-
|
|
1914
|
+
var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
|
|
1805
1915
|
const [frame, setFrame] = useState(0);
|
|
1806
1916
|
useEffect(() => {
|
|
1807
1917
|
if (!disabled) return;
|
|
@@ -1829,7 +1939,7 @@ function InputBar({ input, disabled, processingLabel }) {
|
|
|
1829
1939
|
] })
|
|
1830
1940
|
}
|
|
1831
1941
|
);
|
|
1832
|
-
}
|
|
1942
|
+
});
|
|
1833
1943
|
|
|
1834
1944
|
// src/ui/ModelsView.tsx
|
|
1835
1945
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
@@ -1837,7 +1947,7 @@ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
|
1837
1947
|
function ModelsView({ models, cursor, model, host, provider, effort, query, requireSelection }) {
|
|
1838
1948
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
|
|
1839
1949
|
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
1840
|
-
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1950
|
+
/* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", children: [
|
|
1841
1951
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
|
|
1842
1952
|
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: provider }),
|
|
1843
1953
|
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
@@ -1855,7 +1965,7 @@ function ModelsView({ models, cursor, model, host, provider, effort, query, requ
|
|
|
1855
1965
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "select model" }),
|
|
1856
1966
|
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: query ? `no models match "${query}"` : provider === "lmstudio" ? "no models. load a model in LM Studio and start the server." : "no models found." }) : models.map((m, i) => {
|
|
1857
1967
|
const sel = i === cursor;
|
|
1858
|
-
return /* @__PURE__ */ jsxs3(Text3, { color: sel ? "blue" : void 0, dimColor: !sel, children: [
|
|
1968
|
+
return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate", color: sel ? "blue" : void 0, dimColor: !sel, children: [
|
|
1859
1969
|
sel ? "\u276F " : " ",
|
|
1860
1970
|
m,
|
|
1861
1971
|
m === model ? /* @__PURE__ */ jsx3(Text3, { color: "green", children: " \u25CF" }) : null
|
|
@@ -1901,6 +2011,11 @@ function ProviderPicker({ entries, cursor, activeName, query }) {
|
|
|
1901
2011
|
// src/ui/SessionsView.tsx
|
|
1902
2012
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
1903
2013
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2014
|
+
function truncate(s, max) {
|
|
2015
|
+
if (max <= 0) return "";
|
|
2016
|
+
if (s.length <= max) return s;
|
|
2017
|
+
return s.slice(0, Math.max(0, max - 1)) + "\u2026";
|
|
2018
|
+
}
|
|
1904
2019
|
function relativeTime(iso) {
|
|
1905
2020
|
const diff = Date.now() - new Date(iso).getTime();
|
|
1906
2021
|
const min = Math.floor(diff / 6e4);
|
|
@@ -1916,13 +2031,16 @@ function SessionsView({ sessions, cursor }) {
|
|
|
1916
2031
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
|
|
1917
2032
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: sessions.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "no saved sessions yet" }) : sessions.map((s, i) => {
|
|
1918
2033
|
const active2 = i === cursor;
|
|
1919
|
-
const
|
|
2034
|
+
const meta = `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}`;
|
|
2035
|
+
const cols = process.stdout.columns ?? 80;
|
|
2036
|
+
const titleMax = cols - 9 - meta.length;
|
|
2037
|
+
const label = truncate(s.title, titleMax);
|
|
1920
2038
|
return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
|
|
1921
|
-
/* @__PURE__ */ jsxs5(Text5, { color: active2 ? "blue" : void 0, dimColor: !active2, children: [
|
|
2039
|
+
/* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", color: active2 ? "blue" : void 0, dimColor: !active2, children: [
|
|
1922
2040
|
active2 ? "\u276F " : " ",
|
|
1923
2041
|
label
|
|
1924
2042
|
] }),
|
|
1925
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children:
|
|
2043
|
+
/* @__PURE__ */ jsx5(Text5, { wrap: "truncate", dimColor: true, children: meta })
|
|
1926
2044
|
] }, s.id);
|
|
1927
2045
|
}) }),
|
|
1928
2046
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
|
|
@@ -2198,8 +2316,9 @@ function FilePicker({ matches: matches2, cursor }) {
|
|
|
2198
2316
|
}
|
|
2199
2317
|
|
|
2200
2318
|
// src/ui/ChatView.tsx
|
|
2201
|
-
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
2319
|
+
import { memo as memo2, useState as useState3, useEffect as useEffect3 } from "react";
|
|
2202
2320
|
import { Box as Box9, Text as Text9 } from "ink";
|
|
2321
|
+
import { highlight } from "cli-highlight";
|
|
2203
2322
|
|
|
2204
2323
|
// src/ui/ThinkingBlock.tsx
|
|
2205
2324
|
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
@@ -2292,6 +2411,55 @@ function countLines(s) {
|
|
|
2292
2411
|
if (!s) return 0;
|
|
2293
2412
|
return s.split("\n").length;
|
|
2294
2413
|
}
|
|
2414
|
+
var EXT_LANG = {
|
|
2415
|
+
ts: "typescript",
|
|
2416
|
+
tsx: "typescript",
|
|
2417
|
+
mts: "typescript",
|
|
2418
|
+
cts: "typescript",
|
|
2419
|
+
js: "javascript",
|
|
2420
|
+
jsx: "javascript",
|
|
2421
|
+
mjs: "javascript",
|
|
2422
|
+
cjs: "javascript",
|
|
2423
|
+
json: "json",
|
|
2424
|
+
py: "python",
|
|
2425
|
+
rb: "ruby",
|
|
2426
|
+
go: "go",
|
|
2427
|
+
rs: "rust",
|
|
2428
|
+
java: "java",
|
|
2429
|
+
c: "c",
|
|
2430
|
+
h: "c",
|
|
2431
|
+
cpp: "cpp",
|
|
2432
|
+
cc: "cpp",
|
|
2433
|
+
hpp: "cpp",
|
|
2434
|
+
cs: "csharp",
|
|
2435
|
+
php: "php",
|
|
2436
|
+
swift: "swift",
|
|
2437
|
+
kt: "kotlin",
|
|
2438
|
+
scala: "scala",
|
|
2439
|
+
sh: "bash",
|
|
2440
|
+
bash: "bash",
|
|
2441
|
+
zsh: "bash",
|
|
2442
|
+
yml: "yaml",
|
|
2443
|
+
yaml: "yaml",
|
|
2444
|
+
html: "xml",
|
|
2445
|
+
xml: "xml",
|
|
2446
|
+
css: "css",
|
|
2447
|
+
scss: "scss",
|
|
2448
|
+
sql: "sql",
|
|
2449
|
+
md: "markdown"
|
|
2450
|
+
};
|
|
2451
|
+
function langFromPath(path) {
|
|
2452
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
2453
|
+
return ext ? EXT_LANG[ext] : void 0;
|
|
2454
|
+
}
|
|
2455
|
+
function highlightLine(text, lang) {
|
|
2456
|
+
if (!lang) return text;
|
|
2457
|
+
try {
|
|
2458
|
+
return highlight(text, { language: lang, ignoreIllegals: true });
|
|
2459
|
+
} catch {
|
|
2460
|
+
return text;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2295
2463
|
function FileEditBlock({
|
|
2296
2464
|
label,
|
|
2297
2465
|
path,
|
|
@@ -2302,10 +2470,11 @@ function FileEditBlock({
|
|
|
2302
2470
|
const expanded = useToolExpanded();
|
|
2303
2471
|
const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
|
|
2304
2472
|
const extra = previewLines.length - shown.length;
|
|
2473
|
+
const lang = langFromPath(path);
|
|
2305
2474
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2306
2475
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2307
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2308
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "
|
|
2476
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
2477
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
|
|
2309
2478
|
label,
|
|
2310
2479
|
" "
|
|
2311
2480
|
] }),
|
|
@@ -2319,15 +2488,19 @@ function FileEditBlock({
|
|
|
2319
2488
|
] }) }),
|
|
2320
2489
|
shown.map((ln, i) => {
|
|
2321
2490
|
const width = (process.stdout.columns ?? 80) - 6 - 20;
|
|
2322
|
-
const
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2491
|
+
const textWidth = Math.max(0, width - 2);
|
|
2492
|
+
const plain = ln.text.length > textWidth ? ln.text.slice(0, textWidth) : ln.text.padEnd(textWidth);
|
|
2493
|
+
const code = ln.sign === " " ? plain : highlightLine(plain, lang);
|
|
2494
|
+
return /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(
|
|
2325
2495
|
Text9,
|
|
2326
2496
|
{
|
|
2327
2497
|
wrap: "truncate",
|
|
2328
2498
|
backgroundColor: ln.sign === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
|
|
2329
2499
|
dimColor: ln.sign === " ",
|
|
2330
|
-
children:
|
|
2500
|
+
children: [
|
|
2501
|
+
`${ln.sign} `,
|
|
2502
|
+
code
|
|
2503
|
+
]
|
|
2331
2504
|
}
|
|
2332
2505
|
) }, i);
|
|
2333
2506
|
}),
|
|
@@ -2346,7 +2519,7 @@ var TOOL_LABEL = {
|
|
|
2346
2519
|
glob: "Glob",
|
|
2347
2520
|
grep: "Grep"
|
|
2348
2521
|
};
|
|
2349
|
-
function
|
|
2522
|
+
function truncate2(s, max) {
|
|
2350
2523
|
if (s.length <= max) return s;
|
|
2351
2524
|
return s.slice(0, max - 1) + "\u2026";
|
|
2352
2525
|
}
|
|
@@ -2362,15 +2535,15 @@ function toolHeader(use) {
|
|
|
2362
2535
|
break;
|
|
2363
2536
|
case "run_bash": {
|
|
2364
2537
|
const cmd2 = String(input.command ?? "").replace(/\s+/g, " ");
|
|
2365
|
-
arg =
|
|
2538
|
+
arg = truncate2(cmd2, 120);
|
|
2366
2539
|
break;
|
|
2367
2540
|
}
|
|
2368
2541
|
case "glob":
|
|
2369
2542
|
case "grep":
|
|
2370
|
-
arg =
|
|
2543
|
+
arg = truncate2(String(input.pattern ?? ""), 120);
|
|
2371
2544
|
break;
|
|
2372
2545
|
default: {
|
|
2373
|
-
arg =
|
|
2546
|
+
arg = truncate2(JSON.stringify(input), 80);
|
|
2374
2547
|
}
|
|
2375
2548
|
}
|
|
2376
2549
|
return { label, arg };
|
|
@@ -2412,7 +2585,7 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
2412
2585
|
}
|
|
2413
2586
|
const MAX_LINE_WIDTH = 200;
|
|
2414
2587
|
const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
2415
|
-
const shown = visible.map((l) =>
|
|
2588
|
+
const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
|
|
2416
2589
|
const extra = lines.length - shown.length;
|
|
2417
2590
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2418
2591
|
/* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
@@ -2450,8 +2623,8 @@ function ToolUseLine({ use, result }) {
|
|
|
2450
2623
|
const { label, arg } = toolHeader(use);
|
|
2451
2624
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2452
2625
|
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2453
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2454
|
-
/* @__PURE__ */ jsxs9(Text9, { color: "
|
|
2626
|
+
/* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u25CF " }),
|
|
2627
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "white", children: [
|
|
2455
2628
|
label,
|
|
2456
2629
|
" "
|
|
2457
2630
|
] }),
|
|
@@ -2462,10 +2635,16 @@ function ToolUseLine({ use, result }) {
|
|
|
2462
2635
|
result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
|
|
2463
2636
|
] });
|
|
2464
2637
|
}
|
|
2465
|
-
function
|
|
2638
|
+
var UserMessage = memo2(function UserMessage2({ msg }) {
|
|
2639
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
2640
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u276F " }),
|
|
2641
|
+
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2642
|
+
] });
|
|
2643
|
+
});
|
|
2644
|
+
var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
|
|
2466
2645
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
2467
2646
|
msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
|
|
2468
|
-
/* @__PURE__ */ jsx9(Text9, { color: "
|
|
2647
|
+
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
2469
2648
|
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2470
2649
|
] }),
|
|
2471
2650
|
msg.tool_uses?.map((u) => {
|
|
@@ -2477,14 +2656,16 @@ function AssistantMessage({ msg }) {
|
|
|
2477
2656
|
msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
|
|
2478
2657
|
] }) })
|
|
2479
2658
|
] });
|
|
2480
|
-
}
|
|
2659
|
+
});
|
|
2481
2660
|
function summarizeInput(input) {
|
|
2482
2661
|
if (!input || typeof input !== "object") return "";
|
|
2483
2662
|
const obj = input;
|
|
2484
2663
|
const priority = ["path", "file_path", "command", "pattern", "query"];
|
|
2485
2664
|
for (const k of priority) {
|
|
2486
2665
|
const v = obj[k];
|
|
2487
|
-
if (typeof v === "string" && v.length > 0)
|
|
2666
|
+
if (typeof v === "string" && v.length > 0) {
|
|
2667
|
+
return `${k}: ${v.length > 120 ? v.slice(0, 120) + "\u2026" : v}`;
|
|
2668
|
+
}
|
|
2488
2669
|
}
|
|
2489
2670
|
const first = Object.entries(obj).find(([, v]) => typeof v === "string");
|
|
2490
2671
|
if (first) {
|
|
@@ -2509,7 +2690,7 @@ function PermissionPrompt({ req, cursor }) {
|
|
|
2509
2690
|
/* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
|
|
2510
2691
|
"?"
|
|
2511
2692
|
] }) }),
|
|
2512
|
-
summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
|
|
2693
|
+
summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "truncate", dimColor: true, children: summary }) }),
|
|
2513
2694
|
/* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
|
|
2514
2695
|
i === cursor ? "\u276F " : " ",
|
|
2515
2696
|
i + 1,
|
|
@@ -2540,10 +2721,7 @@ function ChatView({
|
|
|
2540
2721
|
] }, i))
|
|
2541
2722
|
] }),
|
|
2542
2723
|
messages.map(
|
|
2543
|
-
(msg, i) => msg.role === "user" ? /* @__PURE__ */
|
|
2544
|
-
/* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
|
|
2545
|
-
/* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
|
|
2546
|
-
] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
|
|
2724
|
+
(msg, i) => msg.role === "user" ? /* @__PURE__ */ jsx9(UserMessage, { msg }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
|
|
2547
2725
|
),
|
|
2548
2726
|
thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
|
|
2549
2727
|
streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
|
|
@@ -2789,6 +2967,33 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2789
2967
|
init_config();
|
|
2790
2968
|
import { useInput } from "ink";
|
|
2791
2969
|
var EFFORTS = ["low", "medium", "high"];
|
|
2970
|
+
var PASTE_CHIP_LINES = 4;
|
|
2971
|
+
var PASTE_CHIP_CHARS = 200;
|
|
2972
|
+
var pasteStore = /* @__PURE__ */ new Map();
|
|
2973
|
+
var pasteCounter = 0;
|
|
2974
|
+
function clearPasteStore() {
|
|
2975
|
+
pasteStore.clear();
|
|
2976
|
+
pasteCounter = 0;
|
|
2977
|
+
}
|
|
2978
|
+
function expandPastes(text) {
|
|
2979
|
+
let out = text;
|
|
2980
|
+
for (const [chip, full] of pasteStore) out = out.split(chip).join(full);
|
|
2981
|
+
return out;
|
|
2982
|
+
}
|
|
2983
|
+
function stripControls(chunk) {
|
|
2984
|
+
return chunk.replace(/\x1b\[20[01]~/g, "").replace(/\t/g, " ").replace(/[\x00-\x09\x0b-\x1f\x7f]/g, "");
|
|
2985
|
+
}
|
|
2986
|
+
function sanitizePaste(chunk) {
|
|
2987
|
+
if (chunk.length <= 1) return chunk;
|
|
2988
|
+
const cleaned = stripControls(chunk).replace(/\r/g, "");
|
|
2989
|
+
const lines = cleaned.split("\n").length;
|
|
2990
|
+
if (lines > PASTE_CHIP_LINES || cleaned.length > PASTE_CHIP_CHARS) {
|
|
2991
|
+
const chip = `[Pasted #${++pasteCounter} \xB7 ${lines} line${lines === 1 ? "" : "s"}]`;
|
|
2992
|
+
pasteStore.set(chip, cleaned);
|
|
2993
|
+
return chip;
|
|
2994
|
+
}
|
|
2995
|
+
return cleaned.replace(/\n/g, " ");
|
|
2996
|
+
}
|
|
2792
2997
|
function useKeyboard(opts) {
|
|
2793
2998
|
const {
|
|
2794
2999
|
exit,
|
|
@@ -2844,6 +3049,7 @@ function useKeyboard(opts) {
|
|
|
2844
3049
|
setActiveToolResults([]);
|
|
2845
3050
|
setError(null);
|
|
2846
3051
|
setNotice(null);
|
|
3052
|
+
clearPasteStore();
|
|
2847
3053
|
}
|
|
2848
3054
|
const effort = cfg.effort ?? "medium";
|
|
2849
3055
|
useInput((char, key) => {
|
|
@@ -3025,6 +3231,7 @@ function useKeyboard(opts) {
|
|
|
3025
3231
|
return;
|
|
3026
3232
|
}
|
|
3027
3233
|
if (paletteOpen && key.escape) {
|
|
3234
|
+
clearPasteStore();
|
|
3028
3235
|
setInput(() => "");
|
|
3029
3236
|
setPaletteCursor(() => 0);
|
|
3030
3237
|
return;
|
|
@@ -3080,19 +3287,21 @@ function useKeyboard(opts) {
|
|
|
3080
3287
|
}
|
|
3081
3288
|
} else if (trimmed) {
|
|
3082
3289
|
setNotice(null);
|
|
3290
|
+
const message = expandPastes(trimmed);
|
|
3083
3291
|
if (!agentHistory.length && cfg.model) {
|
|
3084
3292
|
const id = sessionId;
|
|
3085
3293
|
const model = cfg.model;
|
|
3086
3294
|
void (async () => {
|
|
3087
3295
|
try {
|
|
3088
|
-
const title = await summarizeMessage(model,
|
|
3089
|
-
persistSession(id, [{ role: "user", content:
|
|
3296
|
+
const title = await summarizeMessage(model, message);
|
|
3297
|
+
persistSession(id, [{ role: "user", content: message }], title);
|
|
3090
3298
|
} catch {
|
|
3091
3299
|
}
|
|
3092
3300
|
})();
|
|
3093
3301
|
}
|
|
3094
|
-
sendMessage(
|
|
3302
|
+
sendMessage(message);
|
|
3095
3303
|
}
|
|
3304
|
+
clearPasteStore();
|
|
3096
3305
|
setInput(() => "");
|
|
3097
3306
|
setPaletteCursor(() => 0);
|
|
3098
3307
|
return;
|
|
@@ -3101,13 +3310,22 @@ function useKeyboard(opts) {
|
|
|
3101
3310
|
setInput((s) => {
|
|
3102
3311
|
setPaletteCursor(() => 0);
|
|
3103
3312
|
setFilePickerCursor(() => 0);
|
|
3313
|
+
let match = "";
|
|
3314
|
+
for (const chip of pasteStore.keys()) {
|
|
3315
|
+
if (s.endsWith(chip) && chip.length > match.length) match = chip;
|
|
3316
|
+
}
|
|
3317
|
+
if (match) {
|
|
3318
|
+
pasteStore.delete(match);
|
|
3319
|
+
return s.slice(0, -match.length);
|
|
3320
|
+
}
|
|
3104
3321
|
return s.slice(0, -1);
|
|
3105
3322
|
});
|
|
3106
3323
|
} else if (char && !key.ctrl && !key.meta && !key.tab) {
|
|
3107
|
-
|
|
3324
|
+
const text = sanitizePaste(char);
|
|
3325
|
+
if (text) setInput((s) => {
|
|
3108
3326
|
setPaletteCursor(() => 0);
|
|
3109
3327
|
setFilePickerCursor(() => 0);
|
|
3110
|
-
return s +
|
|
3328
|
+
return s + text;
|
|
3111
3329
|
});
|
|
3112
3330
|
}
|
|
3113
3331
|
}
|
|
@@ -3262,8 +3480,7 @@ function App() {
|
|
|
3262
3480
|
return Math.round(used / activeCtx * 100);
|
|
3263
3481
|
})();
|
|
3264
3482
|
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
3265
|
-
/* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error }),
|
|
3266
|
-
updateAvailable && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
|
|
3483
|
+
/* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
|
|
3267
3484
|
state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
|
|
3268
3485
|
agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
|
|
3269
3486
|
ChatView,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Terminal AI coding agent powered by Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"start": "tsx src/cli.tsx",
|
|
18
18
|
"eval": "tsx src/cli.tsx eval",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
20
22
|
"dev": "tsx watch src/cli.tsx",
|
|
21
23
|
"build": "tsup",
|
|
22
24
|
"postbuild": "node -e \"if(process.platform!=='win32')require('fs').chmodSync('dist/cli.js',0o755)\"",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
],
|
|
37
39
|
"license": "MIT",
|
|
38
40
|
"dependencies": {
|
|
41
|
+
"cli-highlight": "^2.1.11",
|
|
39
42
|
"execa": "^9.0.0",
|
|
40
43
|
"ink": "^5.0.0",
|
|
41
44
|
"ollama": "^0.5.0",
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"@types/react": "^18.3.0",
|
|
49
52
|
"tsup": "^8.5.1",
|
|
50
53
|
"tsx": "^4.19.0",
|
|
51
|
-
"typescript": "^5.7.0"
|
|
54
|
+
"typescript": "^5.7.0",
|
|
55
|
+
"vitest": "^4.1.9"
|
|
52
56
|
}
|
|
53
57
|
}
|