u-foo 2.5.15 → 3.0.1

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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +350 -246
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +102 -119
  6. package/src/code/context/index.js +11 -1
  7. package/src/code/context/planGraph.js +1410 -0
  8. package/src/code/context/planGraphService.js +857 -0
  9. package/src/code/context/planMode.js +405 -0
  10. package/src/code/context/planProjection.js +432 -0
  11. package/src/code/context/promptLayers.js +21 -5
  12. package/src/code/context/stateCommit.js +2 -0
  13. package/src/code/context/toolRuntime.js +172 -0
  14. package/src/code/context/userInteraction.js +457 -0
  15. package/src/code/context/userNudge.js +116 -0
  16. package/src/code/dispatch.js +17 -1
  17. package/src/code/index.js +4 -0
  18. package/src/code/nativeRunner.js +589 -172
  19. package/src/code/protocol/controlPlane.js +93 -0
  20. package/src/code/protocol/faultHarness.js +90 -0
  21. package/src/code/protocol/index.js +20 -0
  22. package/src/code/protocol/loopEvents.js +102 -0
  23. package/src/code/protocol/materialize.js +107 -0
  24. package/src/code/protocol/messageFixtures.js +116 -0
  25. package/src/code/protocol/ownership.js +147 -0
  26. package/src/code/protocol/protocolValidator.js +165 -0
  27. package/src/code/protocol/suspension.js +173 -0
  28. package/src/code/protocol/toolCallLedger.js +222 -0
  29. package/src/code/protocol/transitions.js +97 -0
  30. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  31. package/src/code/providers/index.js +7 -0
  32. package/src/code/providers/openaiChatTransport.js +98 -0
  33. package/src/code/providers/transportContract.js +46 -0
  34. package/src/code/repl.js +147 -18
  35. package/src/code/runtime/agentWakeup.js +58 -0
  36. package/src/code/runtime/graphOwner.js +41 -0
  37. package/src/code/runtime/graphYieldRouter.js +42 -0
  38. package/src/code/runtime/index.js +15 -0
  39. package/src/code/runtime/loopMailbox.js +124 -0
  40. package/src/code/runtime/runtimeEvents.js +39 -0
  41. package/src/code/runtime/taskControl.js +565 -0
  42. package/src/code/runtime/taskFocus.js +165 -0
  43. package/src/code/runtime/taskLoop.js +394 -0
  44. package/src/code/runtime/taskRun.js +348 -0
  45. package/src/code/runtime/toolProvenance.js +70 -0
  46. package/src/code/runtime/workspaceLease.js +249 -0
  47. package/src/code/sessionStore.js +1 -10
  48. package/src/code/skills/injection.js +1 -0
  49. package/src/code/taskDecomposer.js +32 -8
  50. package/src/code/taskRoute.js +73 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/ui/format/index.js +25 -1
  54. package/src/ui/format/markdownRenderer.js +224 -2
  55. package/src/ui/ink/UcodeApp.js +268 -22
  56. package/src/code/context/featureFlag.js +0 -13
@@ -57,6 +57,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
57
57
  engine: (props.state && props.state.engine) || "ufoo-core",
58
58
  workspaceRoot: props.workspaceRoot,
59
59
  sessionId: (props.state && props.state.sessionId) || "",
60
+ planMode: Boolean(
61
+ props.state
62
+ && props.state.executionState
63
+ && props.state.executionState.planMode
64
+ ),
60
65
  });
61
66
 
62
67
  return function UcodeApp() {
@@ -75,6 +80,15 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
75
80
  showTimer: false,
76
81
  startedAt: 0,
77
82
  });
83
+ const [planUi, setPlanUi] = useState(() => ({
84
+ hasPlan: false,
85
+ visible: false,
86
+ bandLines: [],
87
+ idleHint: "",
88
+ statusLine: "",
89
+ hash: "",
90
+ }));
91
+ const [interactionLines, setInteractionLines] = useState([]);
78
92
  const [spinnerTick, setSpinnerTick] = useState(0);
79
93
  const [size, setSize] = useState({ cols: 0, rows: 0 });
80
94
  const [agents, setAgents] = useState([]);
@@ -121,6 +135,9 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
121
135
  // Persist fence/open-code state across streamed assistant log lines so
122
136
  // ``` blocks stay styled even when deltas arrive one line at a time.
123
137
  const markdownStateRef = useRef({ inCodeBlock: false });
138
+ // GFM tables need the full block for column alignment — buffer consecutive
139
+ // pipe rows and flush as one multi-line markdown unit.
140
+ const tableBufRef = useRef(fmt.createMarkdownTableBuffer());
124
141
 
125
142
  const targetAgent = agentSelectionMode && selectedAgentIndex >= 0
126
143
  ? agents[selectedAgentIndex]
@@ -128,6 +145,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
128
145
 
129
146
  const bumpBackground = useCallback(() => setBackgroundVersion((v) => v + 1), []);
130
147
 
148
+ const refreshPlanUi = useCallback((activityMessage = "") => {
149
+ try {
150
+ const { buildPlanUiProjection } = require("../../code/context/planProjection");
151
+ const {
152
+ getPendingUserInteraction,
153
+ formatInteractionPromptLines,
154
+ syncInteractionFromPlanGraph,
155
+ } = require("../../code/context/userInteraction");
156
+ if (props.state && props.state.executionState) {
157
+ syncInteractionFromPlanGraph(props.state.executionState);
158
+ }
159
+ const next = buildPlanUiProjection(
160
+ props.state && props.state.executionState,
161
+ {
162
+ cols: size.cols || 80,
163
+ activityMessage: String(activityMessage || ""),
164
+ }
165
+ );
166
+ setPlanUi((prev) => (prev && prev.hash === next.hash ? prev : next));
167
+ const pending = getPendingUserInteraction(props.state && props.state.executionState);
168
+ setInteractionLines(pending ? formatInteractionPromptLines(pending) : []);
169
+ return next;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }, [props.state, size.cols]);
174
+
131
175
  const getBackgroundSuffix = useCallback(() => {
132
176
  const tasks = backgroundTasksRef.current;
133
177
  if (!tasks || tasks.size === 0) return "";
@@ -324,17 +368,17 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
324
368
  return true;
325
369
  }, [completionsOpen, completions, completionIndex]);
326
370
 
327
- const appendLogLine = useCallback((text, kind = "assistant") => {
328
- const raw = String(text == null ? "" : text);
371
+ const pushRenderedLogLines = useCallback((rawText, kind = "assistant") => {
372
+ const raw = String(rawText == null ? "" : rawText);
329
373
  let renderedLines = [raw];
330
374
  if (MARKDOWN_LOG_KINDS.has(kind)) {
331
375
  try {
332
376
  renderedLines = fmt.renderLogLinesWithMarkdownAnsi(raw, markdownStateRef.current);
333
377
  if (!Array.isArray(renderedLines) || renderedLines.length === 0) {
334
- renderedLines = [raw];
378
+ renderedLines = raw.split(/\r?\n/);
335
379
  }
336
380
  } catch {
337
- renderedLines = [raw];
381
+ renderedLines = raw.split(/\r?\n/);
338
382
  }
339
383
  }
340
384
  setLogLines((prev) => {
@@ -348,6 +392,23 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
348
392
  });
349
393
  }, []);
350
394
 
395
+ const flushTableBuffer = useCallback(() => {
396
+ const buffered = tableBufRef.current.flush();
397
+ if (buffered == null) return;
398
+ pushRenderedLogLines(buffered, "assistant");
399
+ }, [pushRenderedLogLines]);
400
+
401
+ const appendLogLine = useCallback((text, kind = "assistant") => {
402
+ const raw = String(text == null ? "" : text);
403
+ if (MARKDOWN_LOG_KINDS.has(kind)) {
404
+ if (tableBufRef.current.push(raw)) return;
405
+ flushTableBuffer();
406
+ } else {
407
+ flushTableBuffer();
408
+ }
409
+ pushRenderedLogLines(raw, kind);
410
+ }, [flushTableBuffer, pushRenderedLogLines]);
411
+
351
412
  const renderMergeText = useCallback((merge) => {
352
413
  if (!merge || !Array.isArray(merge.entries)) return "";
353
414
  return fmt.buildToolMergeRowText(merge.entries);
@@ -391,12 +452,14 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
391
452
  // Multi-line text → split into separate log entries so <Static> keys
392
453
  // stay stable when streaming arrives line-by-line. Always promote any
393
454
  // in-flight tool group first so it freezes above the new text.
455
+ // Table rows are re-batched inside appendLogLine before markdown render.
394
456
  const raw = String(text == null ? "" : text);
395
457
  if (!raw) return;
396
458
  flushActiveMerge();
397
459
  const lines = raw.split(/\r?\n/);
398
460
  for (const line of lines) appendLogLine(line, kind);
399
- }, [appendLogLine, flushActiveMerge]);
461
+ if (MARKDOWN_LOG_KINDS.has(kind)) flushTableBuffer();
462
+ }, [appendLogLine, flushActiveMerge, flushTableBuffer]);
400
463
 
401
464
  const expandLastMerge = useCallback(() => {
402
465
  // Try the active group first; fall back to the most recent frozen one.
@@ -464,6 +527,14 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
464
527
  sessionId: (props.state && props.state.sessionId) || "",
465
528
  });
466
529
  appendLogText(formatSessionUsageStatus(usageSummary), "system");
530
+ if (props.state && props.state.executionState) {
531
+ const { formatPlanModeStatus } = require("../../code/context/planMode");
532
+ const planLines = formatPlanModeStatus(props.state.executionState)
533
+ .split("\n")
534
+ .slice(0, 6)
535
+ .join("\n");
536
+ appendLogText(planLines, "system");
537
+ }
467
538
  } catch (err) {
468
539
  appendLogText(`Error: ${err && err.message ? err.message : "status failed"}`, "error");
469
540
  }
@@ -487,6 +558,22 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
487
558
  }
488
559
  return;
489
560
  }
561
+ case "plan": {
562
+ const { applyUcodePlanCommand } = require("../../code/context/planMode");
563
+ const applied = applyUcodePlanCommand(props.state || {}, result);
564
+ appendLogText(applied.output || "", applied.ok ? "system" : "error");
565
+ if (applied.refreshPlanUi || applied.ok) {
566
+ refreshPlanUi();
567
+ }
568
+ if (applied.ok && typeof props.persistSessionState === "function") {
569
+ try {
570
+ props.persistSessionState(props.state);
571
+ } catch {
572
+ // best-effort
573
+ }
574
+ }
575
+ return;
576
+ }
490
577
  case "ubus": {
491
578
  setStatus({ message: "Checking bus messages...", type: "typing", showTimer: false, startedAt: Date.now() });
492
579
  try {
@@ -535,6 +622,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
535
622
  // Rebuild the visible log from the restored session transcript so
536
623
  // the user sees prior turns instead of only a status toast.
537
624
  markdownStateRef.current = { inCodeBlock: false };
625
+ tableBufRef.current = fmt.createMarkdownTableBuffer();
538
626
  const history = fmt.buildUcodeSessionLogEntries(
539
627
  Array.isArray(props.state && props.state.nlMessages) ? props.state.nlMessages : [],
540
628
  { markdownState: markdownStateRef.current, idPrefix: "h", startSeq: 0 },
@@ -621,12 +709,18 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
621
709
  const startedAt = Date.now();
622
710
  const abortController = new AbortController();
623
711
  pendingTaskRef.current = { abortController, startedAt };
624
- const setNlStatus = (msg) => setStatus({
625
- message: msg,
626
- type: "thinking",
627
- showTimer: true,
628
- startedAt,
629
- });
712
+ const setNlStatus = (msg) => {
713
+ const projection = refreshPlanUi(msg);
714
+ const message = projection && projection.hasPlan && projection.activityStatusLine
715
+ ? projection.activityStatusLine
716
+ : msg;
717
+ setStatus({
718
+ message,
719
+ type: "thinking",
720
+ showTimer: true,
721
+ startedAt,
722
+ });
723
+ };
630
724
  const cancelThinkingFlush = () => {
631
725
  if (thinkingTimerRef.current) {
632
726
  clearTimeout(thinkingTimerRef.current);
@@ -702,6 +796,9 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
702
796
  setNlStatus(`${label}...`);
703
797
  dropLeadingStreamBlank = true;
704
798
  }
799
+ if (entry.tool === "plan_graph" || entry.phase === "end" || entry.phase === "result") {
800
+ refreshPlanUi();
801
+ }
705
802
  logToolHint(entry, entry.result);
706
803
  },
707
804
  });
@@ -712,12 +809,14 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
712
809
  pendingTaskRef.current = null;
713
810
  cancelThinkingFlush();
714
811
  thinkingTailRef.current = "";
812
+ refreshPlanUi();
715
813
  setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
716
814
  }
717
815
  if (streamBuf) {
718
816
  if (/[^\s]/.test(streamBuf)) sawStreamText = true;
719
817
  appendLogLine(streamBuf);
720
818
  }
819
+ flushTableBuffer();
721
820
  // Skip the summary echo when the model already streamed its
722
821
  // response in full — otherwise the user sees the same text twice.
723
822
  // Mirrors the shouldSkipSummary check in tui.js.
@@ -745,7 +844,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
745
844
  default:
746
845
  if (result.output) appendLogText(result.output);
747
846
  }
748
- }, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge]);
847
+ }, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge, flushTableBuffer, refreshPlanUi]);
749
848
  // ^ `props` is captured by the createUcodeApp closure on a single mount,
750
849
  // so its reference is stable across renders even though it looks like a
751
850
  // changing dep to React's exhaustive-deps lint.
@@ -848,11 +947,112 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
848
947
  setHistoryIndex(next.length);
849
948
  return next;
850
949
  });
950
+
951
+ // Pending approval/choice/chat takes priority over nudge / new NL.
952
+ try {
953
+ const { hasPendingUserInteraction } = require("../../code/context/userInteraction");
954
+ if (props.state && props.state.executionState && hasPendingUserInteraction(props.state.executionState)) {
955
+ appendLogText(`› ${trimmed}`, "user");
956
+ const startedAt = Date.now();
957
+ setStatus({
958
+ message: "Applying your reply...",
959
+ type: "thinking",
960
+ showTimer: true,
961
+ startedAt,
962
+ });
963
+ runChainRef.current = runChainRef.current
964
+ .then(async () => {
965
+ const submit = typeof props.submitUserInteractionAnswer === "function"
966
+ ? props.submitUserInteractionAnswer
967
+ : require("../../code/protocol").submitUserInteractionAnswer;
968
+ let streamBuf = "";
969
+ let sawStreamText = false;
970
+ let streamStarted = false;
971
+ let dropLeadingStreamBlank = false;
972
+ const result = await submit(trimmed, props.state, {
973
+ onDelta: (delta) => {
974
+ const text = String(delta || "");
975
+ if (!text) return;
976
+ if (!streamStarted) {
977
+ flushActiveMerge();
978
+ streamStarted = true;
979
+ }
980
+ const split = fmt.splitStreamingLogChunk(streamBuf, text, {
981
+ dropLeadingBlank: dropLeadingStreamBlank,
982
+ });
983
+ if (split.sawVisible) {
984
+ sawStreamText = true;
985
+ dropLeadingStreamBlank = false;
986
+ }
987
+ for (const line of split.lines) {
988
+ appendLogLine(line);
989
+ }
990
+ streamBuf = split.buffer;
991
+ },
992
+ });
993
+ if (streamBuf) {
994
+ if (/[^\s]/.test(streamBuf)) sawStreamText = true;
995
+ appendLogLine(streamBuf);
996
+ }
997
+ flushTableBuffer();
998
+ refreshPlanUi();
999
+ if (!result || result.ok === false) {
1000
+ appendLogText(`Error: ${(result && result.error) || "resume failed"}`, "error");
1001
+ } else if (result.shouldEchoSummary) {
1002
+ appendLogText(result.echoSummaryText || result.summary || "", result.waitingUserInteraction ? "system" : "assistant");
1003
+ } else if (result.waitingUserInteraction) {
1004
+ appendLogText("Still waiting for your reply.", "system");
1005
+ }
1006
+ setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1007
+ })
1008
+ .catch((err) => {
1009
+ appendLogText(`Error: ${err && err.message ? err.message : err}`, "error");
1010
+ setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1011
+ });
1012
+ return;
1013
+ }
1014
+ } catch (err) {
1015
+ appendLogText(`Error: ${err && err.message ? err.message : "interaction failed"}`, "error");
1016
+ return;
1017
+ }
1018
+
1019
+ // While a native task is in flight, queue an additional user reminder
1020
+ // for the next LLM turn instead of starting a second NL task.
1021
+ if (pendingTaskRef.current) {
1022
+ const { enqueueUserPrompt } = require("../../code/context/userNudge");
1023
+ const { emptyExecutionState } = require("../../code/context/executionSegment");
1024
+ if (!props.state || typeof props.state !== "object") {
1025
+ appendLogText("Error: missing session state for user reminder", "error");
1026
+ return;
1027
+ }
1028
+ if (!props.state.executionState || typeof props.state.executionState !== "object") {
1029
+ props.state.executionState = emptyExecutionState();
1030
+ }
1031
+ const queued = enqueueUserPrompt(props.state.executionState, trimmed);
1032
+ appendLogText(
1033
+ queued.enqueued
1034
+ ? `Queued user reminder for next model turn: ${trimmed.slice(0, 120)}${trimmed.length > 120 ? "…" : ""}`
1035
+ : "Could not queue user reminder (empty).",
1036
+ "system",
1037
+ );
1038
+ return;
1039
+ }
1040
+
851
1041
  // Serialize executions so streaming tasks don't interleave.
852
1042
  runChainRef.current = runChainRef.current
853
1043
  .then(() => executeLine(value))
854
1044
  .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
855
- }, [draft, executeLine, appendLogText]);
1045
+ }, [
1046
+ draft,
1047
+ executeLine,
1048
+ appendLogText,
1049
+ appendLogLine,
1050
+ flushActiveMerge,
1051
+ flushTableBuffer,
1052
+ props.state,
1053
+ props.submitUserInteractionAnswer,
1054
+ refreshPlanUi,
1055
+ ]);
856
1056
 
857
1057
  useEffect(() => {
858
1058
  if (!stdout) return undefined;
@@ -865,6 +1065,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
865
1065
  return () => stdout.off("resize", update);
866
1066
  }, [stdout]);
867
1067
 
1068
+ useEffect(() => {
1069
+ refreshPlanUi();
1070
+ }, [refreshPlanUi]);
1071
+
868
1072
  // Drive the spinner + elapsed-timer redraws while a task is in flight.
869
1073
  useEffect(() => {
870
1074
  const statusType = inferStatusType(status.message, status.type);
@@ -878,7 +1082,13 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
878
1082
  return () => clearInterval(timer);
879
1083
  }, [status.message, status.type, status.showTimer]);
880
1084
 
881
- const statusText = useMemoStatusText(React, status, spinnerTick, getBackgroundSuffix());
1085
+ const statusText = useMemoStatusText(
1086
+ React,
1087
+ status,
1088
+ spinnerTick,
1089
+ getBackgroundSuffix(),
1090
+ !status.message ? (planUi.idleHint || "") : ""
1091
+ );
882
1092
 
883
1093
  // Top-level catches Ctrl+C / Ctrl+O, plus completion popup navigation
884
1094
  // while a slash/agent menu is open.
@@ -940,12 +1150,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
940
1150
  return h(Box, { flexDirection: "column", width: "100%" },
941
1151
  h(Box, { flexDirection: "column", width: "100%" },
942
1152
  ...(() => {
943
- // Re-render raw markdown at paint time so leftover ** / ### from
944
- // older append paths or nested `**code**` patterns still resolve.
1153
+ // Re-render raw markdown at paint time so leftover ** / ### / tables
1154
+ // from older append paths or nested `**code**` patterns still resolve.
945
1155
  const mdState = { inCodeBlock: false };
946
1156
  return logLines.map((item, idx) => {
947
1157
  let text = item.text || " ";
948
- if (MARKDOWN_LOG_KINDS.has(item.kind) && /(?:\*\*|__|^\s*#{1,6}\s|^\s*`{3})/m.test(text)) {
1158
+ if (MARKDOWN_LOG_KINDS.has(item.kind) && /(?:\*\*|__|^\s*#{1,6}\s|^\s*`{3}|^\s*\|)/m.test(text)) {
949
1159
  try {
950
1160
  const rendered = fmt.renderLogLinesWithMarkdownAnsi(text, mdState);
951
1161
  if (Array.isArray(rendered) && rendered.length > 0) {
@@ -987,6 +1197,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
987
1197
  renderMergeText(activeMerge)
988
1198
  ),
989
1199
  ) : null,
1200
+ planUi.visible && planUi.bandLines.length > 0
1201
+ ? h(Box, {
1202
+ flexDirection: "column",
1203
+ width: "100%",
1204
+ marginTop: 1,
1205
+ },
1206
+ ...planUi.bandLines.map((line, idx) => h(Text, {
1207
+ key: `plan-band-${idx}`,
1208
+ color: "magenta",
1209
+ dimColor: idx > 0,
1210
+ wrap: "truncate",
1211
+ }, line || " ")),
1212
+ )
1213
+ : null,
1214
+ interactionLines.length > 0
1215
+ ? h(Box, {
1216
+ flexDirection: "column",
1217
+ width: "100%",
1218
+ marginTop: 1,
1219
+ },
1220
+ ...interactionLines.map((line, idx) => h(Text, {
1221
+ key: `ask-${idx}`,
1222
+ color: "yellow",
1223
+ wrap: "truncate",
1224
+ }, line || " ")),
1225
+ )
1226
+ : null,
990
1227
  h(Box, { marginTop: 1, width: "100%" },
991
1228
  h(Text, { color: "gray" }, statusText),
992
1229
  h(Box, { flexGrow: 1 }),
@@ -1047,6 +1284,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1047
1284
  const pending = pendingTaskRef.current;
1048
1285
  if (pending && pending.abortController && !pending.abortController.signal.aborted) {
1049
1286
  try { pending.abortController.abort(); } catch { /* ignore */ }
1287
+ try {
1288
+ const { clearUserPrompts } = require("../../code/context/userNudge");
1289
+ if (props.state && props.state.executionState) {
1290
+ clearUserPrompts(props.state.executionState);
1291
+ }
1292
+ } catch { /* ignore */ }
1050
1293
  appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
1051
1294
  setStatus({
1052
1295
  message: "Cancelling...",
@@ -1190,10 +1433,13 @@ function collapseThinkingTail(text, maxChars = 80) {
1190
1433
  return `…${candidate.slice(-(limit - 1))}`;
1191
1434
  }
1192
1435
 
1193
- function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
1436
+ function computeStatusText(status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
1194
1437
  const message = String((status && status.message) || "");
1195
1438
  const suffix = String(backgroundSuffix || "");
1196
- if (!message) return `UCODE · Ready${suffix}`;
1439
+ if (!message) {
1440
+ const hint = String(idlePlanHint || "").trim();
1441
+ return hint ? `UCODE · Ready · ${hint}${suffix}` : `UCODE · Ready${suffix}`;
1442
+ }
1197
1443
  const type = inferStatusType(message, status && status.type);
1198
1444
  if (type === "done" || type === "success") {
1199
1445
  const clean = message.trim();
@@ -1213,11 +1459,11 @@ function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
1213
1459
  return `${indicator} ${message}${timerText}${suffix}`;
1214
1460
  }
1215
1461
 
1216
- function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "") {
1462
+ function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
1217
1463
  // Dependencies intentionally include startedAt so the timer ticks even
1218
1464
  // when the message string is unchanged.
1219
1465
  return React.useMemo(
1220
- () => computeStatusText(status, spinnerTick, backgroundSuffix),
1221
- [status, spinnerTick, backgroundSuffix]
1466
+ () => computeStatusText(status, spinnerTick, backgroundSuffix, idlePlanHint),
1467
+ [status, spinnerTick, backgroundSuffix, idlePlanHint]
1222
1468
  );
1223
1469
  }
@@ -1,13 +0,0 @@
1
- "use strict";
2
-
3
- function isContextV2Enabled(env = process.env) {
4
- const raw = String(env.UFOO_UCODE_CONTEXT_V2 || "").trim().toLowerCase();
5
- // Default ON. Explicit opt-out: 0 / false / off / no.
6
- if (!raw) return true;
7
- if (raw === "0" || raw === "false" || raw === "off" || raw === "no") return false;
8
- return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
9
- }
10
-
11
- module.exports = {
12
- isContextV2Enabled,
13
- };