u-foo 2.3.31 → 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/taskDecomposer.js +5 -4
- package/src/code/tui.js +43 -550
- 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 };
|
|
337
|
-
}
|
|
338
|
-
const enter = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
|
|
339
|
-
return { action: "enter", index: enter };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function cycleAgentSelectionIndex(selectedAgentIndex = -1, totalAgents = 0, direction = "right") {
|
|
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;
|
|
117
|
+
function runUcodeTui(props = {}) {
|
|
118
|
+
if (String(process.env.UFOO_TUI || "").trim().toLowerCase() === "blessed") {
|
|
119
|
+
return runUcodeBlessedTui(props);
|
|
539
120
|
}
|
|
540
|
-
|
|
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
|
-
});
|
|
121
|
+
const { runUcodeInkTui } = require("../ui/components/UcodeApp");
|
|
122
|
+
return runUcodeInkTui(props);
|
|
630
123
|
}
|
|
631
124
|
|
|
632
|
-
function
|
|
125
|
+
function runUcodeBlessedTui({
|
|
633
126
|
stdin = process.stdin,
|
|
634
127
|
stdout = process.stdout,
|
|
635
128
|
runSingleCommand = () => ({ kind: "empty" }),
|