u-foo 3.0.2 → 3.0.3
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 +2 -2
- package/src/code/context/promptLayers.js +6 -5
- package/src/code/context/reducers.js +35 -0
- package/src/code/context/transcriptSync.js +25 -5
- package/src/code/dispatch.js +4 -0
- package/src/code/imageIngest.js +367 -0
- package/src/code/nativeRunner.js +27 -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
package/src/code/nativeRunner.js
CHANGED
|
@@ -50,12 +50,14 @@ const {
|
|
|
50
50
|
} = require("./protocol");
|
|
51
51
|
const { stableStringify } = require("./context/stableJson");
|
|
52
52
|
const { getReadToolDescription } = require("../agents/prompts/native/toolDescriptions/read");
|
|
53
|
+
const { getReadImageToolDescription } = require("../agents/prompts/native/toolDescriptions/readImage");
|
|
53
54
|
const { getWriteToolDescription } = require("../agents/prompts/native/toolDescriptions/write");
|
|
54
55
|
const { getEditToolDescription } = require("../agents/prompts/native/toolDescriptions/edit");
|
|
55
56
|
const { getBashToolDescription } = require("../agents/prompts/native/toolDescriptions/bash");
|
|
56
57
|
|
|
57
58
|
const CORE_TOOL_NAMES = new Set([
|
|
58
59
|
"read",
|
|
60
|
+
"read_image",
|
|
59
61
|
"write",
|
|
60
62
|
"edit",
|
|
61
63
|
"bash",
|
|
@@ -64,7 +66,14 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
64
66
|
"task_run",
|
|
65
67
|
"ask_user",
|
|
66
68
|
]);
|
|
67
|
-
const EXECUTABLE_GRAPH_TOOLS = new Set([
|
|
69
|
+
const EXECUTABLE_GRAPH_TOOLS = new Set([
|
|
70
|
+
"read",
|
|
71
|
+
"read_image",
|
|
72
|
+
"write",
|
|
73
|
+
"edit",
|
|
74
|
+
"bash",
|
|
75
|
+
"artifact_read",
|
|
76
|
+
]);
|
|
68
77
|
const CONTROL_PLANE_TOOLS = new Set(["plan_graph", "task_run"]);
|
|
69
78
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
70
79
|
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
@@ -392,6 +401,23 @@ function buildCoreToolSpecs() {
|
|
|
392
401
|
},
|
|
393
402
|
},
|
|
394
403
|
},
|
|
404
|
+
{
|
|
405
|
+
type: "function",
|
|
406
|
+
function: {
|
|
407
|
+
name: "read_image",
|
|
408
|
+
description: getReadImageToolDescription(),
|
|
409
|
+
parameters: {
|
|
410
|
+
type: "object",
|
|
411
|
+
properties: {
|
|
412
|
+
path: {
|
|
413
|
+
type: "string",
|
|
414
|
+
description: "Workspace-relative path to a png, jpeg, gif, or webp image.",
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
required: ["path"],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
395
421
|
{
|
|
396
422
|
type: "function",
|
|
397
423
|
function: {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { assertTransport } = require("./transportContract");
|
|
4
|
+
const {
|
|
5
|
+
extractVisionPayload,
|
|
6
|
+
stripVisionBase64,
|
|
7
|
+
visionSummaryText,
|
|
8
|
+
toAnthropicImageBlock,
|
|
9
|
+
} = require("./visionBlocks");
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Anthropic Messages API transport adapter.
|
|
@@ -70,11 +76,32 @@ function createAnthropicMessagesTransport(deps = {}) {
|
|
|
70
76
|
}));
|
|
71
77
|
},
|
|
72
78
|
appendToolResult({ collected, call, toolResult }) {
|
|
79
|
+
const vision = extractVisionPayload(toolResult);
|
|
80
|
+
const isError = Boolean(!toolResult || toolResult.ok === false);
|
|
81
|
+
if (vision) {
|
|
82
|
+
const textPayload = stripVisionBase64(toolResult);
|
|
83
|
+
collected.push({
|
|
84
|
+
type: "tool_result",
|
|
85
|
+
tool_use_id: String(call.source.id || ""),
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: clipText(
|
|
90
|
+
`${visionSummaryText(vision, toolResult)}\n${toJsonString(textPayload)}`,
|
|
91
|
+
12000,
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
toAnthropicImageBlock(vision),
|
|
95
|
+
],
|
|
96
|
+
is_error: isError,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
73
100
|
collected.push({
|
|
74
101
|
type: "tool_result",
|
|
75
102
|
tool_use_id: String(call.source.id || ""),
|
|
76
103
|
content: clipText(toJsonString(toolResult), 12000),
|
|
77
|
-
is_error:
|
|
104
|
+
is_error: isError,
|
|
78
105
|
});
|
|
79
106
|
},
|
|
80
107
|
flushToolResults({ messages, collected }) {
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
4
|
const { assertTransport } = require("./transportContract");
|
|
5
|
+
const {
|
|
6
|
+
extractVisionPayload,
|
|
7
|
+
stripVisionBase64,
|
|
8
|
+
visionSummaryText,
|
|
9
|
+
toOpenAiImagePart,
|
|
10
|
+
} = require("./visionBlocks");
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* OpenAI-compatible chat-completions transport adapter.
|
|
@@ -82,11 +88,23 @@ function createOpenAiChatTransport(deps = {}) {
|
|
|
82
88
|
}));
|
|
83
89
|
},
|
|
84
90
|
appendToolResult({ messages, call, toolResult }) {
|
|
91
|
+
const vision = extractVisionPayload(toolResult);
|
|
92
|
+
const payload = vision ? stripVisionBase64(toolResult) : toolResult;
|
|
85
93
|
messages.push({
|
|
86
94
|
role: "tool",
|
|
87
95
|
tool_call_id: call.source.id,
|
|
88
|
-
content: clipText(toJsonString(
|
|
96
|
+
content: clipText(toJsonString(payload), 12000),
|
|
89
97
|
});
|
|
98
|
+
if (vision) {
|
|
99
|
+
const imagePart = toOpenAiImagePart(vision);
|
|
100
|
+
messages.push({
|
|
101
|
+
role: "user",
|
|
102
|
+
content: [
|
|
103
|
+
{ type: "text", text: visionSummaryText(vision, toolResult) },
|
|
104
|
+
imagePart,
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
90
108
|
},
|
|
91
109
|
};
|
|
92
110
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for expanding read_image tool results into provider vision blocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function extractVisionPayload(toolResult = null) {
|
|
8
|
+
if (!toolResult || typeof toolResult !== "object") return null;
|
|
9
|
+
const base64 = String(toolResult.base64 || "").trim();
|
|
10
|
+
const mediaType = String(toolResult.mediaType || "").trim().toLowerCase();
|
|
11
|
+
if (!base64 || !mediaType.startsWith("image/")) return null;
|
|
12
|
+
if (toolResult.ok === false) return null;
|
|
13
|
+
return {
|
|
14
|
+
path: String(toolResult.path || "").trim(),
|
|
15
|
+
mediaType,
|
|
16
|
+
bytes: Number.isFinite(toolResult.bytes) ? toolResult.bytes : null,
|
|
17
|
+
base64,
|
|
18
|
+
artifactId: String(toolResult.artifactId || "").trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isVisionToolResult(toolResult = null) {
|
|
23
|
+
return Boolean(extractVisionPayload(toolResult));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripVisionBase64(value) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => stripVisionBase64(item));
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== "object") return value;
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
33
|
+
if (key === "base64") continue;
|
|
34
|
+
out[key] = stripVisionBase64(entry);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function visionSummaryText(vision = null, toolResult = null) {
|
|
40
|
+
const pathText = (vision && vision.path) || (toolResult && toolResult.path) || "image";
|
|
41
|
+
const mediaType = (vision && vision.mediaType) || (toolResult && toolResult.mediaType) || "image/*";
|
|
42
|
+
const bytes = (vision && vision.bytes) != null
|
|
43
|
+
? vision.bytes
|
|
44
|
+
: (toolResult && toolResult.bytes);
|
|
45
|
+
const parts = [
|
|
46
|
+
`Image loaded for vision: ${pathText}`,
|
|
47
|
+
`mediaType=${mediaType}`,
|
|
48
|
+
];
|
|
49
|
+
if (Number.isFinite(bytes)) parts.push(`bytes=${bytes}`);
|
|
50
|
+
if (vision && vision.artifactId) parts.push(`artifactId=${vision.artifactId}`);
|
|
51
|
+
parts.push("Visual content is attached for this model call only; call read_image again later if needed.");
|
|
52
|
+
return parts.join(" | ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toAnthropicImageBlock(vision = null) {
|
|
56
|
+
if (!vision) return null;
|
|
57
|
+
return {
|
|
58
|
+
type: "image",
|
|
59
|
+
source: {
|
|
60
|
+
type: "base64",
|
|
61
|
+
media_type: vision.mediaType,
|
|
62
|
+
data: vision.base64,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toOpenAiImagePart(vision = null) {
|
|
68
|
+
if (!vision) return null;
|
|
69
|
+
return {
|
|
70
|
+
type: "image_url",
|
|
71
|
+
image_url: {
|
|
72
|
+
url: `data:${vision.mediaType};base64,${vision.base64}`,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function degradeVisionContent(content) {
|
|
78
|
+
if (typeof content === "string") return content;
|
|
79
|
+
if (!Array.isArray(content)) return content;
|
|
80
|
+
const texts = [];
|
|
81
|
+
for (const block of content) {
|
|
82
|
+
if (!block || typeof block !== "object") continue;
|
|
83
|
+
const type = String(block.type || "").trim().toLowerCase();
|
|
84
|
+
if (type === "text" && block.text) {
|
|
85
|
+
texts.push(String(block.text));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (type === "image" || type === "image_url") {
|
|
89
|
+
const pathHint = block.path
|
|
90
|
+
|| (block.source && block.source.path)
|
|
91
|
+
|| "";
|
|
92
|
+
texts.push(pathHint ? `[image: ${pathHint}]` : "[image]");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (type === "tool_result" && Array.isArray(block.content)) {
|
|
96
|
+
texts.push(degradeVisionContent(block.content));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return texts.filter(Boolean).join("\n") || "[multimodal content]";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
extractVisionPayload,
|
|
104
|
+
isVisionToolResult,
|
|
105
|
+
stripVisionBase64,
|
|
106
|
+
visionSummaryText,
|
|
107
|
+
toAnthropicImageBlock,
|
|
108
|
+
toOpenAiImagePart,
|
|
109
|
+
degradeVisionContent,
|
|
110
|
+
};
|
|
@@ -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
|
|