u-foo 3.0.2 → 3.0.4
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/package.json +1 -1
- package/src/agents/prompts/native/toolDescriptions/readImage.js +23 -0
- package/src/code/context/assembler.js +17 -1
- package/src/code/context/planMode.js +3 -2
- package/src/code/context/promptLayers.js +7 -5
- package/src/code/context/reducers.js +35 -0
- package/src/code/context/transcriptSync.js +25 -5
- package/src/code/context/userNudge.js +49 -0
- package/src/code/dispatch.js +4 -0
- package/src/code/imageIngest.js +367 -0
- package/src/code/nativeRunner.js +63 -1
- package/src/code/providers/anthropicMessagesTransport.js +28 -1
- package/src/code/providers/index.js +1 -0
- package/src/code/providers/openaiChatTransport.js +19 -1
- package/src/code/providers/visionBlocks.js +110 -0
- package/src/code/tools/readImage.js +110 -0
- package/src/ui/format/index.js +55 -2
- package/src/ui/ink/MultilineInput.js +38 -2
- package/src/ui/ink/UcodeApp.js +74 -11
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { resolveWorkspacePath } = require("./common");
|
|
6
|
+
|
|
7
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
const EXT_MEDIA = Object.freeze({
|
|
10
|
+
".png": "image/png",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".gif": "image/gif",
|
|
14
|
+
".webp": "image/webp",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function sniffMediaType(buffer = Buffer.alloc(0)) {
|
|
18
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 12) return "";
|
|
19
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
|
20
|
+
return "image/png";
|
|
21
|
+
}
|
|
22
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
23
|
+
return "image/jpeg";
|
|
24
|
+
}
|
|
25
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
|
26
|
+
return "image/gif";
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46
|
|
30
|
+
&& buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50
|
|
31
|
+
) {
|
|
32
|
+
return "image/webp";
|
|
33
|
+
}
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mediaTypeFromPath(filePath = "") {
|
|
38
|
+
const ext = path.extname(String(filePath || "")).toLowerCase();
|
|
39
|
+
return EXT_MEDIA[ext] || "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runReadImageTool(input = {}, options = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const filePath = String(input.path || input.file || "").trim();
|
|
45
|
+
if (!filePath) {
|
|
46
|
+
return { ok: false, error: "path is required" };
|
|
47
|
+
}
|
|
48
|
+
const { workspaceRoot, resolved } = resolveWorkspacePath(
|
|
49
|
+
options.workspaceRoot,
|
|
50
|
+
filePath,
|
|
51
|
+
options.cwd,
|
|
52
|
+
);
|
|
53
|
+
const stat = fs.statSync(resolved);
|
|
54
|
+
if (!stat.isFile()) {
|
|
55
|
+
return { ok: false, error: `not a file: ${resolved}` };
|
|
56
|
+
}
|
|
57
|
+
if (stat.size > MAX_IMAGE_BYTES) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: `image too large (${stat.size} bytes); max ${MAX_IMAGE_BYTES} bytes — compress or resize first`,
|
|
61
|
+
path: resolved,
|
|
62
|
+
bytes: stat.size,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const buffer = fs.readFileSync(resolved);
|
|
67
|
+
const sniffed = sniffMediaType(buffer);
|
|
68
|
+
const fromExt = mediaTypeFromPath(resolved);
|
|
69
|
+
const mediaType = sniffed || fromExt;
|
|
70
|
+
if (!mediaType) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: "unsupported image type (use png, jpeg, gif, or webp)",
|
|
74
|
+
path: resolved,
|
|
75
|
+
bytes: buffer.length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (fromExt && sniffed && fromExt !== sniffed) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `image type mismatch: extension suggests ${fromExt}, bytes are ${sniffed}`,
|
|
82
|
+
path: resolved,
|
|
83
|
+
bytes: buffer.length,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
kind: "image",
|
|
90
|
+
workspaceRoot,
|
|
91
|
+
path: resolved,
|
|
92
|
+
mediaType,
|
|
93
|
+
bytes: buffer.length,
|
|
94
|
+
base64: buffer.toString("base64"),
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: err && err.message ? err.message : "read_image failed",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
MAX_IMAGE_BYTES,
|
|
106
|
+
EXT_MEDIA,
|
|
107
|
+
sniffMediaType,
|
|
108
|
+
mediaTypeFromPath,
|
|
109
|
+
runReadImageTool,
|
|
110
|
+
};
|
package/src/ui/format/index.js
CHANGED
|
@@ -38,10 +38,14 @@ const STATUS_INDICATORS = {
|
|
|
38
38
|
// Keep this list in sync with the keys handled by buildMergedToolSummaryText.
|
|
39
39
|
const TOOL_LABELS = {
|
|
40
40
|
read: "Reading file",
|
|
41
|
+
read_image: "Reading image",
|
|
41
42
|
write: "Writing file",
|
|
42
43
|
edit: "Editing file",
|
|
43
44
|
bash: "Running command",
|
|
44
45
|
artifact_read: "Reading artifact",
|
|
46
|
+
plan_graph: "Updating plan",
|
|
47
|
+
task_run: "Managing task",
|
|
48
|
+
ask_user: "Asking user",
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
|
|
@@ -283,7 +287,15 @@ function messageContentText(message = {}) {
|
|
|
283
287
|
if (Array.isArray(content)) {
|
|
284
288
|
return content.map((part) => {
|
|
285
289
|
if (typeof part === "string") return part;
|
|
286
|
-
if (part
|
|
290
|
+
if (!part || typeof part !== "object") return "";
|
|
291
|
+
const type = String(part.type || "").trim().toLowerCase();
|
|
292
|
+
if (type === "image" || type === "image_url") {
|
|
293
|
+
const name = part.fileName
|
|
294
|
+
|| (part.path ? require("path").basename(String(part.path)) : "")
|
|
295
|
+
|| "image";
|
|
296
|
+
return `[image: ${name}]`;
|
|
297
|
+
}
|
|
298
|
+
if (part.text != null) return String(part.text);
|
|
287
299
|
return "";
|
|
288
300
|
}).join("");
|
|
289
301
|
}
|
|
@@ -296,15 +308,47 @@ function toolMessagePreview(message = {}) {
|
|
|
296
308
|
try {
|
|
297
309
|
const parsed = JSON.parse(raw);
|
|
298
310
|
if (parsed && typeof parsed === "object") {
|
|
311
|
+
if (parsed.kind === "image" || parsed.base64 || parsed.mediaType) {
|
|
312
|
+
const name = require("path").basename(String(parsed.path || parsed.fileName || "image"));
|
|
313
|
+
return parsed.preview || `[image: ${name}]`;
|
|
314
|
+
}
|
|
299
315
|
if (parsed.preview) return String(parsed.preview);
|
|
300
316
|
if (parsed.artifactId) return `artifact:${parsed.artifactId}`;
|
|
317
|
+
if (parsed.base64) {
|
|
318
|
+
const clone = { ...parsed };
|
|
319
|
+
delete clone.base64;
|
|
320
|
+
return JSON.stringify(clone);
|
|
321
|
+
}
|
|
301
322
|
}
|
|
302
323
|
} catch {
|
|
303
324
|
// plain tool text
|
|
304
325
|
}
|
|
326
|
+
if (/data:image\/[a-zA-Z+]+;base64,/.test(raw) || /"base64"\s*:/.test(raw)) {
|
|
327
|
+
return "[image]";
|
|
328
|
+
}
|
|
305
329
|
return raw;
|
|
306
330
|
}
|
|
307
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Collapse attached-image prompt prefixes / base64 blobs for TUI log display.
|
|
334
|
+
*/
|
|
335
|
+
function redactUserMessageForLog(text = "") {
|
|
336
|
+
let out = String(text || "");
|
|
337
|
+
out = out.replace(
|
|
338
|
+
/\[Attached images[^\]]*\]\s*(?:-\s*.+\n?)*/gi,
|
|
339
|
+
(block) => {
|
|
340
|
+
const paths = [...block.matchAll(/^\s*-\s*(.+)$/gm)].map((m) => String(m[1] || "").trim());
|
|
341
|
+
if (paths.length === 0) return "[image]";
|
|
342
|
+
return paths
|
|
343
|
+
.map((p) => `[image: ${require("path").basename(p)}]`)
|
|
344
|
+
.join(" ");
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
out = out.replace(/data:image\/[a-zA-Z+]+;base64,[A-Za-z0-9+/=\s]+/g, "[image]");
|
|
348
|
+
out = out.replace(/"base64"\s*:\s*"[^"]*"/g, '"base64":"[redacted]"');
|
|
349
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
350
|
+
}
|
|
351
|
+
|
|
308
352
|
/**
|
|
309
353
|
* Convert persisted nlMessages into ucode TUI log rows for resume/history.
|
|
310
354
|
* Applies the shared ANSI markdown renderer so restored assistant text matches
|
|
@@ -349,7 +393,7 @@ function buildUcodeSessionLogEntries(messages = [], options = {}) {
|
|
|
349
393
|
if (!message || typeof message !== "object") continue;
|
|
350
394
|
const role = String(message.role || "").trim().toLowerCase();
|
|
351
395
|
if (role === "user") {
|
|
352
|
-
const text = messageContentText(message);
|
|
396
|
+
const text = redactUserMessageForLog(messageContentText(message));
|
|
353
397
|
if (!text.trim()) continue;
|
|
354
398
|
const lines = text.split(/\r?\n/);
|
|
355
399
|
lines.forEach((line, index) => {
|
|
@@ -671,6 +715,12 @@ function normalizeToolLogDetail(tool = "", args = {}, payload = {}) {
|
|
|
671
715
|
return shortenPathDetail(pathText);
|
|
672
716
|
}
|
|
673
717
|
|
|
718
|
+
if (name === "read_image") {
|
|
719
|
+
const pathText = String(argObj.path || resObj.path || resObj.fileName || "").trim();
|
|
720
|
+
const base = pathText ? require("path").basename(pathText) : "image";
|
|
721
|
+
return `[image: ${base}]`;
|
|
722
|
+
}
|
|
723
|
+
|
|
674
724
|
if (name === "artifact_read") {
|
|
675
725
|
const artifactId = String(argObj.artifactId || argObj.id || resObj.artifactId || "").trim();
|
|
676
726
|
const rangeBits = [];
|
|
@@ -1295,6 +1345,9 @@ module.exports = {
|
|
|
1295
1345
|
normalizeModelLabel,
|
|
1296
1346
|
normalizeToolLogDetail,
|
|
1297
1347
|
normalizeToolMergeEntry,
|
|
1348
|
+
messageContentText,
|
|
1349
|
+
toolMessagePreview,
|
|
1350
|
+
redactUserMessageForLog,
|
|
1298
1351
|
parseActiveAgentsFromBusStatus,
|
|
1299
1352
|
planAgentsFooter,
|
|
1300
1353
|
planProjectsRail,
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* component (e.g. completion popup) can
|
|
26
26
|
* handle them. Plain editing keys still work.
|
|
27
27
|
* placeholder (string) rendered in gray when value is empty
|
|
28
|
+
* onPasteText(filtered) optional. Called for non-empty paste/insert
|
|
29
|
+
* chunks before insertText. May return:
|
|
30
|
+
* string — insert that text instead
|
|
31
|
+
* { text } — insert text (may be "")
|
|
32
|
+
* null/undefined — fall back to filtered text
|
|
28
33
|
*
|
|
29
34
|
* Newlines: Enter submits. Use Alt+Enter (delivered as meta+Return) or end the
|
|
30
35
|
* line with `\` (the legacy continuation trick) to insert a literal newline.
|
|
@@ -32,7 +37,8 @@
|
|
|
32
37
|
* plain Enter, so it would silently submit.
|
|
33
38
|
*
|
|
34
39
|
* Bracketed paste arrives as a multi-byte `input` chunk in useInput; we route
|
|
35
|
-
* it through insertText, so multi-line paste already works
|
|
40
|
+
* it through insertText (or onPasteText), so multi-line paste already works
|
|
41
|
+
* without extra code.
|
|
36
42
|
*/
|
|
37
43
|
|
|
38
44
|
const fmt = require("../format");
|
|
@@ -167,6 +173,7 @@ function createMultilineInput({ React, ink }) {
|
|
|
167
173
|
// the IME composition window pops up at the visible (inverse) cursor
|
|
168
174
|
// instead of at the bottom of the screen.
|
|
169
175
|
linesBelowInput = 0,
|
|
176
|
+
onPasteText = null,
|
|
170
177
|
}) {
|
|
171
178
|
// Cursor is owned by this component. preferredCol tracks the visual
|
|
172
179
|
// column we want to keep when bouncing across lines of different widths
|
|
@@ -392,7 +399,36 @@ function createMultilineInput({ React, ink }) {
|
|
|
392
399
|
// Plain character / paste. Filter control bytes.
|
|
393
400
|
if (input && !key.ctrl && !key.meta) {
|
|
394
401
|
const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
|
|
395
|
-
|
|
402
|
+
// Binary clipboard paste can strip to empty — still notify parent so
|
|
403
|
+
// it can try macOS PNGf clipboard ingest.
|
|
404
|
+
if (!filtered) {
|
|
405
|
+
if (typeof onPasteText === "function") {
|
|
406
|
+
try { onPasteText(""); } catch { /* ignore */ }
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (typeof onPasteText === "function" && (filtered.length > 1 || filtered.includes("\n"))) {
|
|
411
|
+
let rewritten;
|
|
412
|
+
try {
|
|
413
|
+
rewritten = onPasteText(filtered);
|
|
414
|
+
} catch {
|
|
415
|
+
rewritten = filtered;
|
|
416
|
+
}
|
|
417
|
+
if (rewritten == null) {
|
|
418
|
+
insertText(filtered);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (typeof rewritten === "string") {
|
|
422
|
+
if (rewritten) insertText(rewritten);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (rewritten && typeof rewritten === "object") {
|
|
426
|
+
const nextText = rewritten.text == null ? filtered : String(rewritten.text);
|
|
427
|
+
if (nextText) insertText(nextText);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
insertText(filtered);
|
|
396
432
|
}
|
|
397
433
|
}, { isActive: interactive });
|
|
398
434
|
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
const { runInk } = require("../runInk");
|
|
18
18
|
const fmt = require("../format");
|
|
19
19
|
const { createMultilineInput } = require("./MultilineInput");
|
|
20
|
+
const {
|
|
21
|
+
handleImagePaste,
|
|
22
|
+
formatUserLogWithAttachments,
|
|
23
|
+
buildAttachedImagesPromptPrefix,
|
|
24
|
+
} = require("../../code/imageIngest");
|
|
20
25
|
|
|
21
26
|
// Throttle for the live thinking-chain status line: rapid thinking_delta
|
|
22
27
|
// chunks would otherwise re-render the footer on every SSE event.
|
|
@@ -70,6 +75,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
70
75
|
);
|
|
71
76
|
const [draft, setDraft] = useState("");
|
|
72
77
|
const [draftVersion, setDraftVersion] = useState(0);
|
|
78
|
+
const [imageAttachments, setImageAttachments] = useState([]);
|
|
73
79
|
// status: idle when message === "". `type` picks a STATUS_INDICATORS
|
|
74
80
|
// bucket; `showTimer` and `startedAt` reproduce the blessed spinner
|
|
75
81
|
// controls. The BG suffix is computed from backgroundTasksRef and
|
|
@@ -510,12 +516,20 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
510
516
|
|
|
511
517
|
const runChainRef = useRef(Promise.resolve());
|
|
512
518
|
|
|
513
|
-
const executeLine = useCallback(async (rawValue) => {
|
|
514
|
-
const
|
|
515
|
-
|
|
519
|
+
const executeLine = useCallback(async (rawValue, options = {}) => {
|
|
520
|
+
const modelSource = options.modelText != null ? options.modelText : rawValue;
|
|
521
|
+
const logSource = options.logText != null ? options.logText : modelSource;
|
|
522
|
+
const preserveNewlines = Boolean(options.preserveNewlines);
|
|
523
|
+
const modelNormalized = preserveNewlines
|
|
524
|
+
? String(modelSource || "").trim()
|
|
525
|
+
: String(modelSource || "").replace(/\r?\n/g, " ").trim();
|
|
526
|
+
const logNormalized = fmt.redactUserMessageForLog(
|
|
527
|
+
String(logSource || "").replace(/\r?\n/g, " ").trim(),
|
|
528
|
+
);
|
|
529
|
+
if (!modelNormalized && !logNormalized) return;
|
|
516
530
|
toolMergeScopeRef.current += 1;
|
|
517
531
|
flushActiveMerge();
|
|
518
|
-
appendLogLine(`› ${
|
|
532
|
+
appendLogLine(`› ${logNormalized || modelNormalized}`, "user");
|
|
519
533
|
|
|
520
534
|
const runtimeWorkspace = String(
|
|
521
535
|
(props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd()
|
|
@@ -523,7 +537,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
523
537
|
|
|
524
538
|
let result;
|
|
525
539
|
try {
|
|
526
|
-
result = props.runSingleCommand(
|
|
540
|
+
result = props.runSingleCommand(modelNormalized, runtimeWorkspace);
|
|
527
541
|
} catch (err) {
|
|
528
542
|
appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`, "error");
|
|
529
543
|
return;
|
|
@@ -963,21 +977,27 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
963
977
|
|
|
964
978
|
const submit = useCallback((submitted) => {
|
|
965
979
|
const value = String(submitted == null ? draft : submitted);
|
|
980
|
+
const attachments = Array.isArray(imageAttachments) ? imageAttachments.slice() : [];
|
|
966
981
|
const trimmed = value.trim();
|
|
967
|
-
if (!trimmed) return;
|
|
982
|
+
if (!trimmed && attachments.length === 0) return;
|
|
968
983
|
setDraft("");
|
|
969
984
|
setDraftVersion((v) => v + 1);
|
|
985
|
+
setImageAttachments([]);
|
|
970
986
|
setInputHistory((prev) => {
|
|
971
|
-
const
|
|
987
|
+
const historyValue = formatUserLogWithAttachments(trimmed, attachments) || trimmed;
|
|
988
|
+
const next = prev.concat([historyValue]).slice(-200);
|
|
972
989
|
setHistoryIndex(next.length);
|
|
973
990
|
return next;
|
|
974
991
|
});
|
|
975
992
|
|
|
993
|
+
const modelText = `${buildAttachedImagesPromptPrefix(attachments)}${trimmed}`.trim();
|
|
994
|
+
const logText = formatUserLogWithAttachments(trimmed, attachments);
|
|
995
|
+
|
|
976
996
|
// Pending approval/choice/chat takes priority over nudge / new NL.
|
|
977
997
|
try {
|
|
978
998
|
const { hasPendingUserInteraction } = require("../../code/context/userInteraction");
|
|
979
999
|
if (props.state && props.state.executionState && hasPendingUserInteraction(props.state.executionState)) {
|
|
980
|
-
appendLogText(`› ${
|
|
1000
|
+
appendLogText(`› ${logText}`, "user");
|
|
981
1001
|
const startedAt = Date.now();
|
|
982
1002
|
setStatus({
|
|
983
1003
|
message: "Applying your reply...",
|
|
@@ -1053,10 +1073,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1053
1073
|
if (!props.state.executionState || typeof props.state.executionState !== "object") {
|
|
1054
1074
|
props.state.executionState = emptyExecutionState();
|
|
1055
1075
|
}
|
|
1056
|
-
const queued = enqueueUserPrompt(props.state.executionState,
|
|
1076
|
+
const queued = enqueueUserPrompt(props.state.executionState, modelText);
|
|
1077
|
+
const reminderPreview = logText.slice(0, 120) + (logText.length > 120 ? "…" : "");
|
|
1057
1078
|
appendLogText(
|
|
1058
1079
|
queued.enqueued
|
|
1059
|
-
? `Queued user reminder for next model turn: ${
|
|
1080
|
+
? `Queued user reminder for next model turn: ${reminderPreview}`
|
|
1060
1081
|
: "Could not queue user reminder (empty).",
|
|
1061
1082
|
"system",
|
|
1062
1083
|
);
|
|
@@ -1065,10 +1086,15 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1065
1086
|
|
|
1066
1087
|
// Serialize executions so streaming tasks don't interleave.
|
|
1067
1088
|
runChainRef.current = runChainRef.current
|
|
1068
|
-
.then(() => executeLine(
|
|
1089
|
+
.then(() => executeLine(modelText, {
|
|
1090
|
+
modelText,
|
|
1091
|
+
logText,
|
|
1092
|
+
preserveNewlines: attachments.length > 0,
|
|
1093
|
+
}))
|
|
1069
1094
|
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
|
|
1070
1095
|
}, [
|
|
1071
1096
|
draft,
|
|
1097
|
+
imageAttachments,
|
|
1072
1098
|
executeLine,
|
|
1073
1099
|
appendLogText,
|
|
1074
1100
|
appendLogLine,
|
|
@@ -1282,6 +1308,16 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1282
1308
|
}),
|
|
1283
1309
|
);
|
|
1284
1310
|
})() : null,
|
|
1311
|
+
imageAttachments.length > 0
|
|
1312
|
+
? h(Box, { flexDirection: "column", width: "100%", marginBottom: 0 },
|
|
1313
|
+
h(Text, { color: "cyan", dimColor: true },
|
|
1314
|
+
imageAttachments.map((item) => {
|
|
1315
|
+
const name = item.fileName || require("path").basename(String(item.relPath || "image"));
|
|
1316
|
+
return `[img] ${name}`;
|
|
1317
|
+
}).join(" "),
|
|
1318
|
+
),
|
|
1319
|
+
)
|
|
1320
|
+
: null,
|
|
1285
1321
|
h(Box, { width: "100%" },
|
|
1286
1322
|
h(MultilineInput, {
|
|
1287
1323
|
value: draft,
|
|
@@ -1292,6 +1328,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1292
1328
|
}
|
|
1293
1329
|
setDraft(next);
|
|
1294
1330
|
},
|
|
1331
|
+
onPasteText: (filtered) => {
|
|
1332
|
+
const workspaceRoot = String(
|
|
1333
|
+
(props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd(),
|
|
1334
|
+
);
|
|
1335
|
+
const sessionId = String((props.state && props.state.sessionId) || "session");
|
|
1336
|
+
const outcome = handleImagePaste(filtered, {
|
|
1337
|
+
workspaceRoot,
|
|
1338
|
+
sessionId,
|
|
1339
|
+
tryClipboard: true,
|
|
1340
|
+
});
|
|
1341
|
+
if (Array.isArray(outcome.attachments) && outcome.attachments.length > 0) {
|
|
1342
|
+
setImageAttachments((prev) => {
|
|
1343
|
+
const next = prev.slice();
|
|
1344
|
+
for (const item of outcome.attachments) {
|
|
1345
|
+
if (!item || !item.relPath) continue;
|
|
1346
|
+
if (next.some((existing) => existing.relPath === item.relPath)) continue;
|
|
1347
|
+
next.push(item);
|
|
1348
|
+
}
|
|
1349
|
+
return next;
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
if (Array.isArray(outcome.errors) && outcome.errors.length > 0 && outcome.attachments.length === 0) {
|
|
1353
|
+
// Soft notice only when nothing was ingested.
|
|
1354
|
+
appendLogText(`Image paste: ${outcome.errors[0]}`, "system");
|
|
1355
|
+
}
|
|
1356
|
+
return { text: outcome.text == null ? filtered : outcome.text };
|
|
1357
|
+
},
|
|
1295
1358
|
onSubmit: (value) => {
|
|
1296
1359
|
setCompletionSuppressedDraft(null);
|
|
1297
1360
|
submit(value);
|