u-foo 3.0.2 → 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.
@@ -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
@@ -510,12 +516,20 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
510
516
 
511
517
  const runChainRef = useRef(Promise.resolve());
512
518
 
513
- const executeLine = useCallback(async (rawValue) => {
514
- const normalized = String(rawValue || "").replace(/\r?\n/g, " ").trim();
515
- if (!normalized) return;
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;
516
530
  toolMergeScopeRef.current += 1;
517
531
  flushActiveMerge();
518
- appendLogLine(`› ${normalized}`, "user");
532
+ appendLogLine(`› ${logNormalized || modelNormalized}`, "user");
519
533
 
520
534
  const runtimeWorkspace = String(
521
535
  (props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd()
@@ -523,7 +537,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
523
537
 
524
538
  let result;
525
539
  try {
526
- result = props.runSingleCommand(normalized, runtimeWorkspace);
540
+ result = props.runSingleCommand(modelNormalized, runtimeWorkspace);
527
541
  } catch (err) {
528
542
  appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`, "error");
529
543
  return;
@@ -963,21 +977,27 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
963
977
 
964
978
  const submit = useCallback((submitted) => {
965
979
  const value = String(submitted == null ? draft : submitted);
980
+ const attachments = Array.isArray(imageAttachments) ? imageAttachments.slice() : [];
966
981
  const trimmed = value.trim();
967
- if (!trimmed) return;
982
+ if (!trimmed && attachments.length === 0) return;
968
983
  setDraft("");
969
984
  setDraftVersion((v) => v + 1);
985
+ setImageAttachments([]);
970
986
  setInputHistory((prev) => {
971
- const next = prev.concat([trimmed]).slice(-200);
987
+ const historyValue = formatUserLogWithAttachments(trimmed, attachments) || trimmed;
988
+ const next = prev.concat([historyValue]).slice(-200);
972
989
  setHistoryIndex(next.length);
973
990
  return next;
974
991
  });
975
992
 
993
+ const modelText = `${buildAttachedImagesPromptPrefix(attachments)}${trimmed}`.trim();
994
+ const logText = formatUserLogWithAttachments(trimmed, attachments);
995
+
976
996
  // Pending approval/choice/chat takes priority over nudge / new NL.
977
997
  try {
978
998
  const { hasPendingUserInteraction } = require("../../code/context/userInteraction");
979
999
  if (props.state && props.state.executionState && hasPendingUserInteraction(props.state.executionState)) {
980
- appendLogText(`› ${trimmed}`, "user");
1000
+ appendLogText(`› ${logText}`, "user");
981
1001
  const startedAt = Date.now();
982
1002
  setStatus({
983
1003
  message: "Applying your reply...",
@@ -1053,10 +1073,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1053
1073
  if (!props.state.executionState || typeof props.state.executionState !== "object") {
1054
1074
  props.state.executionState = emptyExecutionState();
1055
1075
  }
1056
- const queued = enqueueUserPrompt(props.state.executionState, trimmed);
1076
+ const queued = enqueueUserPrompt(props.state.executionState, modelText);
1077
+ const reminderPreview = logText.slice(0, 120) + (logText.length > 120 ? "…" : "");
1057
1078
  appendLogText(
1058
1079
  queued.enqueued
1059
- ? `Queued user reminder for next model turn: ${trimmed.slice(0, 120)}${trimmed.length > 120 ? "…" : ""}`
1080
+ ? `Queued user reminder for next model turn: ${reminderPreview}`
1060
1081
  : "Could not queue user reminder (empty).",
1061
1082
  "system",
1062
1083
  );
@@ -1065,10 +1086,15 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1065
1086
 
1066
1087
  // Serialize executions so streaming tasks don't interleave.
1067
1088
  runChainRef.current = runChainRef.current
1068
- .then(() => executeLine(value))
1089
+ .then(() => executeLine(modelText, {
1090
+ modelText,
1091
+ logText,
1092
+ preserveNewlines: attachments.length > 0,
1093
+ }))
1069
1094
  .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
1070
1095
  }, [
1071
1096
  draft,
1097
+ imageAttachments,
1072
1098
  executeLine,
1073
1099
  appendLogText,
1074
1100
  appendLogLine,
@@ -1282,6 +1308,16 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1282
1308
  }),
1283
1309
  );
1284
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,
1285
1321
  h(Box, { width: "100%" },
1286
1322
  h(MultilineInput, {
1287
1323
  value: draft,
@@ -1292,6 +1328,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1292
1328
  }
1293
1329
  setDraft(next);
1294
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
+ },
1295
1358
  onSubmit: (value) => {
1296
1359
  setCompletionSuppressedDraft(null);
1297
1360
  submit(value);