u-foo 3.0.1 → 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/tasks.js +4 -1
- package/src/agents/prompts/native/toolDescriptions/readImage.js +23 -0
- package/src/app/chat/commandExecutor.js +111 -1
- package/src/app/chat/commands.js +2 -1
- package/src/app/chat/daemonMessageRouter.js +1 -1
- package/src/app/chat/inputSubmitHandler.js +3 -2
- package/src/code/commands.js +3 -3
- package/src/code/context/assembler.js +17 -1
- package/src/code/context/planMode.js +2 -2
- package/src/code/context/promptLayers.js +12 -10
- package/src/code/context/reducers.js +35 -0
- package/src/code/context/transcriptSync.js +25 -5
- package/src/code/dispatch.js +8 -0
- package/src/code/imageIngest.js +367 -0
- package/src/code/modelCommand.js +199 -23
- package/src/code/nativeRunner.js +184 -20
- package/src/code/protocol/protocolValidator.js +3 -3
- package/src/code/providers/anthropicMessagesTransport.js +28 -1
- package/src/code/providers/index.js +2 -0
- package/src/code/providers/modelsCatalog.js +304 -0
- package/src/code/providers/openaiChatTransport.js +19 -1
- package/src/code/providers/visionBlocks.js +110 -0
- package/src/code/repl.js +37 -8
- package/src/code/runtime/taskControl.js +177 -53
- package/src/code/runtime/taskFocus.js +30 -10
- package/src/code/runtime/taskLoop.js +12 -1
- package/src/code/runtime/taskRun.js +10 -1
- package/src/code/thinkingLevels.js +132 -0
- package/src/code/tools/readImage.js +110 -0
- package/src/code/tools/taskRun.js +118 -0
- package/src/config.js +10 -1
- package/src/ui/format/index.js +103 -5
- package/src/ui/ink/ChatApp.js +137 -25
- package/src/ui/ink/MultilineInput.js +38 -2
- package/src/ui/ink/UcodeApp.js +102 -14
- package/src/ui/ink/chatLogModel.js +238 -32
- package/src/ui/ink/chatReducer.js +18 -6
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ucode thinking-intensity levels.
|
|
5
|
+
*
|
|
6
|
+
* Used as the secondary `/model <id> <level>` menu after picking a model.
|
|
7
|
+
* Maps to Anthropic extended-thinking budget_tokens and OpenAI-compatible
|
|
8
|
+
* reasoning_effort where the transport supports it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const THINKING_LEVELS = Object.freeze([
|
|
12
|
+
{
|
|
13
|
+
id: "off",
|
|
14
|
+
desc: "disable extended thinking",
|
|
15
|
+
budgetTokens: 0,
|
|
16
|
+
reasoningEffort: "",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "low",
|
|
20
|
+
desc: "light thinking",
|
|
21
|
+
budgetTokens: 2048,
|
|
22
|
+
reasoningEffort: "low",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "medium",
|
|
26
|
+
desc: "default thinking",
|
|
27
|
+
budgetTokens: 10000,
|
|
28
|
+
reasoningEffort: "medium",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "high",
|
|
32
|
+
desc: "deeper thinking",
|
|
33
|
+
budgetTokens: 32000,
|
|
34
|
+
reasoningEffort: "high",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "max",
|
|
38
|
+
desc: "maximum thinking budget",
|
|
39
|
+
budgetTokens: 48000,
|
|
40
|
+
reasoningEffort: "high",
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const DEFAULT_THINKING_LEVEL = "medium";
|
|
45
|
+
const THINKING_LEVEL_IDS = new Set(THINKING_LEVELS.map((item) => item.id));
|
|
46
|
+
|
|
47
|
+
function normalizeThinkingLevel(value = "") {
|
|
48
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
49
|
+
if (!raw) return "";
|
|
50
|
+
if (raw === "none" || raw === "disable" || raw === "disabled" || raw === "0") return "off";
|
|
51
|
+
if (raw === "med" || raw === "default") return "medium";
|
|
52
|
+
if (raw === "maximum" || raw === "xhigh" || raw === "ultra") return "max";
|
|
53
|
+
if (THINKING_LEVEL_IDS.has(raw)) return raw;
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getThinkingLevel(id = "") {
|
|
58
|
+
const normalized = normalizeThinkingLevel(id) || DEFAULT_THINKING_LEVEL;
|
|
59
|
+
return THINKING_LEVELS.find((item) => item.id === normalized) || THINKING_LEVELS[2];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function suggestThinkingLevels(options = {}) {
|
|
63
|
+
const current = normalizeThinkingLevel(options.current || "") || DEFAULT_THINKING_LEVEL;
|
|
64
|
+
return THINKING_LEVELS.map((item) => ({
|
|
65
|
+
id: item.id,
|
|
66
|
+
desc: item.id === current ? `${item.desc} · current` : item.desc,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveThinkingFromEnvAndConfig({
|
|
71
|
+
env = process.env,
|
|
72
|
+
configLevel = "",
|
|
73
|
+
} = {}) {
|
|
74
|
+
// Explicit numeric budget still wins (advanced override).
|
|
75
|
+
const rawBudget = env && env.UFOO_UCODE_THINKING_BUDGET_TOKENS;
|
|
76
|
+
if (rawBudget !== undefined && rawBudget !== null && String(rawBudget).trim() !== "") {
|
|
77
|
+
const parsed = Number.parseInt(String(rawBudget), 10);
|
|
78
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
79
|
+
return {
|
|
80
|
+
level: "off",
|
|
81
|
+
budgetTokens: 0,
|
|
82
|
+
reasoningEffort: "",
|
|
83
|
+
source: "env-budget",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
level: "",
|
|
88
|
+
budgetTokens: Math.floor(parsed),
|
|
89
|
+
reasoningEffort: "",
|
|
90
|
+
source: "env-budget",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const fromEnv = normalizeThinkingLevel(env && env.UFOO_UCODE_THINKING);
|
|
95
|
+
const fromConfig = normalizeThinkingLevel(configLevel);
|
|
96
|
+
const level = fromEnv || fromConfig || DEFAULT_THINKING_LEVEL;
|
|
97
|
+
const spec = getThinkingLevel(level);
|
|
98
|
+
return {
|
|
99
|
+
level: spec.id,
|
|
100
|
+
budgetTokens: spec.budgetTokens,
|
|
101
|
+
reasoningEffort: spec.reasoningEffort,
|
|
102
|
+
source: fromEnv ? "env" : (fromConfig ? "config" : "default"),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function applyThinkingLevelToEnv(level = "", env = process.env) {
|
|
107
|
+
const normalized = normalizeThinkingLevel(level);
|
|
108
|
+
if (!normalized) return "";
|
|
109
|
+
const spec = getThinkingLevel(normalized);
|
|
110
|
+
try {
|
|
111
|
+
env.UFOO_UCODE_THINKING = spec.id;
|
|
112
|
+
if (spec.budgetTokens > 0) {
|
|
113
|
+
env.UFOO_UCODE_THINKING_BUDGET_TOKENS = String(spec.budgetTokens);
|
|
114
|
+
} else {
|
|
115
|
+
env.UFOO_UCODE_THINKING_BUDGET_TOKENS = "0";
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// ignore env write failures
|
|
119
|
+
}
|
|
120
|
+
return spec.id;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
THINKING_LEVELS,
|
|
125
|
+
THINKING_LEVEL_IDS,
|
|
126
|
+
DEFAULT_THINKING_LEVEL,
|
|
127
|
+
normalizeThinkingLevel,
|
|
128
|
+
getThinkingLevel,
|
|
129
|
+
suggestThinkingLevels,
|
|
130
|
+
resolveThinkingFromEnvAndConfig,
|
|
131
|
+
applyThinkingLevelToEnv,
|
|
132
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
startStandaloneTask,
|
|
5
|
+
cancelTask,
|
|
6
|
+
failTask,
|
|
7
|
+
completeTaskFromLoop,
|
|
8
|
+
} = require("../runtime/taskControl");
|
|
9
|
+
const { getTaskRun } = require("../runtime/taskRun");
|
|
10
|
+
const { emptyExecutionState } = require("../context/executionSegment");
|
|
11
|
+
|
|
12
|
+
function normalizeTaskRunCommand(args = {}) {
|
|
13
|
+
if (!args || typeof args !== "object") return null;
|
|
14
|
+
const operation = String(args.operation || args.op || "").trim().toLowerCase();
|
|
15
|
+
if (!operation) return null;
|
|
16
|
+
return {
|
|
17
|
+
operation,
|
|
18
|
+
objective: args.objective,
|
|
19
|
+
title: args.title,
|
|
20
|
+
taskRunId: args.taskRunId || args.task_run_id,
|
|
21
|
+
nodeId: args.nodeId || args.node_id,
|
|
22
|
+
reason: args.reason,
|
|
23
|
+
result: args.result,
|
|
24
|
+
commandId: args.commandId || args.command_id,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runTaskRunTool(args = {}, options = {}) {
|
|
29
|
+
const command = normalizeTaskRunCommand(args) || args;
|
|
30
|
+
const operation = String(command.operation || "").trim().toLowerCase();
|
|
31
|
+
const executionState = options.executionState && typeof options.executionState === "object"
|
|
32
|
+
? options.executionState
|
|
33
|
+
: emptyExecutionState();
|
|
34
|
+
const commandId = String(command.commandId || "").trim();
|
|
35
|
+
const runTool = options.runTool || null;
|
|
36
|
+
const knownTools = options.knownTools || null;
|
|
37
|
+
|
|
38
|
+
let payload;
|
|
39
|
+
if (operation === "start") {
|
|
40
|
+
payload = startStandaloneTask(executionState, {
|
|
41
|
+
objective: command.objective,
|
|
42
|
+
title: command.title,
|
|
43
|
+
commandId,
|
|
44
|
+
runTool,
|
|
45
|
+
knownTools,
|
|
46
|
+
processImmediately: options.processImmediately !== false,
|
|
47
|
+
});
|
|
48
|
+
} else if (operation === "cancel") {
|
|
49
|
+
payload = cancelTask(executionState, {
|
|
50
|
+
taskRunId: command.taskRunId,
|
|
51
|
+
nodeId: command.nodeId,
|
|
52
|
+
reason: command.reason,
|
|
53
|
+
commandId,
|
|
54
|
+
});
|
|
55
|
+
} else if (operation === "fail") {
|
|
56
|
+
payload = failTask(executionState, {
|
|
57
|
+
taskRunId: command.taskRunId,
|
|
58
|
+
nodeId: command.nodeId,
|
|
59
|
+
reason: command.reason,
|
|
60
|
+
commandId,
|
|
61
|
+
});
|
|
62
|
+
} else if (operation === "complete") {
|
|
63
|
+
payload = completeTaskFromLoop(executionState, {
|
|
64
|
+
taskRunId: command.taskRunId,
|
|
65
|
+
result: command.result,
|
|
66
|
+
commandId,
|
|
67
|
+
});
|
|
68
|
+
} else if (operation === "inspect") {
|
|
69
|
+
const run = getTaskRun(executionState, command.taskRunId);
|
|
70
|
+
if (!run) {
|
|
71
|
+
payload = {
|
|
72
|
+
status: "rejected",
|
|
73
|
+
ok: false,
|
|
74
|
+
errors: [{ code: "TASK_RUN_NOT_FOUND", message: "task run missing" }],
|
|
75
|
+
};
|
|
76
|
+
} else {
|
|
77
|
+
payload = {
|
|
78
|
+
status: "accepted",
|
|
79
|
+
ok: true,
|
|
80
|
+
taskRun: {
|
|
81
|
+
id: run.id,
|
|
82
|
+
kind: run.kind || "",
|
|
83
|
+
status: run.status,
|
|
84
|
+
phase: run.phase,
|
|
85
|
+
objective: run.objective || "",
|
|
86
|
+
title: run.title || "",
|
|
87
|
+
parentGraphId: run.parentGraphId || "",
|
|
88
|
+
parentNodeId: run.parentNodeId || "",
|
|
89
|
+
childGraphId: run.childGraphId || "",
|
|
90
|
+
result: run.result,
|
|
91
|
+
error: run.error,
|
|
92
|
+
changedFiles: Array.isArray(run.changedFiles) ? run.changedFiles.slice() : [],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
payload = {
|
|
98
|
+
status: "rejected",
|
|
99
|
+
ok: false,
|
|
100
|
+
errors: [{
|
|
101
|
+
code: "UNKNOWN_TASK_RUN_OP",
|
|
102
|
+
message: `unknown task_run operation: ${operation || "(empty)"}`,
|
|
103
|
+
}],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ok = payload && payload.ok !== false && payload.status !== "rejected";
|
|
108
|
+
return {
|
|
109
|
+
ok,
|
|
110
|
+
...payload,
|
|
111
|
+
executionState,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
normalizeTaskRunCommand,
|
|
117
|
+
runTaskRunTool,
|
|
118
|
+
};
|
package/src/config.js
CHANGED
|
@@ -2,7 +2,14 @@ const fs = require("fs");
|
|
|
2
2
|
const os = require("os");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
|
|
5
|
-
const UCODE_FIELDS = [
|
|
5
|
+
const UCODE_FIELDS = [
|
|
6
|
+
"ucodeProvider",
|
|
7
|
+
"ucodeModel",
|
|
8
|
+
"ucodeBaseUrl",
|
|
9
|
+
"ucodeApiKey",
|
|
10
|
+
"ucodeAgentDir",
|
|
11
|
+
"ucodeThinking",
|
|
12
|
+
];
|
|
6
13
|
|
|
7
14
|
const SETTINGS_MODEL_DEFAULTS = Object.freeze({
|
|
8
15
|
agent: Object.freeze({
|
|
@@ -47,6 +54,7 @@ const DEFAULT_UCODE_CONFIG = {
|
|
|
47
54
|
ucodeBaseUrl: "",
|
|
48
55
|
ucodeApiKey: "",
|
|
49
56
|
ucodeAgentDir: "",
|
|
57
|
+
ucodeThinking: "",
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
function normalizeLaunchMode(value) {
|
|
@@ -251,6 +259,7 @@ function loadGlobalUcodeConfig() {
|
|
|
251
259
|
ucodeBaseUrl: typeof raw.ucodeBaseUrl === "string" ? raw.ucodeBaseUrl : "",
|
|
252
260
|
ucodeApiKey: typeof raw.ucodeApiKey === "string" ? raw.ucodeApiKey : "",
|
|
253
261
|
ucodeAgentDir: typeof raw.ucodeAgentDir === "string" ? raw.ucodeAgentDir : "",
|
|
262
|
+
ucodeThinking: typeof raw.ucodeThinking === "string" ? raw.ucodeThinking : "",
|
|
254
263
|
};
|
|
255
264
|
}
|
|
256
265
|
|
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 = [];
|
|
@@ -1103,13 +1153,52 @@ function buildCompletions({
|
|
|
1103
1153
|
}
|
|
1104
1154
|
}
|
|
1105
1155
|
|
|
1106
|
-
// Generic top-level argument lists (e.g. /resume <session-id
|
|
1156
|
+
// Generic top-level argument lists (e.g. /resume <session-id>,
|
|
1157
|
+
// /model <id> [thinking]). /model supports a secondary intensity menu.
|
|
1107
1158
|
if (
|
|
1108
1159
|
Array.isArray(argListForHead)
|
|
1109
1160
|
&& argListForHead.length > 0
|
|
1110
1161
|
&& !(headNode && headNode.children)
|
|
1111
1162
|
&& (endsWithWhitespace || tail.length >= 1)
|
|
1112
1163
|
) {
|
|
1164
|
+
const secondaryList = argumentLists && typeof argumentLists === "object"
|
|
1165
|
+
? argumentLists[`${headKey}/thinking`] || argumentLists[`${headKey}/think`]
|
|
1166
|
+
: null;
|
|
1167
|
+
const supportsThinkingMenu = head === "/model" && Array.isArray(secondaryList) && secondaryList.length > 0;
|
|
1168
|
+
|
|
1169
|
+
// Secondary menu: "/model <id> " or "/model <id> <partial>"
|
|
1170
|
+
if (supportsThinkingMenu && (
|
|
1171
|
+
(tail.length === 1 && endsWithWhitespace)
|
|
1172
|
+
|| tail.length >= 2
|
|
1173
|
+
)) {
|
|
1174
|
+
if (tail.length > 2) return [];
|
|
1175
|
+
const modelId = String(tail[0] || "").trim();
|
|
1176
|
+
if (!modelId) return [];
|
|
1177
|
+
const partial = tail.length >= 2 && !endsWithWhitespace
|
|
1178
|
+
? String(tail[1] || "").toLowerCase()
|
|
1179
|
+
: "";
|
|
1180
|
+
const out = [];
|
|
1181
|
+
for (const item of secondaryList) {
|
|
1182
|
+
const id = String((item && (item.alias || item.cmd || item.id || item.name)) || item || "");
|
|
1183
|
+
if (!id) continue;
|
|
1184
|
+
if (partial && !id.toLowerCase().startsWith(partial)) continue;
|
|
1185
|
+
const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
|
|
1186
|
+
out.push({
|
|
1187
|
+
kind: "argument",
|
|
1188
|
+
label: `${head} ${modelId} ${id}`,
|
|
1189
|
+
replace: `${head} ${modelId} ${id}`,
|
|
1190
|
+
description: desc,
|
|
1191
|
+
hasChildren: false,
|
|
1192
|
+
});
|
|
1193
|
+
if (out.length >= limit) break;
|
|
1194
|
+
}
|
|
1195
|
+
if (partial && out.length === 1) {
|
|
1196
|
+
const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
|
|
1197
|
+
if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
|
|
1198
|
+
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1113
1202
|
if (tail.length > 1) return [];
|
|
1114
1203
|
const partial = String(tail[0] || "").toLowerCase();
|
|
1115
1204
|
const out = [];
|
|
@@ -1118,17 +1207,23 @@ function buildCompletions({
|
|
|
1118
1207
|
if (!id) continue;
|
|
1119
1208
|
if (partial && !id.toLowerCase().startsWith(partial)) continue;
|
|
1120
1209
|
const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
|
|
1210
|
+
const hasChildren = Boolean(
|
|
1211
|
+
(item && item.hasChildren)
|
|
1212
|
+
|| supportsThinkingMenu
|
|
1213
|
+
);
|
|
1121
1214
|
out.push({
|
|
1122
1215
|
kind: "argument",
|
|
1123
1216
|
label: `${head} ${id}`,
|
|
1124
|
-
|
|
1217
|
+
// Trailing space keeps the popup open for the thinking submenu.
|
|
1218
|
+
replace: hasChildren ? `${head} ${id} ` : `${head} ${id} `,
|
|
1125
1219
|
description: desc,
|
|
1126
|
-
hasChildren
|
|
1220
|
+
hasChildren,
|
|
1127
1221
|
});
|
|
1128
1222
|
if (out.length >= limit) break;
|
|
1129
1223
|
}
|
|
1130
1224
|
if (partial && out.length === 1) {
|
|
1131
1225
|
const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
|
|
1226
|
+
// Keep the popup open when the sole match still has a submenu.
|
|
1132
1227
|
if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
|
|
1133
1228
|
}
|
|
1134
1229
|
return out;
|
|
@@ -1250,6 +1345,9 @@ module.exports = {
|
|
|
1250
1345
|
normalizeModelLabel,
|
|
1251
1346
|
normalizeToolLogDetail,
|
|
1252
1347
|
normalizeToolMergeEntry,
|
|
1348
|
+
messageContentText,
|
|
1349
|
+
toolMessagePreview,
|
|
1350
|
+
redactUserMessageForLog,
|
|
1253
1351
|
parseActiveAgentsFromBusStatus,
|
|
1254
1352
|
planAgentsFooter,
|
|
1255
1353
|
planProjectsRail,
|