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
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiline text input for the ink-based ucode TUI.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the behaviour of the blessed `_listener` in src/code/tui.js, but
|
|
7
|
+
* built on ink's useInput. Cursor math is delegated to src/ui/format so the
|
|
8
|
+
* legacy and ink editors stay in sync (and so the existing jest coverage of
|
|
9
|
+
* those helpers protects this component too).
|
|
10
|
+
*
|
|
11
|
+
* Props:
|
|
12
|
+
* value (string) text contents (controlled)
|
|
13
|
+
* onChange(nextValue) called when value changes
|
|
14
|
+
* onSubmit(value) Enter pressed without modifiers, no trailing `\`
|
|
15
|
+
* onCancel() Esc pressed; the parent decides what to do
|
|
16
|
+
* (e.g. abort an in-flight task). The component
|
|
17
|
+
* does NOT mutate `value` on cancel.
|
|
18
|
+
* onArrowUpAtTop(value) cursor on the first visual row, Up pressed
|
|
19
|
+
* onArrowDownAtBottom(value) cursor on the last visual row, Down pressed
|
|
20
|
+
* onArrowLeftAtEmpty(value) Left pressed while value is empty
|
|
21
|
+
* onArrowRightAtEmpty(value) Right pressed while value is empty
|
|
22
|
+
* width (number) wrap width in display cells
|
|
23
|
+
* interactive (boolean) gates useInput; pass false for non-TTY mounts
|
|
24
|
+
* interceptArrowsAndEnter (boolean)
|
|
25
|
+
* when true, Up/Down/Left/Right/Return are
|
|
26
|
+
* suppressed inside the editor so a parent
|
|
27
|
+
* component (e.g. completion popup) can
|
|
28
|
+
* handle them. Plain editing keys still work.
|
|
29
|
+
* placeholder (string) rendered in gray when value is empty
|
|
30
|
+
*
|
|
31
|
+
* Newlines: Enter submits. Use Alt+Enter (delivered as meta+Return) or end the
|
|
32
|
+
* line with `\` (the legacy continuation trick) to insert a literal newline.
|
|
33
|
+
* We do NOT rely on Shift+Enter — many terminals don't distinguish it from
|
|
34
|
+
* plain Enter, so it would silently submit.
|
|
35
|
+
*
|
|
36
|
+
* Bracketed paste arrives as a multi-byte `input` chunk in useInput; we route
|
|
37
|
+
* it through insertText, so multi-line paste already works without extra code.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const fmt = require("../format");
|
|
41
|
+
|
|
42
|
+
function createMultilineInput({ React, ink }) {
|
|
43
|
+
const { useState, useCallback, useMemo, useEffect } = React;
|
|
44
|
+
const { Box, Text, useInput } = ink;
|
|
45
|
+
const h = React.createElement;
|
|
46
|
+
|
|
47
|
+
return function MultilineInput({
|
|
48
|
+
value = "",
|
|
49
|
+
valueVersion = 0,
|
|
50
|
+
onChange = () => {},
|
|
51
|
+
onSubmit = () => {},
|
|
52
|
+
onCancel = () => {},
|
|
53
|
+
onArrowUpAtTop,
|
|
54
|
+
onArrowDownAtBottom,
|
|
55
|
+
onArrowLeftAtEmpty,
|
|
56
|
+
onArrowRightAtEmpty,
|
|
57
|
+
width = 80,
|
|
58
|
+
interactive = true,
|
|
59
|
+
interceptArrowsAndEnter = false,
|
|
60
|
+
placeholder = "",
|
|
61
|
+
promptPrefix = "› ",
|
|
62
|
+
promptColor = "magenta",
|
|
63
|
+
borderColor = "gray",
|
|
64
|
+
}) {
|
|
65
|
+
// Cursor is owned by this component. preferredCol tracks the visual
|
|
66
|
+
// column we want to keep when bouncing across lines of different widths
|
|
67
|
+
// via Up/Down.
|
|
68
|
+
const [cursorState, setCursorState] = useState(() => String(value || "").length);
|
|
69
|
+
const [preferredCol, setPreferredCol] = useState(null);
|
|
70
|
+
const cursorPos = fmt.clampCursorPos(cursorState, value);
|
|
71
|
+
|
|
72
|
+
// When the parent forces a new value via valueVersion (e.g. accepting
|
|
73
|
+
// a completion), park the cursor at the end of the freshly inserted
|
|
74
|
+
// text so the user can keep typing without arrow-keying back to the
|
|
75
|
+
// tail.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setCursorState(String(value || "").length);
|
|
78
|
+
setPreferredCol(null);
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, [valueVersion]);
|
|
81
|
+
|
|
82
|
+
const wrapWidth = Math.max(1, Math.floor(Number(width) || 80));
|
|
83
|
+
|
|
84
|
+
const setCursor = useCallback((next) => {
|
|
85
|
+
setCursorState(fmt.clampCursorPos(next, value));
|
|
86
|
+
}, [value]);
|
|
87
|
+
const resetPreferredCol = useCallback(() => setPreferredCol(null), []);
|
|
88
|
+
|
|
89
|
+
const change = useCallback((nextValue, nextCursor) => {
|
|
90
|
+
const clamped = fmt.clampCursorPos(nextCursor, nextValue);
|
|
91
|
+
setCursorState(clamped);
|
|
92
|
+
onChange(nextValue);
|
|
93
|
+
}, [onChange]);
|
|
94
|
+
|
|
95
|
+
const insertText = useCallback((text) => {
|
|
96
|
+
const before = value.slice(0, cursorPos);
|
|
97
|
+
const after = value.slice(cursorPos);
|
|
98
|
+
change(`${before}${text}${after}`, cursorPos + text.length);
|
|
99
|
+
setPreferredCol(null);
|
|
100
|
+
}, [value, cursorPos, change]);
|
|
101
|
+
|
|
102
|
+
const replaceRange = useCallback((start, end, text) => {
|
|
103
|
+
const safeStart = Math.max(0, Math.min(value.length, start));
|
|
104
|
+
const safeEnd = Math.max(safeStart, Math.min(value.length, end));
|
|
105
|
+
const next = `${value.slice(0, safeStart)}${text}${value.slice(safeEnd)}`;
|
|
106
|
+
change(next, safeStart + text.length);
|
|
107
|
+
setPreferredCol(null);
|
|
108
|
+
}, [value, change]);
|
|
109
|
+
|
|
110
|
+
const deleteBefore = useCallback(() => {
|
|
111
|
+
if (cursorPos <= 0) return;
|
|
112
|
+
replaceRange(cursorPos - 1, cursorPos, "");
|
|
113
|
+
}, [cursorPos, replaceRange]);
|
|
114
|
+
|
|
115
|
+
const deleteAt = useCallback(() => {
|
|
116
|
+
if (cursorPos >= value.length) return;
|
|
117
|
+
replaceRange(cursorPos, cursorPos + 1, "");
|
|
118
|
+
}, [cursorPos, value.length, replaceRange]);
|
|
119
|
+
|
|
120
|
+
const deleteToBoundary = useCallback((boundary) => {
|
|
121
|
+
const target = fmt.moveCursorToVisualLineBoundary({
|
|
122
|
+
cursorPos,
|
|
123
|
+
inputValue: value,
|
|
124
|
+
width: wrapWidth,
|
|
125
|
+
boundary,
|
|
126
|
+
});
|
|
127
|
+
const start = Math.min(cursorPos, target);
|
|
128
|
+
const end = Math.max(cursorPos, target);
|
|
129
|
+
if (start === end) return;
|
|
130
|
+
replaceRange(start, end, "");
|
|
131
|
+
}, [cursorPos, value, wrapWidth, replaceRange]);
|
|
132
|
+
|
|
133
|
+
const deleteWordBefore = useCallback(() => {
|
|
134
|
+
const next = fmt.deleteWordBeforeCursor(value, cursorPos);
|
|
135
|
+
change(next.value, next.cursorPos);
|
|
136
|
+
setPreferredCol(null);
|
|
137
|
+
}, [value, cursorPos, change]);
|
|
138
|
+
|
|
139
|
+
const deleteWordAfter = useCallback(() => {
|
|
140
|
+
const end = fmt.moveCursorByWord(value, cursorPos, "forward");
|
|
141
|
+
replaceRange(cursorPos, end, "");
|
|
142
|
+
}, [value, cursorPos, replaceRange]);
|
|
143
|
+
|
|
144
|
+
useInput((input, key) => {
|
|
145
|
+
// Let the parent absorb arrow keys + Enter when a popup (e.g. the
|
|
146
|
+
// completion list) is open. We still process plain text input so
|
|
147
|
+
// typing continues to filter the popup live.
|
|
148
|
+
if (interceptArrowsAndEnter && (
|
|
149
|
+
key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return
|
|
150
|
+
)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Submit: Enter without modifiers, except after a trailing backslash
|
|
154
|
+
// (which is the legacy "\\\n" continuation trick). Alt+Enter inserts
|
|
155
|
+
// a literal newline. Shift+Enter is intentionally NOT used: many
|
|
156
|
+
// terminals don't distinguish it from plain Enter.
|
|
157
|
+
if (key.return) {
|
|
158
|
+
if (key.meta) {
|
|
159
|
+
insertText("\n");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (cursorPos > 0 && value[cursorPos - 1] === "\\") {
|
|
163
|
+
replaceRange(cursorPos - 1, cursorPos, "\n");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
onSubmit(value);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (key.escape) {
|
|
170
|
+
onCancel();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key.ctrl) {
|
|
174
|
+
if (input === "a") {
|
|
175
|
+
setCursor(fmt.moveCursorToVisualLineBoundary({
|
|
176
|
+
cursorPos, inputValue: value, width: wrapWidth, boundary: "start",
|
|
177
|
+
}));
|
|
178
|
+
resetPreferredCol();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (input === "e") {
|
|
182
|
+
setCursor(fmt.moveCursorToVisualLineBoundary({
|
|
183
|
+
cursorPos, inputValue: value, width: wrapWidth, boundary: "end",
|
|
184
|
+
}));
|
|
185
|
+
resetPreferredCol();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (input === "b") {
|
|
189
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "left"));
|
|
190
|
+
resetPreferredCol();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (input === "f") {
|
|
194
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "right"));
|
|
195
|
+
resetPreferredCol();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (input === "d") { deleteAt(); return; }
|
|
199
|
+
if (input === "h") { deleteBefore(); return; }
|
|
200
|
+
if (input === "k") { deleteToBoundary("end"); return; }
|
|
201
|
+
if (input === "u") { deleteToBoundary("start"); return; }
|
|
202
|
+
if (input === "w") { deleteWordBefore(); return; }
|
|
203
|
+
// Ctrl+C is parent's responsibility (typically exits the app).
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key.meta) {
|
|
207
|
+
if (input === "b") {
|
|
208
|
+
setCursor(fmt.moveCursorByWord(value, cursorPos, "backward"));
|
|
209
|
+
resetPreferredCol();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (input === "f") {
|
|
213
|
+
setCursor(fmt.moveCursorByWord(value, cursorPos, "forward"));
|
|
214
|
+
resetPreferredCol();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (input === "d") { deleteWordAfter(); return; }
|
|
218
|
+
}
|
|
219
|
+
if (key.backspace) {
|
|
220
|
+
if (key.meta || key.ctrl) deleteWordBefore();
|
|
221
|
+
else deleteBefore();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (key.delete) {
|
|
225
|
+
// ink reports key.delete for the 0x7F byte that most terminals send
|
|
226
|
+
// when the user presses the top-left Delete key (a.k.a. Backspace on
|
|
227
|
+
// non-Mac keyboards). Treat it as "delete the character before the
|
|
228
|
+
// cursor" by default. Real forward-delete (Fn+Delete on macOS) sends
|
|
229
|
+
// an escape sequence and ink also sets key.delete with no leading
|
|
230
|
+
// input — we can't reliably tell them apart, so favour the much
|
|
231
|
+
// more common backspace semantics. Meta+Delete still maps to
|
|
232
|
+
// delete-to-line-end as before.
|
|
233
|
+
if (key.meta) deleteToBoundary("end");
|
|
234
|
+
else deleteBefore();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (key.leftArrow) {
|
|
238
|
+
if (!value && typeof onArrowLeftAtEmpty === "function") {
|
|
239
|
+
onArrowLeftAtEmpty(value);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "left"));
|
|
243
|
+
resetPreferredCol();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (key.rightArrow) {
|
|
247
|
+
if (!value && typeof onArrowRightAtEmpty === "function") {
|
|
248
|
+
onArrowRightAtEmpty(value);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
setCursor(fmt.moveCursorHorizontally(cursorPos, value, "right"));
|
|
252
|
+
resetPreferredCol();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (key.upArrow) {
|
|
256
|
+
if (value) {
|
|
257
|
+
const move = fmt.moveCursorVertically({
|
|
258
|
+
cursorPos, inputValue: value, width: wrapWidth,
|
|
259
|
+
direction: "up", preferredCol,
|
|
260
|
+
});
|
|
261
|
+
setPreferredCol(move.preferredCol);
|
|
262
|
+
if (move.moved) {
|
|
263
|
+
setCursor(move.nextCursorPos);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (typeof onArrowUpAtTop === "function") onArrowUpAtTop(value);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.downArrow) {
|
|
271
|
+
if (value) {
|
|
272
|
+
const move = fmt.moveCursorVertically({
|
|
273
|
+
cursorPos, inputValue: value, width: wrapWidth,
|
|
274
|
+
direction: "down", preferredCol,
|
|
275
|
+
});
|
|
276
|
+
setPreferredCol(move.preferredCol);
|
|
277
|
+
if (move.moved) {
|
|
278
|
+
setCursor(move.nextCursorPos);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (typeof onArrowDownAtBottom === "function") onArrowDownAtBottom(value);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Plain character / paste. Filter control bytes.
|
|
287
|
+
if (input && !key.ctrl && !key.meta) {
|
|
288
|
+
const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
|
|
289
|
+
if (filtered) insertText(filtered);
|
|
290
|
+
}
|
|
291
|
+
}, { isActive: interactive });
|
|
292
|
+
|
|
293
|
+
// Render: split into logical lines, then split each into visual rows by
|
|
294
|
+
// wrap width. Highlight one cell at the cursor location. With a
|
|
295
|
+
// placeholder, we still draw the cursor (visible at offset 0) and append
|
|
296
|
+
// the placeholder text in gray after it.
|
|
297
|
+
const showPlaceholder = !value && !!placeholder;
|
|
298
|
+
const visualRows = useMemo(
|
|
299
|
+
() => layoutRows(value, wrapWidth, cursorPos),
|
|
300
|
+
[value, wrapWidth, cursorPos]
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return h(Box, {
|
|
304
|
+
borderStyle: "single",
|
|
305
|
+
borderTop: true,
|
|
306
|
+
borderBottom: true,
|
|
307
|
+
borderLeft: false,
|
|
308
|
+
borderRight: false,
|
|
309
|
+
borderColor,
|
|
310
|
+
flexDirection: "column",
|
|
311
|
+
width: "100%",
|
|
312
|
+
},
|
|
313
|
+
...visualRows.map((row, idx) =>
|
|
314
|
+
h(Box, { key: `row-${idx}` },
|
|
315
|
+
idx === 0 ? h(Text, { color: promptColor }, promptPrefix) : h(Text, null, " "),
|
|
316
|
+
...row.segments.map((seg, segIdx) =>
|
|
317
|
+
h(Text, {
|
|
318
|
+
key: `s-${segIdx}`,
|
|
319
|
+
inverse: seg.cursor,
|
|
320
|
+
color: showPlaceholder && idx === 0 && segIdx === 0 ? "gray" : undefined,
|
|
321
|
+
}, seg.text)
|
|
322
|
+
),
|
|
323
|
+
showPlaceholder && idx === 0
|
|
324
|
+
? h(Text, { color: "gray" }, placeholder)
|
|
325
|
+
: null,
|
|
326
|
+
)
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Lay out `value` into visual rows respecting `width`, and mark the cell at
|
|
334
|
+
* `cursor` so the renderer can invert it. Returns:
|
|
335
|
+
* [{ segments: [{ text, cursor }] }, ...]
|
|
336
|
+
*
|
|
337
|
+
* Cursor at end-of-input is rendered as an inverted space appended to the
|
|
338
|
+
* final row. Newlines split rows but never appear in segments. Pass
|
|
339
|
+
* `cursor < 0` to suppress the cursor entirely (used in placeholder mode).
|
|
340
|
+
*/
|
|
341
|
+
function layoutRows(value, width, cursor) {
|
|
342
|
+
const text = String(value == null ? "" : value);
|
|
343
|
+
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
|
344
|
+
const rawCursor = Number(cursor);
|
|
345
|
+
const showCursor = Number.isFinite(rawCursor) && rawCursor >= 0;
|
|
346
|
+
const cursorIdx = showCursor
|
|
347
|
+
? Math.min(text.length, Math.floor(rawCursor))
|
|
348
|
+
: -1;
|
|
349
|
+
|
|
350
|
+
const rows = [];
|
|
351
|
+
let row = { segments: [], cellsUsed: 0, cursorPlaced: false };
|
|
352
|
+
const pushRow = () => {
|
|
353
|
+
rows.push(row);
|
|
354
|
+
row = { segments: [], cellsUsed: 0, cursorPlaced: false };
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
358
|
+
const ch = text[i];
|
|
359
|
+
if (ch === "\n") {
|
|
360
|
+
if (cursorIdx === i && !row.cursorPlaced) {
|
|
361
|
+
row.segments.push({ text: " ", cursor: true });
|
|
362
|
+
row.cursorPlaced = true;
|
|
363
|
+
}
|
|
364
|
+
pushRow();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const w = fmt.displayCellWidth(ch);
|
|
368
|
+
if (row.cellsUsed + w > safeWidth) pushRow();
|
|
369
|
+
if (cursorIdx === i) {
|
|
370
|
+
row.segments.push({ text: ch, cursor: true });
|
|
371
|
+
row.cursorPlaced = true;
|
|
372
|
+
} else {
|
|
373
|
+
row.segments.push({ text: ch, cursor: false });
|
|
374
|
+
}
|
|
375
|
+
row.cellsUsed += w;
|
|
376
|
+
}
|
|
377
|
+
if (cursorIdx === text.length && !row.cursorPlaced) {
|
|
378
|
+
row.segments.push({ text: " ", cursor: true });
|
|
379
|
+
}
|
|
380
|
+
rows.push(row);
|
|
381
|
+
return rows;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = {
|
|
385
|
+
createMultilineInput,
|
|
386
|
+
layoutRows,
|
|
387
|
+
};
|