miii-agent 0.1.24 → 0.1.26
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 +6 -0
- package/dist/cli.js +288 -84
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/miii-agent"><img src="https://img.shields.io/npm/v/miii-agent" alt="npm version"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/miii-agent"><img src="https://img.shields.io/npm/dt/miii-agent" alt="npm total downloads"></a>
|
|
11
12
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license"></a>
|
|
12
13
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="node version"></a>
|
|
13
14
|
<a href="https://ollama.com"><img src="https://img.shields.io/badge/powered%20by-Ollama-black" alt="powered by Ollama"></a>
|
|
@@ -68,6 +69,10 @@ It doesn't just chat, either — it decomposes the problem, calls tools, and che
|
|
|
68
69
|
miii doctor # grade every installed model
|
|
69
70
|
miii doctor qwen2.5-coder:7b # grade one
|
|
70
71
|
```
|
|
72
|
+
- **🖼️ Paste images** — copy a screenshot and hit `Ctrl+V` to attach it to your message, or paste an image file path. Great for "why does this UI look broken?" or reading an error screenshot. **Needs a vision-capable model** (`llava`, `llama3.2-vision`, `qwen2-vl`, …) — text-only models silently ignore the image.
|
|
73
|
+
```bash
|
|
74
|
+
ollama pull llava # or llama3.2-vision
|
|
75
|
+
```
|
|
71
76
|
- **💧 Lossless output spill** — that 50K-line test log won't get truncated and leave the model guessing. miii spills the full output to disk and lets the model page through it. Nothing is ever lost.
|
|
72
77
|
- **🔒 Permission-gated tools** — you approve what the agent can touch; "always" approvals persist. File tools are confined to your working directory.
|
|
73
78
|
- **📄 `MIII.md`** — drop one in your repo to teach miii your conventions, build/test commands, and do's & don'ts. Same idea as `CLAUDE.md`, read every turn.
|
|
@@ -96,6 +101,7 @@ File tools (`read_file`, `write_file`, `edit_file`) reject `../` traversal and a
|
|
|
96
101
|
|-----|--------|
|
|
97
102
|
| `Enter` | Send prompt |
|
|
98
103
|
| `@filename` | Attach file to context |
|
|
104
|
+
| `Ctrl+V` | Paste clipboard image (needs a vision model) |
|
|
99
105
|
| `/models` | Switch active model |
|
|
100
106
|
| `/clear` | Reset conversation |
|
|
101
107
|
| `Esc` | Stop generation or tool run |
|
package/dist/cli.js
CHANGED
|
@@ -129,6 +129,15 @@ function isConnectionError(err) {
|
|
|
129
129
|
const msg = err instanceof Error ? err.message : String(err);
|
|
130
130
|
return msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("connect");
|
|
131
131
|
}
|
|
132
|
+
function isNoVisionError(err) {
|
|
133
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
134
|
+
return msg.includes("does not support image") || msg.includes("image input");
|
|
135
|
+
}
|
|
136
|
+
function noVisionError(model) {
|
|
137
|
+
return new Error(
|
|
138
|
+
`"${model}" can't read images \u2014 it has no vision support. Switch to a vision-capable model (e.g. llava, llama3.2-vision, qwen2-vl) with /models, then resend. Pull one with: ollama pull llava`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
132
141
|
async function listModels(entry) {
|
|
133
142
|
try {
|
|
134
143
|
const { models } = await makeClient(entry).list();
|
|
@@ -208,6 +217,7 @@ async function* chat(entry, model, messages, tools, opts) {
|
|
|
208
217
|
}
|
|
209
218
|
} catch (err) {
|
|
210
219
|
if (signal?.aborted) return;
|
|
220
|
+
if (isNoVisionError(err)) throw noVisionError(model);
|
|
211
221
|
if (isConnectionError(err)) {
|
|
212
222
|
throw new Error(NOT_RUNNING);
|
|
213
223
|
}
|
|
@@ -233,6 +243,7 @@ async function* chat(entry, model, messages, tools, opts) {
|
|
|
233
243
|
}
|
|
234
244
|
} catch (err) {
|
|
235
245
|
if (opts?.signal?.aborted) return;
|
|
246
|
+
if (isNoVisionError(err)) throw noVisionError(model);
|
|
236
247
|
if (isConnectionError(err)) {
|
|
237
248
|
throw new Error(NOT_RUNNING);
|
|
238
249
|
}
|
|
@@ -401,6 +412,10 @@ async function* chat2(entry, model, messages, tools, opts) {
|
|
|
401
412
|
const delta = choices[0].delta ?? {};
|
|
402
413
|
const finishReason = choices[0].finish_reason;
|
|
403
414
|
if (finishReason) lastFinishReason = finishReason;
|
|
415
|
+
const reasoning = delta.reasoning_content ?? (typeof delta.reasoning === "string" ? delta.reasoning : void 0) ?? delta.reasoning?.content;
|
|
416
|
+
if (reasoning) {
|
|
417
|
+
yield { content: "", thinking: reasoning, done: false };
|
|
418
|
+
}
|
|
404
419
|
if (delta.content) {
|
|
405
420
|
yield { content: delta.content, done: false };
|
|
406
421
|
}
|
|
@@ -1425,7 +1440,9 @@ function toOllamaMessages(history, system) {
|
|
|
1425
1440
|
const out = [{ role: "system", content: system }];
|
|
1426
1441
|
for (const msg of history) {
|
|
1427
1442
|
if (typeof msg.content === "string") {
|
|
1428
|
-
|
|
1443
|
+
const om = { role: msg.role === "system" ? "system" : msg.role, content: msg.content };
|
|
1444
|
+
if (msg.role === "user" && msg.images && msg.images.length > 0) om.images = msg.images;
|
|
1445
|
+
out.push(om);
|
|
1429
1446
|
continue;
|
|
1430
1447
|
}
|
|
1431
1448
|
if (msg.role === "assistant") {
|
|
@@ -1611,7 +1628,11 @@ async function* runAgent(opts) {
|
|
|
1611
1628
|
const effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
|
|
1612
1629
|
const history = [
|
|
1613
1630
|
...opts.history,
|
|
1614
|
-
{
|
|
1631
|
+
{
|
|
1632
|
+
role: "user",
|
|
1633
|
+
content: opts.userText,
|
|
1634
|
+
...opts.images && opts.images.length > 0 ? { images: opts.images } : {}
|
|
1635
|
+
}
|
|
1615
1636
|
];
|
|
1616
1637
|
let promptTokens = 0;
|
|
1617
1638
|
let evalTokens = 0;
|
|
@@ -1829,14 +1850,14 @@ var init_loop = __esm({
|
|
|
1829
1850
|
});
|
|
1830
1851
|
|
|
1831
1852
|
// eval/runner.ts
|
|
1832
|
-
import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as
|
|
1833
|
-
import { dirname as dirname3, join as
|
|
1834
|
-
import { tmpdir } from "os";
|
|
1853
|
+
import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "fs";
|
|
1854
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
1855
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1835
1856
|
async function runScenario(model, s) {
|
|
1836
|
-
const dir = mkdtempSync(
|
|
1857
|
+
const dir = mkdtempSync(join10(tmpdir2(), "miii-eval-"));
|
|
1837
1858
|
const prevCwd = process.cwd();
|
|
1838
1859
|
for (const [rel, content] of Object.entries(s.files ?? {})) {
|
|
1839
|
-
const abs =
|
|
1860
|
+
const abs = join10(dir, rel);
|
|
1840
1861
|
mkdirSync6(dirname3(abs), { recursive: true });
|
|
1841
1862
|
writeFileSync7(abs, content, "utf-8");
|
|
1842
1863
|
}
|
|
@@ -1876,7 +1897,7 @@ async function runScenario(model, s) {
|
|
|
1876
1897
|
r.durationMs = Date.now() - start;
|
|
1877
1898
|
if (r.error) {
|
|
1878
1899
|
r.reason = `loop error: ${r.error}`;
|
|
1879
|
-
|
|
1900
|
+
rmSync4(dir, { recursive: true, force: true });
|
|
1880
1901
|
return r;
|
|
1881
1902
|
}
|
|
1882
1903
|
try {
|
|
@@ -1886,7 +1907,7 @@ async function runScenario(model, s) {
|
|
|
1886
1907
|
} catch (err) {
|
|
1887
1908
|
r.reason = `check threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
1888
1909
|
}
|
|
1889
|
-
|
|
1910
|
+
rmSync4(dir, { recursive: true, force: true });
|
|
1890
1911
|
return r;
|
|
1891
1912
|
}
|
|
1892
1913
|
var autoYes;
|
|
@@ -1899,13 +1920,13 @@ var init_runner = __esm({
|
|
|
1899
1920
|
});
|
|
1900
1921
|
|
|
1901
1922
|
// eval/scenarios.ts
|
|
1902
|
-
import { readFileSync as
|
|
1903
|
-
import { join as
|
|
1923
|
+
import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
|
|
1924
|
+
import { join as join11 } from "path";
|
|
1904
1925
|
var read, scenarios;
|
|
1905
1926
|
var init_scenarios = __esm({
|
|
1906
1927
|
"eval/scenarios.ts"() {
|
|
1907
1928
|
"use strict";
|
|
1908
|
-
read = (dir, f) =>
|
|
1929
|
+
read = (dir, f) => existsSync8(join11(dir, f)) ? readFileSync9(join11(dir, f), "utf-8") : null;
|
|
1909
1930
|
scenarios = [
|
|
1910
1931
|
{
|
|
1911
1932
|
name: "edit-exact-string",
|
|
@@ -2055,7 +2076,7 @@ init_client();
|
|
|
2055
2076
|
init_config();
|
|
2056
2077
|
import { useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
|
|
2057
2078
|
import { Box as Box13, Text as Text13, useApp } from "ink";
|
|
2058
|
-
import { homedir as
|
|
2079
|
+
import { homedir as homedir7 } from "os";
|
|
2059
2080
|
import { sep as sep2 } from "path";
|
|
2060
2081
|
|
|
2061
2082
|
// src/ui/WelcomeBlock.tsx
|
|
@@ -2342,7 +2363,8 @@ function persistSession(id, messages, title) {
|
|
|
2342
2363
|
};
|
|
2343
2364
|
const lines = [JSON.stringify({ type: "meta", ...meta })];
|
|
2344
2365
|
for (const message of messages) {
|
|
2345
|
-
|
|
2366
|
+
const { images: _img, ...rest } = message;
|
|
2367
|
+
lines.push(JSON.stringify({ type: "message", message: rest }));
|
|
2346
2368
|
}
|
|
2347
2369
|
writeFileSync2(sessionPath(id), lines.join("\n") + "\n", "utf-8");
|
|
2348
2370
|
}
|
|
@@ -3102,6 +3124,83 @@ function renderMarkdownStreaming(content) {
|
|
|
3102
3124
|
// src/ui/ThinkingBlock.tsx
|
|
3103
3125
|
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
3104
3126
|
import { Box as Box8, Text as Text8 } from "ink";
|
|
3127
|
+
|
|
3128
|
+
// src/ui/layout.ts
|
|
3129
|
+
function formatTokens(n) {
|
|
3130
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
|
|
3131
|
+
return String(n);
|
|
3132
|
+
}
|
|
3133
|
+
function formatDuration(ms) {
|
|
3134
|
+
const totalSec = ms / 1e3;
|
|
3135
|
+
if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
|
|
3136
|
+
const m = Math.floor(totalSec / 60);
|
|
3137
|
+
const s = Math.round(totalSec - m * 60);
|
|
3138
|
+
return `${m}m ${s}s`;
|
|
3139
|
+
}
|
|
3140
|
+
function countLines(s) {
|
|
3141
|
+
if (!s) return 0;
|
|
3142
|
+
return s.split("\n").length;
|
|
3143
|
+
}
|
|
3144
|
+
function truncate2(s, max) {
|
|
3145
|
+
if (s.length <= max) return s;
|
|
3146
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
3147
|
+
}
|
|
3148
|
+
function clipTail(rendered, max) {
|
|
3149
|
+
const lines = rendered.split("\n");
|
|
3150
|
+
if (lines.length <= max) return { text: rendered, clipped: 0 };
|
|
3151
|
+
return { text: lines.slice(-max).join("\n"), clipped: lines.length - max };
|
|
3152
|
+
}
|
|
3153
|
+
function clipTailVisual(content, maxRows, width) {
|
|
3154
|
+
const w = Math.max(1, width);
|
|
3155
|
+
const lines = content.split("\n");
|
|
3156
|
+
const visualRows = (line) => Math.max(1, Math.ceil(line.length / w));
|
|
3157
|
+
let rows = 0;
|
|
3158
|
+
let start = lines.length;
|
|
3159
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3160
|
+
const h = visualRows(lines[i]);
|
|
3161
|
+
if (rows + h > maxRows && start < lines.length) break;
|
|
3162
|
+
rows += h;
|
|
3163
|
+
start = i;
|
|
3164
|
+
}
|
|
3165
|
+
if (start === 0) return { text: content, clipped: 0 };
|
|
3166
|
+
return { text: lines.slice(start).join("\n"), clipped: start };
|
|
3167
|
+
}
|
|
3168
|
+
function liveFrameRows() {
|
|
3169
|
+
const rows = process.stdout.rows ?? 24;
|
|
3170
|
+
return Math.max(6, rows - 8);
|
|
3171
|
+
}
|
|
3172
|
+
var COLLAPSED_LINES = 3;
|
|
3173
|
+
function estimateToolRows(use, result) {
|
|
3174
|
+
const input = use.input ?? {};
|
|
3175
|
+
const noErr = !result?.is_error;
|
|
3176
|
+
if (use.name === "write_file" && noErr) {
|
|
3177
|
+
const total = countLines(String(input.content ?? ""));
|
|
3178
|
+
const shown = Math.min(total, COLLAPSED_LINES);
|
|
3179
|
+
return 2 + shown + (total > shown ? 1 : 0);
|
|
3180
|
+
}
|
|
3181
|
+
if (use.name === "edit_file" && noErr) {
|
|
3182
|
+
const total = countLines(String(input.old_str ?? "")) + countLines(String(input.new_str ?? ""));
|
|
3183
|
+
const shown = Math.min(total, COLLAPSED_LINES);
|
|
3184
|
+
return 2 + shown + (total > shown ? 1 : 0);
|
|
3185
|
+
}
|
|
3186
|
+
let rows = 1;
|
|
3187
|
+
if (result) {
|
|
3188
|
+
const lines = (result.content ?? "").split("\n");
|
|
3189
|
+
const multi = (use.name === "run_bash" || use.name === "grep" || use.name === "glob" || result.is_error) && lines.length > 1;
|
|
3190
|
+
if (multi) {
|
|
3191
|
+
const shown = Math.min(lines.length, COLLAPSED_LINES);
|
|
3192
|
+
rows += 1 + shown + (lines.length > shown ? 1 : 0);
|
|
3193
|
+
} else {
|
|
3194
|
+
rows += 1;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
return rows;
|
|
3198
|
+
}
|
|
3199
|
+
function contentWidth() {
|
|
3200
|
+
return Math.max(20, (process.stdout.columns ?? 80) - 4);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/ui/ThinkingBlock.tsx
|
|
3105
3204
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3106
3205
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3107
3206
|
var globalThinkingVisible = false;
|
|
@@ -3142,10 +3241,13 @@ function ThinkingBlock({ content }) {
|
|
|
3142
3241
|
] })
|
|
3143
3242
|
] }),
|
|
3144
3243
|
visible && content ? (() => {
|
|
3145
|
-
const
|
|
3146
|
-
const
|
|
3147
|
-
const
|
|
3148
|
-
return /* @__PURE__ */
|
|
3244
|
+
const width = Math.max(20, contentWidth() - 2);
|
|
3245
|
+
const budget = Math.max(4, liveFrameRows() - 1);
|
|
3246
|
+
const { text, clipped } = clipTailVisual(content, budget, width);
|
|
3247
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
|
|
3248
|
+
clipped > 0 && /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: `\u2191 ${clipped} earlier line${clipped === 1 ? "" : "s"} above` }),
|
|
3249
|
+
/* @__PURE__ */ jsx8(Box8, { width, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, wrap: "wrap", children: text }) })
|
|
3250
|
+
] });
|
|
3149
3251
|
})() : null
|
|
3150
3252
|
] });
|
|
3151
3253
|
}
|
|
@@ -3156,6 +3258,7 @@ var EMPTY_STATE_HINTS = [
|
|
|
3156
3258
|
"\u2022 /models \u2014 switch model or effort",
|
|
3157
3259
|
"\u2022 /new \u2014 start a new chat",
|
|
3158
3260
|
"\u2022 /sessions \u2014 view saved chats",
|
|
3261
|
+
"\u2022 ctrl+v \u2014 paste an image (needs a vision model)",
|
|
3159
3262
|
"\u2022 ctrl+t \u2014 toggle thinking"
|
|
3160
3263
|
];
|
|
3161
3264
|
var EMPTY_STATE_TITLE = "Ask anything, or try:";
|
|
@@ -3188,66 +3291,6 @@ function useToolExpanded() {
|
|
|
3188
3291
|
return expanded;
|
|
3189
3292
|
}
|
|
3190
3293
|
|
|
3191
|
-
// src/ui/layout.ts
|
|
3192
|
-
function formatTokens(n) {
|
|
3193
|
-
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
|
|
3194
|
-
return String(n);
|
|
3195
|
-
}
|
|
3196
|
-
function formatDuration(ms) {
|
|
3197
|
-
const totalSec = ms / 1e3;
|
|
3198
|
-
if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
|
|
3199
|
-
const m = Math.floor(totalSec / 60);
|
|
3200
|
-
const s = Math.round(totalSec - m * 60);
|
|
3201
|
-
return `${m}m ${s}s`;
|
|
3202
|
-
}
|
|
3203
|
-
function countLines(s) {
|
|
3204
|
-
if (!s) return 0;
|
|
3205
|
-
return s.split("\n").length;
|
|
3206
|
-
}
|
|
3207
|
-
function truncate2(s, max) {
|
|
3208
|
-
if (s.length <= max) return s;
|
|
3209
|
-
return s.slice(0, max - 1) + "\u2026";
|
|
3210
|
-
}
|
|
3211
|
-
function clipTail(rendered, max) {
|
|
3212
|
-
const lines = rendered.split("\n");
|
|
3213
|
-
if (lines.length <= max) return { text: rendered, clipped: 0 };
|
|
3214
|
-
return { text: lines.slice(-max).join("\n"), clipped: lines.length - max };
|
|
3215
|
-
}
|
|
3216
|
-
function liveFrameRows() {
|
|
3217
|
-
const rows = process.stdout.rows ?? 24;
|
|
3218
|
-
return Math.max(6, rows - 8);
|
|
3219
|
-
}
|
|
3220
|
-
var COLLAPSED_LINES = 3;
|
|
3221
|
-
function estimateToolRows(use, result) {
|
|
3222
|
-
const input = use.input ?? {};
|
|
3223
|
-
const noErr = !result?.is_error;
|
|
3224
|
-
if (use.name === "write_file" && noErr) {
|
|
3225
|
-
const total = countLines(String(input.content ?? ""));
|
|
3226
|
-
const shown = Math.min(total, COLLAPSED_LINES);
|
|
3227
|
-
return 2 + shown + (total > shown ? 1 : 0);
|
|
3228
|
-
}
|
|
3229
|
-
if (use.name === "edit_file" && noErr) {
|
|
3230
|
-
const total = countLines(String(input.old_str ?? "")) + countLines(String(input.new_str ?? ""));
|
|
3231
|
-
const shown = Math.min(total, COLLAPSED_LINES);
|
|
3232
|
-
return 2 + shown + (total > shown ? 1 : 0);
|
|
3233
|
-
}
|
|
3234
|
-
let rows = 1;
|
|
3235
|
-
if (result) {
|
|
3236
|
-
const lines = (result.content ?? "").split("\n");
|
|
3237
|
-
const multi = (use.name === "run_bash" || use.name === "grep" || use.name === "glob" || result.is_error) && lines.length > 1;
|
|
3238
|
-
if (multi) {
|
|
3239
|
-
const shown = Math.min(lines.length, COLLAPSED_LINES);
|
|
3240
|
-
rows += 1 + shown + (lines.length > shown ? 1 : 0);
|
|
3241
|
-
} else {
|
|
3242
|
-
rows += 1;
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
return rows;
|
|
3246
|
-
}
|
|
3247
|
-
function contentWidth() {
|
|
3248
|
-
return Math.max(20, (process.stdout.columns ?? 80) - 4);
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
3294
|
// src/ui/ToolBlock.tsx
|
|
3252
3295
|
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3253
3296
|
var COLLAPSED_LINES2 = 3;
|
|
@@ -3661,7 +3704,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
3661
3704
|
setPendingPermission(null);
|
|
3662
3705
|
req.resolve(answers[cursor]);
|
|
3663
3706
|
}
|
|
3664
|
-
async function sendMessage(text) {
|
|
3707
|
+
async function sendMessage(text, images) {
|
|
3665
3708
|
if (busyRef.current || !model) return;
|
|
3666
3709
|
busyRef.current = true;
|
|
3667
3710
|
setBusy(true);
|
|
@@ -3719,6 +3762,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
3719
3762
|
cwd: process.cwd(),
|
|
3720
3763
|
history: agentHistory,
|
|
3721
3764
|
userText: text,
|
|
3765
|
+
images,
|
|
3722
3766
|
permissions: { ask: askPermission },
|
|
3723
3767
|
signal: controller.signal,
|
|
3724
3768
|
num_ctx: activeCtx ?? void 0
|
|
@@ -3847,17 +3891,142 @@ function useAgentRunner(model, activeCtx) {
|
|
|
3847
3891
|
};
|
|
3848
3892
|
}
|
|
3849
3893
|
|
|
3894
|
+
// src/ui/hooks/useKeyboard.ts
|
|
3895
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
3896
|
+
import { basename, join as join9 } from "path";
|
|
3897
|
+
import { homedir as homedir6 } from "os";
|
|
3898
|
+
import { useInput, useStdout } from "ink";
|
|
3899
|
+
|
|
3900
|
+
// src/ui/clipboard.ts
|
|
3901
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3902
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
3903
|
+
import { join as join8 } from "path";
|
|
3904
|
+
import { tmpdir } from "os";
|
|
3905
|
+
function safeRm(p) {
|
|
3906
|
+
try {
|
|
3907
|
+
rmSync3(p, { force: true });
|
|
3908
|
+
} catch {
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
function consume(p) {
|
|
3912
|
+
try {
|
|
3913
|
+
const b64 = readFileSync7(p).toString("base64");
|
|
3914
|
+
return b64.length > 0 ? b64 : null;
|
|
3915
|
+
} catch {
|
|
3916
|
+
return null;
|
|
3917
|
+
} finally {
|
|
3918
|
+
safeRm(p);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
function readMac(out) {
|
|
3922
|
+
try {
|
|
3923
|
+
execFileSync2("pngpaste", [out], { stdio: "ignore" });
|
|
3924
|
+
if (existsSync6(out)) return consume(out);
|
|
3925
|
+
} catch {
|
|
3926
|
+
}
|
|
3927
|
+
const png = "\xABclass PNGf\xBB";
|
|
3928
|
+
const script = [
|
|
3929
|
+
"try",
|
|
3930
|
+
`set f to open for access (POSIX file "${out}") with write permission`,
|
|
3931
|
+
`set theData to (the clipboard as ${png})`,
|
|
3932
|
+
"write theData to f",
|
|
3933
|
+
"close access f",
|
|
3934
|
+
"on error",
|
|
3935
|
+
"try",
|
|
3936
|
+
"close access f",
|
|
3937
|
+
"end try",
|
|
3938
|
+
'return "NOIMG"',
|
|
3939
|
+
"end try"
|
|
3940
|
+
];
|
|
3941
|
+
try {
|
|
3942
|
+
const res = execFileSync2("osascript", script.flatMap((s) => ["-e", s]), { encoding: "utf8" });
|
|
3943
|
+
if (res.includes("NOIMG")) {
|
|
3944
|
+
safeRm(out);
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
if (existsSync6(out)) return consume(out);
|
|
3948
|
+
} catch {
|
|
3949
|
+
}
|
|
3950
|
+
safeRm(out);
|
|
3951
|
+
return null;
|
|
3952
|
+
}
|
|
3953
|
+
function readLinux(out) {
|
|
3954
|
+
for (const [cmd2, args2] of [
|
|
3955
|
+
["wl-paste", ["--type", "image/png"]],
|
|
3956
|
+
["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
|
|
3957
|
+
]) {
|
|
3958
|
+
try {
|
|
3959
|
+
const buf = execFileSync2(cmd2, args2, { maxBuffer: 64 * 1024 * 1024 });
|
|
3960
|
+
if (buf.length > 0) return buf.toString("base64");
|
|
3961
|
+
} catch {
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
return null;
|
|
3965
|
+
}
|
|
3966
|
+
function readWindows(out) {
|
|
3967
|
+
const ps = [
|
|
3968
|
+
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing;",
|
|
3969
|
+
"$img = [System.Windows.Forms.Clipboard]::GetImage();",
|
|
3970
|
+
// Single-quoted PS string → backslashes are literal, no escaping needed.
|
|
3971
|
+
`if ($img -ne $null) { $img.Save('${out}', [System.Drawing.Imaging.ImageFormat]::Png); 'OK' } else { 'NOIMG' }`
|
|
3972
|
+
].join(" ");
|
|
3973
|
+
try {
|
|
3974
|
+
const res = execFileSync2(
|
|
3975
|
+
"powershell",
|
|
3976
|
+
["-NoProfile", "-NonInteractive", "-STA", "-Command", ps],
|
|
3977
|
+
{ encoding: "utf8" }
|
|
3978
|
+
);
|
|
3979
|
+
if (res.includes("NOIMG")) {
|
|
3980
|
+
safeRm(out);
|
|
3981
|
+
return null;
|
|
3982
|
+
}
|
|
3983
|
+
if (existsSync6(out)) return consume(out);
|
|
3984
|
+
} catch {
|
|
3985
|
+
}
|
|
3986
|
+
safeRm(out);
|
|
3987
|
+
return null;
|
|
3988
|
+
}
|
|
3989
|
+
function readClipboardImage() {
|
|
3990
|
+
const out = join8(tmpdir(), `miii-clip-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
|
|
3991
|
+
if (process.platform === "darwin") return readMac(out);
|
|
3992
|
+
if (process.platform === "linux") return readLinux(out);
|
|
3993
|
+
if (process.platform === "win32") return readWindows(out);
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3850
3997
|
// src/ui/hooks/useKeyboard.ts
|
|
3851
3998
|
init_config();
|
|
3852
|
-
import { useInput } from "ink";
|
|
3853
3999
|
var EFFORTS = ["low", "medium", "high"];
|
|
3854
4000
|
var PASTE_CHIP_LINES = 4;
|
|
3855
4001
|
var PASTE_CHIP_CHARS = 200;
|
|
3856
4002
|
var pasteStore = /* @__PURE__ */ new Map();
|
|
3857
4003
|
var pasteCounter = 0;
|
|
4004
|
+
var imageStore = /* @__PURE__ */ new Map();
|
|
4005
|
+
var imageCounter = 0;
|
|
4006
|
+
var IMAGE_EXT_RE = /\.(png|jpe?g|webp|gif|bmp)$/i;
|
|
3858
4007
|
function clearPasteStore() {
|
|
3859
4008
|
pasteStore.clear();
|
|
3860
4009
|
pasteCounter = 0;
|
|
4010
|
+
imageStore.clear();
|
|
4011
|
+
imageCounter = 0;
|
|
4012
|
+
}
|
|
4013
|
+
function tryImagePaste(cleaned) {
|
|
4014
|
+
let p = cleaned.trim();
|
|
4015
|
+
if (p.startsWith('"') && p.endsWith('"') || p.startsWith("'") && p.endsWith("'")) {
|
|
4016
|
+
p = p.slice(1, -1);
|
|
4017
|
+
}
|
|
4018
|
+
p = p.replace(/\\ /g, " ");
|
|
4019
|
+
if (p.includes("\n") || !IMAGE_EXT_RE.test(p)) return null;
|
|
4020
|
+
if (p.startsWith("~/")) p = join9(homedir6(), p.slice(2));
|
|
4021
|
+
if (!existsSync7(p)) return null;
|
|
4022
|
+
try {
|
|
4023
|
+
const b64 = readFileSync8(p).toString("base64");
|
|
4024
|
+
const chip = `[Image #${++imageCounter} \xB7 ${basename(p)}]`;
|
|
4025
|
+
imageStore.set(chip, b64);
|
|
4026
|
+
return chip;
|
|
4027
|
+
} catch {
|
|
4028
|
+
return null;
|
|
4029
|
+
}
|
|
3861
4030
|
}
|
|
3862
4031
|
var inputHistory = [];
|
|
3863
4032
|
var historyIndex = -1;
|
|
@@ -3878,6 +4047,8 @@ function stripControls(chunk) {
|
|
|
3878
4047
|
function sanitizePaste(chunk) {
|
|
3879
4048
|
if (chunk.length <= 1) return chunk;
|
|
3880
4049
|
const cleaned = stripControls(chunk).replace(/\r/g, "");
|
|
4050
|
+
const imageChip = tryImagePaste(cleaned);
|
|
4051
|
+
if (imageChip) return imageChip;
|
|
3881
4052
|
const lines = cleaned.split("\n").length;
|
|
3882
4053
|
if (lines > PASTE_CHIP_LINES || cleaned.length > PASTE_CHIP_CHARS) {
|
|
3883
4054
|
const chip = `[Pasted #${++pasteCounter} \xB7 ${lines} line${lines === 1 ? "" : "s"}]`;
|
|
@@ -3935,6 +4106,10 @@ function useKeyboard(opts) {
|
|
|
3935
4106
|
setActiveToolResults,
|
|
3936
4107
|
setError
|
|
3937
4108
|
} = agent;
|
|
4109
|
+
const { write } = useStdout();
|
|
4110
|
+
function hardClear() {
|
|
4111
|
+
write("\x1B[2J\x1B[3J\x1B[H");
|
|
4112
|
+
}
|
|
3938
4113
|
function clearSession() {
|
|
3939
4114
|
setMessages(() => []);
|
|
3940
4115
|
setAgentHistory([]);
|
|
@@ -4108,6 +4283,19 @@ function useKeyboard(opts) {
|
|
|
4108
4283
|
}
|
|
4109
4284
|
if (state === "ready") {
|
|
4110
4285
|
if (busyRef.current) return;
|
|
4286
|
+
if (key.ctrl && char === "v") {
|
|
4287
|
+
const b64 = readClipboardImage();
|
|
4288
|
+
if (!b64) {
|
|
4289
|
+
setNotice("no image in clipboard");
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
const chip = `[Image #${++imageCounter} \xB7 clipboard]`;
|
|
4293
|
+
imageStore.set(chip, b64);
|
|
4294
|
+
historyIndex = -1;
|
|
4295
|
+
setInput((s) => s.slice(0, caret) + chip + s.slice(caret));
|
|
4296
|
+
setCaret((i) => i + chip.length);
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4111
4299
|
const paletteOpen = input.startsWith("/");
|
|
4112
4300
|
const matches2 = paletteOpen ? filteredCommands(input) : [];
|
|
4113
4301
|
const mention = !paletteOpen ? parseMention(input) : null;
|
|
@@ -4196,10 +4384,12 @@ function useKeyboard(opts) {
|
|
|
4196
4384
|
setCursor(() => Math.max(0, providers.findIndex((p) => p.name === cfg.provider)));
|
|
4197
4385
|
setState("providers");
|
|
4198
4386
|
} else if (trimmed === "/clear") {
|
|
4387
|
+
hardClear();
|
|
4199
4388
|
clearSession();
|
|
4200
4389
|
} else if (trimmed === "/new") {
|
|
4201
4390
|
if (agentHistory.length) setNotice("session saved");
|
|
4202
4391
|
setSessionId(newSessionId());
|
|
4392
|
+
hardClear();
|
|
4203
4393
|
clearSession();
|
|
4204
4394
|
} else if (trimmed === "/sessions") {
|
|
4205
4395
|
setSessions(listSessions());
|
|
@@ -4218,8 +4408,16 @@ function useKeyboard(opts) {
|
|
|
4218
4408
|
}
|
|
4219
4409
|
} else if (trimmed) {
|
|
4220
4410
|
setNotice(null);
|
|
4221
|
-
const
|
|
4222
|
-
|
|
4411
|
+
const images = [];
|
|
4412
|
+
let textPart = trimmed;
|
|
4413
|
+
for (const [chip, b64] of imageStore) {
|
|
4414
|
+
if (textPart.includes(chip)) {
|
|
4415
|
+
images.push(b64);
|
|
4416
|
+
textPart = textPart.split(chip).join("").trim();
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
const message = expandPastes(textPart) || "Describe the attached image.";
|
|
4420
|
+
sendMessage(message, images.length ? images : void 0);
|
|
4223
4421
|
}
|
|
4224
4422
|
clearPasteStore();
|
|
4225
4423
|
setInput(() => "");
|
|
@@ -4253,8 +4451,14 @@ function useKeyboard(opts) {
|
|
|
4253
4451
|
for (const chip of pasteStore.keys()) {
|
|
4254
4452
|
if (before.endsWith(chip) && chip.length > match.length) match = chip;
|
|
4255
4453
|
}
|
|
4454
|
+
for (const chip of imageStore.keys()) {
|
|
4455
|
+
if (before.endsWith(chip) && chip.length > match.length) match = chip;
|
|
4456
|
+
}
|
|
4256
4457
|
const cut = match ? match.length : 1;
|
|
4257
|
-
if (match)
|
|
4458
|
+
if (match) {
|
|
4459
|
+
pasteStore.delete(match);
|
|
4460
|
+
imageStore.delete(match);
|
|
4461
|
+
}
|
|
4258
4462
|
setInput((s) => s.slice(0, caret - cut) + s.slice(caret));
|
|
4259
4463
|
setCaret((i) => Math.max(0, i - cut));
|
|
4260
4464
|
} else if (char && !key.ctrl && !key.meta && !key.tab) {
|
|
@@ -4310,7 +4514,7 @@ async function checkForUpdate() {
|
|
|
4310
4514
|
import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4311
4515
|
function App() {
|
|
4312
4516
|
const { exit } = useApp();
|
|
4313
|
-
const cwd = process.cwd().replace(
|
|
4517
|
+
const cwd = process.cwd().replace(homedir7(), "~").split(sep2).join("/");
|
|
4314
4518
|
const [cfg, setCfg] = useState5(loadConfig());
|
|
4315
4519
|
const [models, setModels] = useState5([]);
|
|
4316
4520
|
const [contexts, setContexts] = useState5(() => cfg.modelContexts ?? {});
|
package/package.json
CHANGED