u-foo 2.3.30 → 2.3.32
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 +5 -1
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/chat/commandExecutor.js +6 -2
- package/src/chat/daemonMessageRouter.js +9 -1
- package/src/chat/daemonTransport.js +2 -1
- package/src/chat/dashboardKeyController.js +0 -40
- package/src/chat/dashboardView.js +0 -20
- package/src/chat/index.js +9 -1
- package/src/chat/inputSubmitHandler.js +34 -0
- package/src/chat/projectCloseController.js +1 -1
- package/src/chat/shellCommand.js +42 -0
- package/src/chat/transport.js +16 -3
- package/src/cli.js +4 -3
- package/src/code/agent.js +4 -0
- package/src/code/nativeRunner.js +74 -0
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +73 -561
- package/src/daemon/index.js +169 -27
- package/src/daemon/ipcServer.js +23 -1
- package/src/daemon/promptRequest.js +6 -1
- package/src/daemon/run.js +11 -4
- package/src/projects/runtimes.js +1 -1
- package/src/ufoo/agentRegistryDiagnostics.js +43 -0
- package/src/ui/MIGRATION.md +382 -0
- package/src/ui/components/ChatApp.js +2950 -0
- package/src/ui/components/DashboardBar.js +417 -0
- package/src/ui/components/InkDemo.js +96 -0
- package/src/ui/components/MultilineInput.js +387 -0
- package/src/ui/components/UcodeApp.js +813 -0
- package/src/ui/components/agentMirror.js +725 -0
- package/src/ui/components/chatReducer.js +337 -0
- package/src/ui/format/index.js +997 -0
- package/src/ui/index.js +9 -0
- package/src/ui/runInk.js +57 -0
- package/src/utils/nodeExecutable.js +26 -0
package/src/code/tui.js
CHANGED
|
@@ -1,57 +1,40 @@
|
|
|
1
|
-
const
|
|
2
|
-
const pkg = require("../../package.json");
|
|
3
|
-
|
|
4
|
-
const UCODE_BANNER_LINES = [
|
|
5
|
-
"█ █ █▀▀ █▀█ █▀▄ █▀▀",
|
|
6
|
-
"█ █ █ █ █ █ █ █▀ ",
|
|
7
|
-
"▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀",
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
const UCODE_VERSION = String((pkg && pkg.version) || "dev");
|
|
11
|
-
|
|
12
|
-
// Status indicators
|
|
13
|
-
const STATUS_INDICATORS = {
|
|
14
|
-
thinking: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
15
|
-
typing: ["◐", "◓", "◑", "◒"],
|
|
16
|
-
waiting: ["∙", "∙∙", "∙∙∙", "∙∙", "∙"],
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
|
|
20
|
-
|
|
21
|
-
function charDisplayWidth(char = "") {
|
|
22
|
-
if (!char) return 0;
|
|
23
|
-
const code = char.codePointAt(0) || 0;
|
|
24
|
-
if (code === 0) return 0;
|
|
25
|
-
if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
|
|
26
|
-
if ((code >= 0x0300 && code <= 0x036f) ||
|
|
27
|
-
(code >= 0x1ab0 && code <= 0x1aff) ||
|
|
28
|
-
(code >= 0x1dc0 && code <= 0x1dff) ||
|
|
29
|
-
(code >= 0x20d0 && code <= 0x20ff) ||
|
|
30
|
-
(code >= 0xfe20 && code <= 0xfe2f)) {
|
|
31
|
-
return 0;
|
|
32
|
-
}
|
|
33
|
-
if ((code >= 0x1100 && code <= 0x115f) ||
|
|
34
|
-
code === 0x2329 ||
|
|
35
|
-
code === 0x232a ||
|
|
36
|
-
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
37
|
-
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
38
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
39
|
-
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
40
|
-
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
41
|
-
(code >= 0xff00 && code <= 0xff60) ||
|
|
42
|
-
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
43
|
-
(code >= 0x1f300 && code <= 0x1faff)) {
|
|
44
|
-
return 2;
|
|
45
|
-
}
|
|
46
|
-
return 1;
|
|
47
|
-
}
|
|
1
|
+
const fmt = require("../ui/format");
|
|
48
2
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
3
|
+
const {
|
|
4
|
+
STATUS_INDICATORS,
|
|
5
|
+
StreamBuffer,
|
|
6
|
+
UCODE_BANNER_LINES,
|
|
7
|
+
UCODE_VERSION,
|
|
8
|
+
buildMergedToolExpandedLines,
|
|
9
|
+
buildMergedToolSummaryText,
|
|
10
|
+
buildUcodeBannerLines,
|
|
11
|
+
clampCursorPos,
|
|
12
|
+
createEscapeTagStripper,
|
|
13
|
+
cycleAgentSelectionIndex,
|
|
14
|
+
deleteWordBeforeCursor,
|
|
15
|
+
displayCellWidth,
|
|
16
|
+
filterSelectableAgents,
|
|
17
|
+
findLogicalLineEnd,
|
|
18
|
+
findLogicalLineStart,
|
|
19
|
+
formatHighlightedUserInput,
|
|
20
|
+
formatPendingElapsed,
|
|
21
|
+
loadActiveAgents,
|
|
22
|
+
moveCursorByWord,
|
|
23
|
+
moveCursorHorizontally,
|
|
24
|
+
moveCursorToVisualLineBoundary,
|
|
25
|
+
moveCursorVertically,
|
|
26
|
+
normalizeBashToolCommand,
|
|
27
|
+
normalizeModelLabel,
|
|
28
|
+
normalizeToolMergeEntry,
|
|
29
|
+
parseActiveAgentsFromBusStatus,
|
|
30
|
+
renderLogLinesWithMarkdown,
|
|
31
|
+
resolveAgentSelectionOnDown,
|
|
32
|
+
resolveHistoryDownTransition,
|
|
33
|
+
shouldClearAgentSelectionOnUp,
|
|
34
|
+
shouldEnterAgentSelection,
|
|
35
|
+
shouldUseUcodeTui,
|
|
36
|
+
stripLeakedEscapeTags,
|
|
37
|
+
} = fmt;
|
|
55
38
|
|
|
56
39
|
function safeRead(getter, fallback = undefined) {
|
|
57
40
|
try {
|
|
@@ -75,109 +58,6 @@ function resolveLogContentWidth({ logBox = null, screen = null, fallback = 80 }
|
|
|
75
58
|
return Math.max(1, fallback);
|
|
76
59
|
}
|
|
77
60
|
|
|
78
|
-
function formatHighlightedUserInput(text = "", {
|
|
79
|
-
width = 80,
|
|
80
|
-
escapeText = (value) => String(value || ""),
|
|
81
|
-
} = {}) {
|
|
82
|
-
const plain = String(text || "").trim();
|
|
83
|
-
if (!plain) return "";
|
|
84
|
-
const targetWidth = Math.max(1, Math.floor(Number(width) || 80) - 1);
|
|
85
|
-
const prefix = " → ";
|
|
86
|
-
const suffix = " ";
|
|
87
|
-
const contentWidth = displayCellWidth(`${prefix}${plain}${suffix}`);
|
|
88
|
-
const pad = " ".repeat(Math.max(0, targetWidth - contentWidth));
|
|
89
|
-
return `{cyan-bg}{white-fg}${prefix}${escapeText(plain)}${suffix}${pad}{/white-fg}{/cyan-bg}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Stream buffer for smooth output
|
|
93
|
-
class StreamBuffer {
|
|
94
|
-
constructor(writer, options = {}) {
|
|
95
|
-
this.writer = writer;
|
|
96
|
-
this.buffer = "";
|
|
97
|
-
this.delay = options.delay || 8; // ms between chunks
|
|
98
|
-
this.chunkSize = options.chunkSize || 3; // chars per chunk
|
|
99
|
-
this.isStreaming = false;
|
|
100
|
-
this.streamPromise = null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async write(text) {
|
|
104
|
-
this.buffer += text;
|
|
105
|
-
if (!this.isStreaming) {
|
|
106
|
-
this.isStreaming = true;
|
|
107
|
-
this.streamPromise = this.flush();
|
|
108
|
-
}
|
|
109
|
-
return this.streamPromise;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async flush() {
|
|
113
|
-
while (this.buffer.length > 0) {
|
|
114
|
-
const chunk = this.buffer.slice(0, this.chunkSize);
|
|
115
|
-
this.buffer = this.buffer.slice(this.chunkSize);
|
|
116
|
-
this.writer(chunk);
|
|
117
|
-
if (this.buffer.length > 0) {
|
|
118
|
-
await new Promise(resolve => setTimeout(resolve, this.delay));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
this.isStreaming = false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async finish() {
|
|
125
|
-
if (this.isStreaming) {
|
|
126
|
-
await this.streamPromise;
|
|
127
|
-
}
|
|
128
|
-
if (this.buffer.length > 0) {
|
|
129
|
-
this.writer(this.buffer);
|
|
130
|
-
this.buffer = "";
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function normalizeModelLabel(model = "") {
|
|
136
|
-
const text = String(model || "").trim();
|
|
137
|
-
if (text) return text;
|
|
138
|
-
return "default";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
|
|
142
|
-
const modelLabel = normalizeModelLabel(model);
|
|
143
|
-
void width;
|
|
144
|
-
void engine; // Not using engine anymore
|
|
145
|
-
void nickname;
|
|
146
|
-
void agentId;
|
|
147
|
-
|
|
148
|
-
// Get current working directory with ~ for home
|
|
149
|
-
const path = require("path");
|
|
150
|
-
const os = require("os");
|
|
151
|
-
const currentDir = workspaceRoot || process.cwd();
|
|
152
|
-
const homeDir = os.homedir();
|
|
153
|
-
|
|
154
|
-
// Replace home directory with ~
|
|
155
|
-
let shortPath = currentDir;
|
|
156
|
-
if (currentDir.startsWith(homeDir)) {
|
|
157
|
-
shortPath = currentDir.replace(homeDir, "~");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const logoLines = UCODE_BANNER_LINES.map((line) => chalk.cyan(line));
|
|
161
|
-
const infoLines = [];
|
|
162
|
-
infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
|
|
163
|
-
infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
|
|
164
|
-
infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
|
|
165
|
-
const normalizedSessionId = String(sessionId || "").trim();
|
|
166
|
-
if (normalizedSessionId) {
|
|
167
|
-
infoLines.push(`${chalk.dim("Session:")} ${chalk.gray(normalizedSessionId)}`);
|
|
168
|
-
}
|
|
169
|
-
const logoPadding = " ".repeat(
|
|
170
|
-
UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
|
|
171
|
-
);
|
|
172
|
-
const rows = Math.max(logoLines.length, infoLines.length);
|
|
173
|
-
|
|
174
|
-
return Array.from({ length: rows }, (_, index) => {
|
|
175
|
-
const logoLine = logoLines[index] || logoPadding;
|
|
176
|
-
const info = infoLines[index] || "";
|
|
177
|
-
return ` ${logoLine} ${info}`;
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
61
|
function escapeBlessedLiteral(text) {
|
|
182
62
|
const raw = String(text == null ? "" : text);
|
|
183
63
|
const safe = raw.replace(/\{\/escape\}/g, "{open}/escape{close}");
|
|
@@ -195,7 +75,7 @@ function buildUcodeBannerBlessedLines({
|
|
|
195
75
|
} = {}) {
|
|
196
76
|
const modelLabel = normalizeModelLabel(model);
|
|
197
77
|
void width;
|
|
198
|
-
void engine;
|
|
78
|
+
void engine;
|
|
199
79
|
void nickname;
|
|
200
80
|
void agentId;
|
|
201
81
|
|
|
@@ -234,402 +114,15 @@ function buildUcodeBannerBlessedLines({
|
|
|
234
114
|
});
|
|
235
115
|
}
|
|
236
116
|
|
|
237
|
-
function
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
if (forceTui) return true;
|
|
241
|
-
return Boolean(stdin && stdin.isTTY && stdout && stdout.isTTY);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Helper function to load agents from bus
|
|
245
|
-
function parseActiveAgentsFromBusStatus(busStatus = "") {
|
|
246
|
-
const lines = String(busStatus || "").replace(ANSI_PATTERN, "").split(/\r?\n/);
|
|
247
|
-
const agents = [];
|
|
248
|
-
let inOnlineSection = false;
|
|
249
|
-
|
|
250
|
-
for (const line of lines) {
|
|
251
|
-
const trimmed = String(line || "").trim();
|
|
252
|
-
if (!trimmed) continue;
|
|
253
|
-
|
|
254
|
-
if (/^Online agents:\s*$/i.test(trimmed)) {
|
|
255
|
-
inOnlineSection = true;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (!inOnlineSection) continue;
|
|
259
|
-
|
|
260
|
-
if (/^\(none\)$/i.test(trimmed)) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Next heading means we have left the online agents section
|
|
265
|
-
if (/^[A-Za-z][A-Za-z ]+:\s*$/.test(trimmed)) {
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const rawId = trimmed.replace(/\s+\([^)]+\)\s*$/, "");
|
|
270
|
-
if (!rawId) continue;
|
|
271
|
-
const [type, ...idParts] = rawId.split(":");
|
|
272
|
-
const id = idParts.join(":");
|
|
273
|
-
if (!type) continue;
|
|
274
|
-
|
|
275
|
-
agents.push({
|
|
276
|
-
type,
|
|
277
|
-
id,
|
|
278
|
-
status: "active",
|
|
279
|
-
fullId: rawId,
|
|
280
|
-
nickname: (trimmed.match(/\(([^)]+)\)\s*$/) || [])[1] || "",
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Fallback for legacy output: "type:id (active|idle)"
|
|
285
|
-
if (agents.length === 0) {
|
|
286
|
-
for (const line of lines) {
|
|
287
|
-
const trimmed = String(line || "").trim();
|
|
288
|
-
const match = trimmed.match(/^([a-z-]+):([a-f0-9]+)\s+\((active|idle)\)$/);
|
|
289
|
-
if (!match) continue;
|
|
290
|
-
agents.push({
|
|
291
|
-
type: match[1],
|
|
292
|
-
id: match[2],
|
|
293
|
-
status: match[3],
|
|
294
|
-
fullId: `${match[1]}:${match[2]}`,
|
|
295
|
-
nickname: "",
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return agents;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function loadActiveAgents(workspaceRoot) {
|
|
304
|
-
try {
|
|
305
|
-
const { execSync } = require("child_process");
|
|
306
|
-
const busStatus = execSync("ufoo bus status", {
|
|
307
|
-
cwd: workspaceRoot,
|
|
308
|
-
encoding: "utf8",
|
|
309
|
-
});
|
|
310
|
-
return parseActiveAgentsFromBusStatus(busStatus);
|
|
311
|
-
} catch {
|
|
312
|
-
return [];
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) => String(value || "")) {
|
|
317
|
-
const { renderMarkdownLines } = require("../shared/markdownRenderer");
|
|
318
|
-
return renderMarkdownLines(text, state, escapeFn);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function shouldEnterAgentSelection(inputValue = "") {
|
|
322
|
-
const text = String(inputValue || "");
|
|
323
|
-
const trimmed = text.trim();
|
|
324
|
-
return !trimmed;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function resolveAgentSelectionOnDown({
|
|
328
|
-
agentSelectionMode = false,
|
|
329
|
-
selectedAgentIndex = -1,
|
|
330
|
-
totalAgents = 0,
|
|
331
|
-
} = {}) {
|
|
332
|
-
const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
|
|
333
|
-
if (total <= 0) return { action: "none", index: -1 };
|
|
334
|
-
if (agentSelectionMode) {
|
|
335
|
-
const keep = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
|
|
336
|
-
return { action: "hold", index: keep };
|
|
117
|
+
function runUcodeTui(props = {}) {
|
|
118
|
+
if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
|
|
119
|
+
return runUcodeBlessedTui(props);
|
|
337
120
|
}
|
|
338
|
-
const
|
|
339
|
-
return
|
|
121
|
+
const { runUcodeInkTui } = require("../ui/components/UcodeApp");
|
|
122
|
+
return runUcodeInkTui(props);
|
|
340
123
|
}
|
|
341
124
|
|
|
342
|
-
function
|
|
343
|
-
const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
|
|
344
|
-
if (total <= 0) return -1;
|
|
345
|
-
const current = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
|
|
346
|
-
if (direction === "left") {
|
|
347
|
-
return (current - 1 + total) % total;
|
|
348
|
-
}
|
|
349
|
-
return (current + 1) % total;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function shouldClearAgentSelectionOnUp({
|
|
353
|
-
agentSelectionMode = false,
|
|
354
|
-
inputValue = "",
|
|
355
|
-
} = {}) {
|
|
356
|
-
return Boolean(agentSelectionMode && shouldEnterAgentSelection(inputValue));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function moveCursorHorizontally(cursorPos = 0, inputValue = "", direction = "right") {
|
|
360
|
-
const text = String(inputValue || "");
|
|
361
|
-
const max = text.length;
|
|
362
|
-
const pos = Number.isFinite(cursorPos) ? Math.max(0, Math.floor(cursorPos)) : 0;
|
|
363
|
-
if (direction === "left") return Math.max(0, pos - 1);
|
|
364
|
-
return Math.min(max, pos + 1);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function clampCursorPos(cursorPos = 0, inputValue = "") {
|
|
368
|
-
const text = String(inputValue || "");
|
|
369
|
-
const pos = Number.isFinite(cursorPos) ? Math.floor(cursorPos) : 0;
|
|
370
|
-
return Math.max(0, Math.min(text.length, pos));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function findLogicalLineStart(inputValue = "", cursorPos = 0) {
|
|
374
|
-
const text = String(inputValue || "");
|
|
375
|
-
const pos = clampCursorPos(cursorPos, text);
|
|
376
|
-
const prevNewline = text.lastIndexOf("\n", Math.max(0, pos - 1));
|
|
377
|
-
return prevNewline === -1 ? 0 : prevNewline + 1;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function findLogicalLineEnd(inputValue = "", cursorPos = 0) {
|
|
381
|
-
const text = String(inputValue || "");
|
|
382
|
-
const pos = clampCursorPos(cursorPos, text);
|
|
383
|
-
const nextNewline = text.indexOf("\n", pos);
|
|
384
|
-
return nextNewline === -1 ? text.length : nextNewline;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function moveCursorToVisualLineBoundary({
|
|
388
|
-
cursorPos = 0,
|
|
389
|
-
inputValue = "",
|
|
390
|
-
width = 80,
|
|
391
|
-
boundary = "start",
|
|
392
|
-
strWidth,
|
|
393
|
-
} = {}) {
|
|
394
|
-
const inputMath = require("../chat/inputMath");
|
|
395
|
-
const text = String(inputValue || "");
|
|
396
|
-
const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
397
|
-
const pos = clampCursorPos(cursorPos, text);
|
|
398
|
-
const { row } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
|
|
399
|
-
if (boundary === "end") {
|
|
400
|
-
return inputMath.getCursorPosForRowCol(text, row, normalizedWidth, normalizedWidth, strWidth);
|
|
401
|
-
}
|
|
402
|
-
return inputMath.getCursorPosForRowCol(text, row, 0, normalizedWidth, strWidth);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function moveCursorVertically({
|
|
406
|
-
cursorPos = 0,
|
|
407
|
-
inputValue = "",
|
|
408
|
-
width = 80,
|
|
409
|
-
direction = "down",
|
|
410
|
-
preferredCol = null,
|
|
411
|
-
strWidth,
|
|
412
|
-
} = {}) {
|
|
413
|
-
const inputMath = require("../chat/inputMath");
|
|
414
|
-
const text = String(inputValue || "");
|
|
415
|
-
const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
416
|
-
const pos = clampCursorPos(cursorPos, text);
|
|
417
|
-
const { row, col } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
|
|
418
|
-
const totalRows = inputMath.countLines(text, normalizedWidth, strWidth);
|
|
419
|
-
const targetCol = Number.isFinite(preferredCol) ? preferredCol : col;
|
|
420
|
-
|
|
421
|
-
if (direction === "up") {
|
|
422
|
-
if (row <= 0) {
|
|
423
|
-
return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "top" };
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
moved: true,
|
|
427
|
-
nextCursorPos: inputMath.getCursorPosForRowCol(text, row - 1, targetCol, normalizedWidth, strWidth),
|
|
428
|
-
preferredCol: targetCol,
|
|
429
|
-
boundary: "",
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (row >= totalRows - 1) {
|
|
434
|
-
return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "bottom" };
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
moved: true,
|
|
438
|
-
nextCursorPos: inputMath.getCursorPosForRowCol(text, row + 1, targetCol, normalizedWidth, strWidth),
|
|
439
|
-
preferredCol: targetCol,
|
|
440
|
-
boundary: "",
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function deleteWordBeforeCursor(inputValue = "", cursorPos = 0) {
|
|
445
|
-
const text = String(inputValue || "");
|
|
446
|
-
const pos = clampCursorPos(cursorPos, text);
|
|
447
|
-
if (pos <= 0) return { value: text, cursorPos: pos };
|
|
448
|
-
const before = text.slice(0, pos);
|
|
449
|
-
const after = text.slice(pos);
|
|
450
|
-
const match = before.match(/\s*\S+\s*$/);
|
|
451
|
-
const start = match ? pos - match[0].length : Math.max(0, pos - 1);
|
|
452
|
-
return {
|
|
453
|
-
value: before.slice(0, start) + after,
|
|
454
|
-
cursorPos: start,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function moveCursorByWord(inputValue = "", cursorPos = 0, direction = "forward") {
|
|
459
|
-
const text = String(inputValue || "");
|
|
460
|
-
const pos = clampCursorPos(cursorPos, text);
|
|
461
|
-
if (direction === "backward") {
|
|
462
|
-
const before = text.slice(0, pos);
|
|
463
|
-
const trimmedEnd = before.search(/\S\s*$/) >= 0 ? before.replace(/\s+$/, "") : before;
|
|
464
|
-
const match = trimmedEnd.match(/\S+$/);
|
|
465
|
-
return match ? trimmedEnd.length - match[0].length : 0;
|
|
466
|
-
}
|
|
467
|
-
const after = text.slice(pos);
|
|
468
|
-
const match = after.match(/^\s*\S+/);
|
|
469
|
-
return match ? Math.min(text.length, pos + match[0].length) : text.length;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function resolveHistoryDownTransition({
|
|
473
|
-
inputHistory = [],
|
|
474
|
-
historyIndex = 0,
|
|
475
|
-
currentValue = "",
|
|
476
|
-
} = {}) {
|
|
477
|
-
const history = Array.isArray(inputHistory) ? inputHistory : [];
|
|
478
|
-
if (history.length <= 0) {
|
|
479
|
-
return {
|
|
480
|
-
moved: false,
|
|
481
|
-
nextHistoryIndex: Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0,
|
|
482
|
-
nextValue: String(currentValue || ""),
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
const currentIndex = Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0;
|
|
486
|
-
const nextHistoryIndex = Math.min(history.length, currentIndex + 1);
|
|
487
|
-
const nextValue = nextHistoryIndex >= history.length ? "" : String(history[nextHistoryIndex] || "");
|
|
488
|
-
const moved = nextHistoryIndex !== currentIndex || nextValue !== String(currentValue || "");
|
|
489
|
-
return {
|
|
490
|
-
moved,
|
|
491
|
-
nextHistoryIndex,
|
|
492
|
-
nextValue,
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function filterSelectableAgents(agents = [], selfSubscriberId = "") {
|
|
497
|
-
const selfId = String(selfSubscriberId || "").trim();
|
|
498
|
-
const list = Array.isArray(agents) ? agents : [];
|
|
499
|
-
if (!selfId) {
|
|
500
|
-
return list.filter((agent) => {
|
|
501
|
-
const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
|
|
502
|
-
const type = String(agent && agent.type ? agent.type : "").trim();
|
|
503
|
-
if (fullId === "ufoo-agent") return false;
|
|
504
|
-
if (type === "ufoo-agent") return false;
|
|
505
|
-
return true;
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
return list.filter((agent) => {
|
|
509
|
-
const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
|
|
510
|
-
const type = String(agent && agent.type ? agent.type : "").trim();
|
|
511
|
-
if (!fullId) return true;
|
|
512
|
-
if (fullId === "ufoo-agent") return false;
|
|
513
|
-
if (type === "ufoo-agent") return false;
|
|
514
|
-
return fullId !== selfId;
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function stripLeakedEscapeTags(text = "") {
|
|
519
|
-
const source = String(text == null ? "" : text);
|
|
520
|
-
const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
|
|
521
|
-
const withoutDanglingEscape = withoutClosedTags.replace(/\{\s*\/?\s*escape[\s\S]*$/gi, "");
|
|
522
|
-
return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function findTrailingEscapeTagPrefix(text = "") {
|
|
526
|
-
const raw = String(text == null ? "" : text);
|
|
527
|
-
if (!raw) return "";
|
|
528
|
-
const windowSize = 40;
|
|
529
|
-
const tail = raw.slice(Math.max(0, raw.length - windowSize));
|
|
530
|
-
const braceIndex = tail.lastIndexOf("{");
|
|
531
|
-
if (braceIndex < 0) return "";
|
|
532
|
-
const suffix = tail.slice(braceIndex);
|
|
533
|
-
if (suffix.includes("}")) return "";
|
|
534
|
-
|
|
535
|
-
const compact = suffix.toLowerCase().replace(/\s+/g, "");
|
|
536
|
-
if (!compact.startsWith("{")) return "";
|
|
537
|
-
if (/^\{\/?e?s?c?a?p?e?[^}]*$/.test(compact)) {
|
|
538
|
-
return suffix;
|
|
539
|
-
}
|
|
540
|
-
return "";
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function createEscapeTagStripper() {
|
|
544
|
-
let carry = "";
|
|
545
|
-
|
|
546
|
-
return {
|
|
547
|
-
write(chunk = "") {
|
|
548
|
-
const incoming = String(chunk == null ? "" : chunk);
|
|
549
|
-
if (!incoming && !carry) return "";
|
|
550
|
-
const combined = `${carry}${incoming}`;
|
|
551
|
-
const trailing = findTrailingEscapeTagPrefix(combined);
|
|
552
|
-
const safeText = trailing
|
|
553
|
-
? combined.slice(0, combined.length - trailing.length)
|
|
554
|
-
: combined;
|
|
555
|
-
carry = trailing;
|
|
556
|
-
return stripLeakedEscapeTags(safeText);
|
|
557
|
-
},
|
|
558
|
-
flush() {
|
|
559
|
-
if (!carry) return "";
|
|
560
|
-
// carry only stores trailing prefixes of escape tags; do not emit it
|
|
561
|
-
// to avoid leaking partial markers like "{/escape" at stream end.
|
|
562
|
-
const rest = "";
|
|
563
|
-
carry = "";
|
|
564
|
-
return rest;
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function formatPendingElapsed(ms = 0) {
|
|
570
|
-
const totalSeconds = Math.max(0, Math.floor(Number(ms) / 1000));
|
|
571
|
-
return `${totalSeconds} s`;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function normalizeBashToolCommand(args = {}, payload = {}) {
|
|
575
|
-
const argObj = args && typeof args === "object" ? args : {};
|
|
576
|
-
const resObj = payload && typeof payload === "object" ? payload : {};
|
|
577
|
-
const command = String(argObj.command || argObj.cmd || "").trim();
|
|
578
|
-
const code = Number.isFinite(resObj.code) ? `exit ${resObj.code}` : "";
|
|
579
|
-
return [command, code].filter(Boolean).join(" · ");
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function normalizeToolMergeEntry(entry = {}) {
|
|
583
|
-
const source = entry && typeof entry === "object" ? entry : {};
|
|
584
|
-
const tool = String(source.tool || "").trim().toLowerCase() || "tool";
|
|
585
|
-
const detail = String(source.detail || "").trim();
|
|
586
|
-
const isError = Boolean(source.isError);
|
|
587
|
-
const errorText = String(source.errorText || "").trim();
|
|
588
|
-
const summary = [tool, detail].filter(Boolean).join(" · ") || tool;
|
|
589
|
-
return {
|
|
590
|
-
tool,
|
|
591
|
-
detail,
|
|
592
|
-
isError,
|
|
593
|
-
errorText,
|
|
594
|
-
summary,
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function buildMergedToolSummaryText(entries = []) {
|
|
599
|
-
const list = Array.isArray(entries)
|
|
600
|
-
? entries.map((item) => normalizeToolMergeEntry(item))
|
|
601
|
-
: [];
|
|
602
|
-
const count = list.length;
|
|
603
|
-
if (count <= 0) return "Ran tool";
|
|
604
|
-
const first = list[0];
|
|
605
|
-
if (count === 1) return `Ran ${first.summary}`;
|
|
606
|
-
const errorCount = list.filter((item) => item.isError).length;
|
|
607
|
-
const errorSuffix = errorCount > 0 ? ` · ${errorCount} error${errorCount === 1 ? "" : "s"}` : "";
|
|
608
|
-
return `Ran ${first.summary} · … +${count - 1} calls${errorSuffix}`;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function buildMergedToolExpandedLines(entries = []) {
|
|
612
|
-
const list = Array.isArray(entries)
|
|
613
|
-
? entries.map((item) => normalizeToolMergeEntry(item))
|
|
614
|
-
: [];
|
|
615
|
-
const maxLength = 120; // Max length for expanded lines
|
|
616
|
-
return list.map((item, index) => {
|
|
617
|
-
const base = item.summary;
|
|
618
|
-
let line;
|
|
619
|
-
if (!item.isError) {
|
|
620
|
-
line = base;
|
|
621
|
-
} else {
|
|
622
|
-
line = item.errorText ? `${base} · error: ${item.errorText}` : `${base} · error`;
|
|
623
|
-
}
|
|
624
|
-
// Truncate long lines
|
|
625
|
-
if (line.length > maxLength) {
|
|
626
|
-
return line.slice(0, maxLength - 3) + "...";
|
|
627
|
-
}
|
|
628
|
-
return line;
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function runUcodeTui({
|
|
125
|
+
function runUcodeBlessedTui({
|
|
633
126
|
stdin = process.stdin,
|
|
634
127
|
stdout = process.stdout,
|
|
635
128
|
runSingleCommand = () => ({ kind: "empty" }),
|
|
@@ -1615,29 +1108,44 @@ function runUcodeTui({
|
|
|
1615
1108
|
}
|
|
1616
1109
|
|
|
1617
1110
|
if (result.kind === "nl") {
|
|
1618
|
-
const statusMessages = [
|
|
1619
|
-
"Thinking...",
|
|
1620
|
-
"Processing your request...",
|
|
1621
|
-
"Analyzing...",
|
|
1622
|
-
"Working on it...",
|
|
1623
|
-
];
|
|
1624
|
-
const randomStatus = statusMessages[Math.floor(Math.random() * statusMessages.length)];
|
|
1625
1111
|
const abortController = new AbortController();
|
|
1626
1112
|
const escapeStripper = createEscapeTagStripper();
|
|
1627
1113
|
pendingTask = {
|
|
1628
1114
|
abortController,
|
|
1629
1115
|
startedAt: Date.now(),
|
|
1630
1116
|
};
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1117
|
+
const TOOL_LABELS = {
|
|
1118
|
+
read: "Reading file",
|
|
1119
|
+
write: "Writing file",
|
|
1120
|
+
edit: "Editing file",
|
|
1121
|
+
bash: "Running command",
|
|
1122
|
+
};
|
|
1123
|
+
const setNlStatus = (msg) => {
|
|
1124
|
+
updateStatus(msg, "thinking", {
|
|
1125
|
+
showTimer: true,
|
|
1126
|
+
startedAt: pendingTask.startedAt,
|
|
1127
|
+
});
|
|
1128
|
+
};
|
|
1129
|
+
setNlStatus("Waiting for model...");
|
|
1635
1130
|
let streamState = null;
|
|
1636
1131
|
let renderedToolLogCount = 0;
|
|
1637
1132
|
let nlResult = null;
|
|
1638
1133
|
try {
|
|
1639
1134
|
nlResult = await runNaturalLanguageTask(result.task, state, {
|
|
1640
1135
|
signal: abortController.signal,
|
|
1136
|
+
onPhase: (event) => {
|
|
1137
|
+
if (!event || typeof event !== "object") return;
|
|
1138
|
+
if (event.type === "request_start") {
|
|
1139
|
+
setNlStatus("Waiting for model...");
|
|
1140
|
+
} else if (event.type === "thinking_delta") {
|
|
1141
|
+
setNlStatus("Thinking...");
|
|
1142
|
+
} else if (event.type === "text_delta") {
|
|
1143
|
+
setNlStatus("Generating response...");
|
|
1144
|
+
} else if (event.type === "tool_request") {
|
|
1145
|
+
const label = TOOL_LABELS[String(event.name || "").toLowerCase()] || `Calling ${event.name}`;
|
|
1146
|
+
setNlStatus(`${label}...`);
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1641
1149
|
onDelta: (delta) => {
|
|
1642
1150
|
const text = escapeStripper.write(String(delta || ""));
|
|
1643
1151
|
if (!text) return;
|
|
@@ -1648,6 +1156,10 @@ function runUcodeTui({
|
|
|
1648
1156
|
},
|
|
1649
1157
|
onToolLog: (entry) => {
|
|
1650
1158
|
renderedToolLogCount += 1;
|
|
1159
|
+
if (entry && entry.tool && entry.phase === "start") {
|
|
1160
|
+
const label = TOOL_LABELS[String(entry.tool || "").toLowerCase()] || `Calling ${entry.tool}`;
|
|
1161
|
+
setNlStatus(`${label}...`);
|
|
1162
|
+
}
|
|
1651
1163
|
logToolHint(entry);
|
|
1652
1164
|
},
|
|
1653
1165
|
});
|