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
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -165,38 +165,58 @@ function loadChatHistory(projectRoot, cap = 200, options = {}) {
|
|
|
165
165
|
const raw = fs.readFileSync(file, "utf8");
|
|
166
166
|
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
167
167
|
const out = [];
|
|
168
|
-
const pushLine = (line = "") => {
|
|
168
|
+
const pushLine = (line = "", sourceType = "") => {
|
|
169
169
|
const value = String(line || "");
|
|
170
170
|
if (!value.trim()) {
|
|
171
|
-
if (out.length > 0
|
|
171
|
+
if (out.length > 0) {
|
|
172
|
+
const last = out[out.length - 1];
|
|
173
|
+
const lastText = typeof last === "object" ? last.text : last;
|
|
174
|
+
if (lastText !== "") out.push({ text: "", sourceType: sourceType || "system" });
|
|
175
|
+
}
|
|
172
176
|
return;
|
|
173
177
|
}
|
|
174
|
-
out.push(value);
|
|
178
|
+
out.push(sourceType ? { text: value, sourceType } : value);
|
|
175
179
|
};
|
|
176
180
|
for (const line of lines) {
|
|
177
181
|
try {
|
|
178
182
|
const entry = JSON.parse(line);
|
|
179
183
|
if (!entry) continue;
|
|
180
184
|
if (entry.type === "spacer") {
|
|
181
|
-
pushLine("");
|
|
185
|
+
pushLine("", "system");
|
|
182
186
|
continue;
|
|
183
187
|
}
|
|
184
188
|
const text = String(entry.text || "");
|
|
185
189
|
if (!text) continue;
|
|
190
|
+
const sourceType = String(entry.type || "");
|
|
186
191
|
// Strip blessed-tag markup that the legacy log writer used; ink
|
|
187
192
|
// can't render those tags and we don't want them shown literally.
|
|
188
193
|
const stripped = text.replace(/\{[^{}]+\}/g, "");
|
|
189
194
|
for (const renderedLine of normalizeInkLogLines(stripped)) {
|
|
190
|
-
pushLine(renderedLine);
|
|
195
|
+
pushLine(renderedLine, sourceType);
|
|
191
196
|
}
|
|
192
197
|
} catch {
|
|
193
198
|
// ignore malformed lines
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
|
-
while (out.length > 0
|
|
197
|
-
|
|
201
|
+
while (out.length > 0) {
|
|
202
|
+
const first = out[0];
|
|
203
|
+
const firstText = typeof first === "object" ? first.text : first;
|
|
204
|
+
if (firstText !== "") break;
|
|
205
|
+
out.shift();
|
|
206
|
+
}
|
|
207
|
+
while (out.length > 0) {
|
|
208
|
+
const last = out[out.length - 1];
|
|
209
|
+
const lastText = typeof last === "object" ? last.text : last;
|
|
210
|
+
if (lastText !== "") break;
|
|
211
|
+
out.pop();
|
|
212
|
+
}
|
|
198
213
|
const capped = out.slice(-cap);
|
|
199
|
-
while (capped.length > 0
|
|
214
|
+
while (capped.length > 0) {
|
|
215
|
+
const first = capped[0];
|
|
216
|
+
const firstText = typeof first === "object" ? first.text : first;
|
|
217
|
+
if (firstText !== "") break;
|
|
218
|
+
capped.shift();
|
|
219
|
+
}
|
|
200
220
|
return capped;
|
|
201
221
|
} catch {
|
|
202
222
|
return [];
|
|
@@ -435,12 +455,26 @@ function createThrottledSender(send, windowMs = 500) {
|
|
|
435
455
|
// Kinds whose log entries render as a margin-bottom "transcript cell" in
|
|
436
456
|
// buildChatLogGroups. Kept in sync with canAppendToChatLogGroup in
|
|
437
457
|
// chatLogModel.js.
|
|
438
|
-
const STATIC_GROUPABLE_KINDS = new Set([
|
|
458
|
+
const STATIC_GROUPABLE_KINDS = new Set([
|
|
459
|
+
"assistant",
|
|
460
|
+
"agent",
|
|
461
|
+
"report",
|
|
462
|
+
"success",
|
|
463
|
+
"error",
|
|
464
|
+
"meta",
|
|
465
|
+
"system",
|
|
466
|
+
"plain",
|
|
467
|
+
]);
|
|
439
468
|
|
|
440
469
|
// Shared row colors for both the dynamic (stream) and <Static> renderers.
|
|
470
|
+
// Aligned with ucode LOG_LINE_TEXT_PROPS: user green+bold, system dim gray,
|
|
471
|
+
// team bus/agent cyan, ufoo assistant white/bold marker.
|
|
441
472
|
const CHAT_LOG_ROW_PALETTE = {
|
|
473
|
+
user: { marker: "green", speaker: "green", body: "green", bold: true },
|
|
442
474
|
assistant: { marker: "cyan", speaker: "white", body: undefined, bold: true },
|
|
443
475
|
agent: { marker: "cyan", speaker: "cyan", body: undefined, bold: false },
|
|
476
|
+
report: { marker: "yellow", speaker: "yellow", body: undefined, bold: false },
|
|
477
|
+
system: { marker: "gray", speaker: "gray", body: "gray", bold: false, dim: true },
|
|
444
478
|
error: { marker: "red", speaker: "red", body: "red", bold: true },
|
|
445
479
|
success: { marker: "green", speaker: "green", body: "green", bold: false },
|
|
446
480
|
divider: { marker: "gray", speaker: "gray", body: "gray", bold: false },
|
|
@@ -460,21 +494,39 @@ function decorateStaticLogEntry(prev, entry) {
|
|
|
460
494
|
const markdownState = prev && prev.markdownState && typeof prev.markdownState === "object"
|
|
461
495
|
? { inCodeBlock: Boolean(prev.markdownState.inCodeBlock) }
|
|
462
496
|
: { inCodeBlock: false };
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
497
|
+
const source = entry && typeof entry === "object" ? entry : { text: entry };
|
|
498
|
+
const sourceText = source.text != null ? String(source.text) : String(entry || "");
|
|
499
|
+
const sourceType = String(source.sourceType || source.type || "");
|
|
500
|
+
const meta = source.meta && typeof source.meta === "object" ? source.meta : {};
|
|
501
|
+
const row = buildChatLogLineModel({
|
|
502
|
+
...source,
|
|
503
|
+
text: sourceText,
|
|
504
|
+
sourceType,
|
|
505
|
+
meta,
|
|
506
|
+
}, { markdownState, sourceType, meta });
|
|
467
507
|
const continuation = Boolean(
|
|
468
508
|
prev
|
|
469
|
-
&& (
|
|
470
|
-
|
|
509
|
+
&& (
|
|
510
|
+
((row.kind === "plain" || row.kind === "spacer") && STATIC_GROUPABLE_KINDS.has(prev.groupKind))
|
|
511
|
+
|| (prev.groupKind === "user" && row.kind === "user" && row.marker !== "›")
|
|
512
|
+
)
|
|
471
513
|
);
|
|
472
514
|
const groupKind = continuation ? prev.groupKind : row.kind;
|
|
473
515
|
// A gap belongs between visual blocks: only on entries that START a new
|
|
474
516
|
// block, and only when the previous block was a transcript group (whose
|
|
475
517
|
// old dynamic renderer contributed a trailing marginBottom).
|
|
476
|
-
|
|
477
|
-
|
|
518
|
+
// User turns also get a leading gap so › prompts don't sit flush against
|
|
519
|
+
// the previous transcript cell (ucode parity).
|
|
520
|
+
const marginBefore = Boolean(
|
|
521
|
+
!continuation
|
|
522
|
+
&& prev
|
|
523
|
+
&& (
|
|
524
|
+
STATIC_GROUPABLE_KINDS.has(prev.groupKind)
|
|
525
|
+
|| prev.groupKind === "user"
|
|
526
|
+
|| row.kind === "user"
|
|
527
|
+
)
|
|
528
|
+
);
|
|
529
|
+
return { entry: source, row, groupKind, continuation, marginBefore, markdownState };
|
|
478
530
|
}
|
|
479
531
|
|
|
480
532
|
function createInkStreamState({
|
|
@@ -1315,7 +1367,15 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1315
1367
|
}
|
|
1316
1368
|
const lines = normalizeInkLogLines(text);
|
|
1317
1369
|
if (lines.length === 0) return;
|
|
1318
|
-
|
|
1370
|
+
const payload = lines.map((line, index) => ({
|
|
1371
|
+
text: line,
|
|
1372
|
+
type,
|
|
1373
|
+
sourceType: type,
|
|
1374
|
+
// Attach router meta only on the first physical line so multi-line
|
|
1375
|
+
// bus/reply bodies don't duplicate publisher payloads.
|
|
1376
|
+
meta: index === 0 && meta && typeof meta === "object" ? meta : {},
|
|
1377
|
+
}));
|
|
1378
|
+
dispatch({ type: "log/appendMany", lines: payload });
|
|
1319
1379
|
appendScopedHistory(type, stripBlessedTags(text), meta);
|
|
1320
1380
|
}, [appendScopedHistory, setStatusText]);
|
|
1321
1381
|
|
|
@@ -3505,6 +3565,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3505
3565
|
return buildChatLogGroups(lines.map((line, idx) => ({
|
|
3506
3566
|
id: `s-${idx}`,
|
|
3507
3567
|
text: idx === 0 ? `${prefix}${line}` : ` ${line}`,
|
|
3568
|
+
sourceType: "bus",
|
|
3569
|
+
type: "bus",
|
|
3508
3570
|
})));
|
|
3509
3571
|
}, [state.activeStream]);
|
|
3510
3572
|
|
|
@@ -3512,6 +3574,18 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3512
3574
|
return null;
|
|
3513
3575
|
}
|
|
3514
3576
|
|
|
3577
|
+
const renderUserLogBody = (bodyText = "") => {
|
|
3578
|
+
const body = String(bodyText || "");
|
|
3579
|
+
const atMatch = body.match(/^@([^\s]+)\s+(.*)$/);
|
|
3580
|
+
if (atMatch) {
|
|
3581
|
+
return {
|
|
3582
|
+
at: atMatch[1],
|
|
3583
|
+
rest: atMatch[2] || "",
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
return { at: "", rest: body };
|
|
3587
|
+
};
|
|
3588
|
+
|
|
3515
3589
|
const renderChatLogEntry = (entry, group) => {
|
|
3516
3590
|
const row = entry && entry.row ? entry.row : buildChatLogLineModel("");
|
|
3517
3591
|
const key = entry && entry.id ? entry.id : `log-${row.body}`;
|
|
@@ -3529,12 +3603,31 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3529
3603
|
h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
|
|
3530
3604
|
);
|
|
3531
3605
|
}
|
|
3606
|
+
if (row.kind === "user") {
|
|
3607
|
+
const userBody = renderUserLogBody(row.bodyText);
|
|
3608
|
+
return h(Box, { key, width: "100%", marginBottom: 1 },
|
|
3609
|
+
h(Text, { color: "green", bold: true }, row.markerText || "› "),
|
|
3610
|
+
userBody.at
|
|
3611
|
+
? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
|
|
3612
|
+
: null,
|
|
3613
|
+
h(Text, { color: "green", bold: true, wrap: "wrap" }, userBody.rest),
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3532
3616
|
const markerText = entry && entry.continuation
|
|
3533
|
-
? (group && (group.kind === "assistant" || group.kind === "agent") ? " " : " ")
|
|
3617
|
+
? (group && (group.kind === "assistant" || group.kind === "agent" || group.kind === "report") ? " " : " ")
|
|
3534
3618
|
: row.markerText;
|
|
3619
|
+
const bodyProps = {
|
|
3620
|
+
color: colors.body,
|
|
3621
|
+
wrap: "wrap",
|
|
3622
|
+
};
|
|
3623
|
+
if (colors.dim) bodyProps.dimColor = true;
|
|
3535
3624
|
return h(Box, { key, width: "100%" },
|
|
3536
|
-
h(Text, {
|
|
3537
|
-
|
|
3625
|
+
h(Text, {
|
|
3626
|
+
color: colors.marker,
|
|
3627
|
+
bold: row.kind === "error" || row.kind === "assistant",
|
|
3628
|
+
dimColor: Boolean(colors.dim),
|
|
3629
|
+
}, markerText),
|
|
3630
|
+
h(Text, bodyProps,
|
|
3538
3631
|
row.speaker && !(entry && entry.continuation)
|
|
3539
3632
|
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3540
3633
|
: null,
|
|
@@ -3551,7 +3644,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3551
3644
|
if (entries.length === 0) return null;
|
|
3552
3645
|
const first = entries[0] || {};
|
|
3553
3646
|
const row = first.row || buildChatLogLineModel("");
|
|
3554
|
-
if (row.kind === "spacer" || row.kind === "banner" || row.kind === "divider") {
|
|
3647
|
+
if (row.kind === "spacer" || row.kind === "banner" || row.kind === "divider" || row.kind === "user") {
|
|
3555
3648
|
return renderChatLogEntry(first, group);
|
|
3556
3649
|
}
|
|
3557
3650
|
return h(Box, {
|
|
@@ -3587,12 +3680,31 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3587
3680
|
h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
|
|
3588
3681
|
);
|
|
3589
3682
|
}
|
|
3683
|
+
if (row.kind === "user") {
|
|
3684
|
+
const userBody = renderUserLogBody(row.bodyText);
|
|
3685
|
+
return h(Box, { key, width: "100%", marginTop, marginBottom: 1 },
|
|
3686
|
+
h(Text, { color: "green", bold: true }, row.markerText || "› "),
|
|
3687
|
+
userBody.at
|
|
3688
|
+
? h(Text, { color: "magenta", bold: true }, `@${userBody.at} `)
|
|
3689
|
+
: null,
|
|
3690
|
+
h(Text, { color: "green", bold: true, wrap: "wrap" }, userBody.rest),
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3590
3693
|
const markerText = continuation
|
|
3591
|
-
? (groupKind === "assistant" || groupKind === "agent" ? " " : " ")
|
|
3694
|
+
? (groupKind === "assistant" || groupKind === "agent" || groupKind === "report" ? " " : " ")
|
|
3592
3695
|
: row.markerText;
|
|
3696
|
+
const bodyProps = {
|
|
3697
|
+
color: colors.body,
|
|
3698
|
+
wrap: "wrap",
|
|
3699
|
+
};
|
|
3700
|
+
if (colors.dim) bodyProps.dimColor = true;
|
|
3593
3701
|
return h(Box, { key, width: "100%", marginTop },
|
|
3594
|
-
h(Text, {
|
|
3595
|
-
|
|
3702
|
+
h(Text, {
|
|
3703
|
+
color: colors.marker,
|
|
3704
|
+
bold: row.kind === "error" || row.kind === "assistant",
|
|
3705
|
+
dimColor: Boolean(colors.dim),
|
|
3706
|
+
}, markerText),
|
|
3707
|
+
h(Text, bodyProps,
|
|
3596
3708
|
row.speaker && !continuation
|
|
3597
3709
|
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3598
3710
|
: null,
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* component (e.g. completion popup) can
|
|
26
26
|
* handle them. Plain editing keys still work.
|
|
27
27
|
* placeholder (string) rendered in gray when value is empty
|
|
28
|
+
* onPasteText(filtered) optional. Called for non-empty paste/insert
|
|
29
|
+
* chunks before insertText. May return:
|
|
30
|
+
* string — insert that text instead
|
|
31
|
+
* { text } — insert text (may be "")
|
|
32
|
+
* null/undefined — fall back to filtered text
|
|
28
33
|
*
|
|
29
34
|
* Newlines: Enter submits. Use Alt+Enter (delivered as meta+Return) or end the
|
|
30
35
|
* line with `\` (the legacy continuation trick) to insert a literal newline.
|
|
@@ -32,7 +37,8 @@
|
|
|
32
37
|
* plain Enter, so it would silently submit.
|
|
33
38
|
*
|
|
34
39
|
* Bracketed paste arrives as a multi-byte `input` chunk in useInput; we route
|
|
35
|
-
* it through insertText, so multi-line paste already works
|
|
40
|
+
* it through insertText (or onPasteText), so multi-line paste already works
|
|
41
|
+
* without extra code.
|
|
36
42
|
*/
|
|
37
43
|
|
|
38
44
|
const fmt = require("../format");
|
|
@@ -167,6 +173,7 @@ function createMultilineInput({ React, ink }) {
|
|
|
167
173
|
// the IME composition window pops up at the visible (inverse) cursor
|
|
168
174
|
// instead of at the bottom of the screen.
|
|
169
175
|
linesBelowInput = 0,
|
|
176
|
+
onPasteText = null,
|
|
170
177
|
}) {
|
|
171
178
|
// Cursor is owned by this component. preferredCol tracks the visual
|
|
172
179
|
// column we want to keep when bouncing across lines of different widths
|
|
@@ -392,7 +399,36 @@ function createMultilineInput({ React, ink }) {
|
|
|
392
399
|
// Plain character / paste. Filter control bytes.
|
|
393
400
|
if (input && !key.ctrl && !key.meta) {
|
|
394
401
|
const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
|
|
395
|
-
|
|
402
|
+
// Binary clipboard paste can strip to empty — still notify parent so
|
|
403
|
+
// it can try macOS PNGf clipboard ingest.
|
|
404
|
+
if (!filtered) {
|
|
405
|
+
if (typeof onPasteText === "function") {
|
|
406
|
+
try { onPasteText(""); } catch { /* ignore */ }
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (typeof onPasteText === "function" && (filtered.length > 1 || filtered.includes("\n"))) {
|
|
411
|
+
let rewritten;
|
|
412
|
+
try {
|
|
413
|
+
rewritten = onPasteText(filtered);
|
|
414
|
+
} catch {
|
|
415
|
+
rewritten = filtered;
|
|
416
|
+
}
|
|
417
|
+
if (rewritten == null) {
|
|
418
|
+
insertText(filtered);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (typeof rewritten === "string") {
|
|
422
|
+
if (rewritten) insertText(rewritten);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (rewritten && typeof rewritten === "object") {
|
|
426
|
+
const nextText = rewritten.text == null ? filtered : String(rewritten.text);
|
|
427
|
+
if (nextText) insertText(nextText);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
insertText(filtered);
|
|
396
432
|
}
|
|
397
433
|
}, { isActive: interactive });
|
|
398
434
|
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
const { runInk } = require("../runInk");
|
|
18
18
|
const fmt = require("../format");
|
|
19
19
|
const { createMultilineInput } = require("./MultilineInput");
|
|
20
|
+
const {
|
|
21
|
+
handleImagePaste,
|
|
22
|
+
formatUserLogWithAttachments,
|
|
23
|
+
buildAttachedImagesPromptPrefix,
|
|
24
|
+
} = require("../../code/imageIngest");
|
|
20
25
|
|
|
21
26
|
// Throttle for the live thinking-chain status line: rapid thinking_delta
|
|
22
27
|
// chunks would otherwise re-render the footer on every SSE event.
|
|
@@ -70,6 +75,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
70
75
|
);
|
|
71
76
|
const [draft, setDraft] = useState("");
|
|
72
77
|
const [draftVersion, setDraftVersion] = useState(0);
|
|
78
|
+
const [imageAttachments, setImageAttachments] = useState([]);
|
|
73
79
|
// status: idle when message === "". `type` picks a STATUS_INDICATORS
|
|
74
80
|
// bucket; `showTimer` and `startedAt` reproduce the blessed spinner
|
|
75
81
|
// controls. The BG suffix is computed from backgroundTasksRef and
|
|
@@ -323,14 +329,36 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
323
329
|
|
|
324
330
|
const { UCODE_COMMAND_REGISTRY, UCODE_COMMAND_TREE } = require("../../code/commands");
|
|
325
331
|
const { listSessionSummaries } = require("../../code/sessionStore");
|
|
326
|
-
const { suggestUcodeModels, applyUcodeModelCommand } = require("../../code/modelCommand");
|
|
332
|
+
const { suggestUcodeModels, suggestUcodeThinkingLevels, applyUcodeModelCommand, listUcodeModels } = require("../../code/modelCommand");
|
|
327
333
|
let resumeSessions = [];
|
|
328
334
|
try {
|
|
329
335
|
resumeSessions = listSessionSummaries(props.workspaceRoot || process.cwd(), { limit: 40 });
|
|
330
336
|
} catch {
|
|
331
337
|
resumeSessions = [];
|
|
332
338
|
}
|
|
333
|
-
const
|
|
339
|
+
const [remoteModels, setRemoteModels] = useState([]);
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
let cancelled = false;
|
|
342
|
+
(async () => {
|
|
343
|
+
try {
|
|
344
|
+
const listed = await listUcodeModels(props.state || {}, {
|
|
345
|
+
workspaceRoot: props.workspaceRoot || process.cwd(),
|
|
346
|
+
});
|
|
347
|
+
if (!cancelled && listed.ok) {
|
|
348
|
+
setRemoteModels(Array.isArray(listed.models) ? listed.models : []);
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
if (!cancelled) setRemoteModels([]);
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
return () => { cancelled = true; };
|
|
355
|
+
}, [
|
|
356
|
+
props.workspaceRoot,
|
|
357
|
+
props.state && props.state.provider,
|
|
358
|
+
props.state && props.state.model,
|
|
359
|
+
]);
|
|
360
|
+
const modelSuggestions = suggestUcodeModels(props.state || {}, { models: remoteModels });
|
|
361
|
+
const thinkingSuggestions = suggestUcodeThinkingLevels(props.state || {});
|
|
334
362
|
|
|
335
363
|
const completions = fmt.buildCompletions({
|
|
336
364
|
text: draft,
|
|
@@ -341,6 +369,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
341
369
|
argumentLists: {
|
|
342
370
|
"/resume": resumeSessions,
|
|
343
371
|
"/model": modelSuggestions,
|
|
372
|
+
"/model/thinking": thinkingSuggestions,
|
|
344
373
|
},
|
|
345
374
|
limit: 20,
|
|
346
375
|
});
|
|
@@ -487,12 +516,20 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
487
516
|
|
|
488
517
|
const runChainRef = useRef(Promise.resolve());
|
|
489
518
|
|
|
490
|
-
const executeLine = useCallback(async (rawValue) => {
|
|
491
|
-
const
|
|
492
|
-
|
|
519
|
+
const executeLine = useCallback(async (rawValue, options = {}) => {
|
|
520
|
+
const modelSource = options.modelText != null ? options.modelText : rawValue;
|
|
521
|
+
const logSource = options.logText != null ? options.logText : modelSource;
|
|
522
|
+
const preserveNewlines = Boolean(options.preserveNewlines);
|
|
523
|
+
const modelNormalized = preserveNewlines
|
|
524
|
+
? String(modelSource || "").trim()
|
|
525
|
+
: String(modelSource || "").replace(/\r?\n/g, " ").trim();
|
|
526
|
+
const logNormalized = fmt.redactUserMessageForLog(
|
|
527
|
+
String(logSource || "").replace(/\r?\n/g, " ").trim(),
|
|
528
|
+
);
|
|
529
|
+
if (!modelNormalized && !logNormalized) return;
|
|
493
530
|
toolMergeScopeRef.current += 1;
|
|
494
531
|
flushActiveMerge();
|
|
495
|
-
appendLogLine(`› ${
|
|
532
|
+
appendLogLine(`› ${logNormalized || modelNormalized}`, "user");
|
|
496
533
|
|
|
497
534
|
const runtimeWorkspace = String(
|
|
498
535
|
(props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd()
|
|
@@ -500,7 +537,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
500
537
|
|
|
501
538
|
let result;
|
|
502
539
|
try {
|
|
503
|
-
result = props.runSingleCommand(
|
|
540
|
+
result = props.runSingleCommand(modelNormalized, runtimeWorkspace);
|
|
504
541
|
} catch (err) {
|
|
505
542
|
appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`, "error");
|
|
506
543
|
return;
|
|
@@ -541,7 +578,9 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
541
578
|
return;
|
|
542
579
|
}
|
|
543
580
|
case "model": {
|
|
544
|
-
const applied = applyUcodeModelCommand(props.state || {}, result
|
|
581
|
+
const applied = await applyUcodeModelCommand(props.state || {}, result, {
|
|
582
|
+
workspaceRoot: runtimeWorkspace,
|
|
583
|
+
});
|
|
545
584
|
appendLogText(applied.output || "", applied.ok ? "system" : "error");
|
|
546
585
|
if (applied.ok && result.action === "set" && typeof props.persistSessionState === "function") {
|
|
547
586
|
try {
|
|
@@ -938,21 +977,27 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
938
977
|
|
|
939
978
|
const submit = useCallback((submitted) => {
|
|
940
979
|
const value = String(submitted == null ? draft : submitted);
|
|
980
|
+
const attachments = Array.isArray(imageAttachments) ? imageAttachments.slice() : [];
|
|
941
981
|
const trimmed = value.trim();
|
|
942
|
-
if (!trimmed) return;
|
|
982
|
+
if (!trimmed && attachments.length === 0) return;
|
|
943
983
|
setDraft("");
|
|
944
984
|
setDraftVersion((v) => v + 1);
|
|
985
|
+
setImageAttachments([]);
|
|
945
986
|
setInputHistory((prev) => {
|
|
946
|
-
const
|
|
987
|
+
const historyValue = formatUserLogWithAttachments(trimmed, attachments) || trimmed;
|
|
988
|
+
const next = prev.concat([historyValue]).slice(-200);
|
|
947
989
|
setHistoryIndex(next.length);
|
|
948
990
|
return next;
|
|
949
991
|
});
|
|
950
992
|
|
|
993
|
+
const modelText = `${buildAttachedImagesPromptPrefix(attachments)}${trimmed}`.trim();
|
|
994
|
+
const logText = formatUserLogWithAttachments(trimmed, attachments);
|
|
995
|
+
|
|
951
996
|
// Pending approval/choice/chat takes priority over nudge / new NL.
|
|
952
997
|
try {
|
|
953
998
|
const { hasPendingUserInteraction } = require("../../code/context/userInteraction");
|
|
954
999
|
if (props.state && props.state.executionState && hasPendingUserInteraction(props.state.executionState)) {
|
|
955
|
-
appendLogText(`› ${
|
|
1000
|
+
appendLogText(`› ${logText}`, "user");
|
|
956
1001
|
const startedAt = Date.now();
|
|
957
1002
|
setStatus({
|
|
958
1003
|
message: "Applying your reply...",
|
|
@@ -1028,10 +1073,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1028
1073
|
if (!props.state.executionState || typeof props.state.executionState !== "object") {
|
|
1029
1074
|
props.state.executionState = emptyExecutionState();
|
|
1030
1075
|
}
|
|
1031
|
-
const queued = enqueueUserPrompt(props.state.executionState,
|
|
1076
|
+
const queued = enqueueUserPrompt(props.state.executionState, modelText);
|
|
1077
|
+
const reminderPreview = logText.slice(0, 120) + (logText.length > 120 ? "…" : "");
|
|
1032
1078
|
appendLogText(
|
|
1033
1079
|
queued.enqueued
|
|
1034
|
-
? `Queued user reminder for next model turn: ${
|
|
1080
|
+
? `Queued user reminder for next model turn: ${reminderPreview}`
|
|
1035
1081
|
: "Could not queue user reminder (empty).",
|
|
1036
1082
|
"system",
|
|
1037
1083
|
);
|
|
@@ -1040,10 +1086,15 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1040
1086
|
|
|
1041
1087
|
// Serialize executions so streaming tasks don't interleave.
|
|
1042
1088
|
runChainRef.current = runChainRef.current
|
|
1043
|
-
.then(() => executeLine(
|
|
1089
|
+
.then(() => executeLine(modelText, {
|
|
1090
|
+
modelText,
|
|
1091
|
+
logText,
|
|
1092
|
+
preserveNewlines: attachments.length > 0,
|
|
1093
|
+
}))
|
|
1044
1094
|
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
|
|
1045
1095
|
}, [
|
|
1046
1096
|
draft,
|
|
1097
|
+
imageAttachments,
|
|
1047
1098
|
executeLine,
|
|
1048
1099
|
appendLogText,
|
|
1049
1100
|
appendLogLine,
|
|
@@ -1257,6 +1308,16 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1257
1308
|
}),
|
|
1258
1309
|
);
|
|
1259
1310
|
})() : null,
|
|
1311
|
+
imageAttachments.length > 0
|
|
1312
|
+
? h(Box, { flexDirection: "column", width: "100%", marginBottom: 0 },
|
|
1313
|
+
h(Text, { color: "cyan", dimColor: true },
|
|
1314
|
+
imageAttachments.map((item) => {
|
|
1315
|
+
const name = item.fileName || require("path").basename(String(item.relPath || "image"));
|
|
1316
|
+
return `[img] ${name}`;
|
|
1317
|
+
}).join(" "),
|
|
1318
|
+
),
|
|
1319
|
+
)
|
|
1320
|
+
: null,
|
|
1260
1321
|
h(Box, { width: "100%" },
|
|
1261
1322
|
h(MultilineInput, {
|
|
1262
1323
|
value: draft,
|
|
@@ -1267,6 +1328,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
1267
1328
|
}
|
|
1268
1329
|
setDraft(next);
|
|
1269
1330
|
},
|
|
1331
|
+
onPasteText: (filtered) => {
|
|
1332
|
+
const workspaceRoot = String(
|
|
1333
|
+
(props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd(),
|
|
1334
|
+
);
|
|
1335
|
+
const sessionId = String((props.state && props.state.sessionId) || "session");
|
|
1336
|
+
const outcome = handleImagePaste(filtered, {
|
|
1337
|
+
workspaceRoot,
|
|
1338
|
+
sessionId,
|
|
1339
|
+
tryClipboard: true,
|
|
1340
|
+
});
|
|
1341
|
+
if (Array.isArray(outcome.attachments) && outcome.attachments.length > 0) {
|
|
1342
|
+
setImageAttachments((prev) => {
|
|
1343
|
+
const next = prev.slice();
|
|
1344
|
+
for (const item of outcome.attachments) {
|
|
1345
|
+
if (!item || !item.relPath) continue;
|
|
1346
|
+
if (next.some((existing) => existing.relPath === item.relPath)) continue;
|
|
1347
|
+
next.push(item);
|
|
1348
|
+
}
|
|
1349
|
+
return next;
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
if (Array.isArray(outcome.errors) && outcome.errors.length > 0 && outcome.attachments.length === 0) {
|
|
1353
|
+
// Soft notice only when nothing was ingested.
|
|
1354
|
+
appendLogText(`Image paste: ${outcome.errors[0]}`, "system");
|
|
1355
|
+
}
|
|
1356
|
+
return { text: outcome.text == null ? filtered : outcome.text };
|
|
1357
|
+
},
|
|
1270
1358
|
onSubmit: (value) => {
|
|
1271
1359
|
setCompletionSuppressedDraft(null);
|
|
1272
1360
|
submit(value);
|