u-foo 2.5.15 → 3.0.0

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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +333 -243
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +97 -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 +398 -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 +2 -0
  18. package/src/code/nativeRunner.js +518 -37
  19. package/src/code/repl.js +160 -18
  20. package/src/code/runtime/agentWakeup.js +58 -0
  21. package/src/code/runtime/graphOwner.js +41 -0
  22. package/src/code/runtime/graphYieldRouter.js +42 -0
  23. package/src/code/runtime/index.js +15 -0
  24. package/src/code/runtime/loopMailbox.js +124 -0
  25. package/src/code/runtime/runtimeEvents.js +39 -0
  26. package/src/code/runtime/taskControl.js +565 -0
  27. package/src/code/runtime/taskFocus.js +165 -0
  28. package/src/code/runtime/taskLoop.js +383 -0
  29. package/src/code/runtime/taskRun.js +187 -0
  30. package/src/code/runtime/toolProvenance.js +70 -0
  31. package/src/code/runtime/workspaceLease.js +208 -0
  32. package/src/code/sessionStore.js +0 -10
  33. package/src/code/skills/injection.js +1 -0
  34. package/src/code/taskDecomposer.js +32 -8
  35. package/src/code/tools/askUser.js +11 -0
  36. package/src/code/tools/planGraph.js +29 -0
  37. package/src/ui/format/index.js +25 -1
  38. package/src/ui/format/markdownRenderer.js +224 -2
  39. package/src/ui/ink/UcodeApp.js +285 -22
  40. 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,129 @@ 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 {
954
+ hasPendingUserInteraction,
955
+ parseUserInteractionInput,
956
+ getPendingUserInteraction,
957
+ } = require("../../code/context/userInteraction");
958
+ if (props.state && props.state.executionState && hasPendingUserInteraction(props.state.executionState)) {
959
+ const pending = getPendingUserInteraction(props.state.executionState);
960
+ const parsed = parseUserInteractionInput(pending, trimmed);
961
+ if (!parsed.ok) {
962
+ appendLogText(parsed.error || "Invalid reply", "error");
963
+ return;
964
+ }
965
+ appendLogText(`› ${trimmed}`, "user");
966
+ const startedAt = Date.now();
967
+ setStatus({
968
+ message: "Applying your reply...",
969
+ type: "thinking",
970
+ showTimer: true,
971
+ startedAt,
972
+ });
973
+ runChainRef.current = runChainRef.current
974
+ .then(async () => {
975
+ const resume = typeof props.resumeAfterUserInteraction === "function"
976
+ ? props.resumeAfterUserInteraction
977
+ : require("../../code/agent").resumeAfterUserInteraction;
978
+ let streamBuf = "";
979
+ let sawStreamText = false;
980
+ let streamStarted = false;
981
+ let dropLeadingStreamBlank = false;
982
+ const result = await resume(trimmed, props.state, {
983
+ onDelta: (delta) => {
984
+ const text = String(delta || "");
985
+ if (!text) return;
986
+ if (!streamStarted) {
987
+ flushActiveMerge();
988
+ streamStarted = true;
989
+ }
990
+ const split = fmt.splitStreamingLogChunk(streamBuf, text, {
991
+ dropLeadingBlank: dropLeadingStreamBlank,
992
+ });
993
+ if (split.sawVisible) {
994
+ sawStreamText = true;
995
+ dropLeadingStreamBlank = false;
996
+ }
997
+ for (const line of split.lines) {
998
+ appendLogLine(line);
999
+ }
1000
+ streamBuf = split.buffer;
1001
+ },
1002
+ });
1003
+ if (streamBuf) {
1004
+ if (/[^\s]/.test(streamBuf)) sawStreamText = true;
1005
+ appendLogLine(streamBuf);
1006
+ }
1007
+ flushTableBuffer();
1008
+ refreshPlanUi();
1009
+ if (result && result.waitingUserInteraction) {
1010
+ appendLogText("Still waiting for your reply.", "system");
1011
+ setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1012
+ return;
1013
+ }
1014
+ if (!result || result.ok === false) {
1015
+ appendLogText(`Error: ${(result && result.error) || "resume failed"}`, "error");
1016
+ } else {
1017
+ // Skip summary echo when deltas were already rendered (mirrors NL path).
1018
+ const shouldSkipSummary = Boolean(result.streamed && result.ok && sawStreamText);
1019
+ if (result.summary && !shouldSkipSummary) {
1020
+ appendLogText(result.summary);
1021
+ }
1022
+ }
1023
+ setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1024
+ })
1025
+ .catch((err) => {
1026
+ appendLogText(`Error: ${err && err.message ? err.message : err}`, "error");
1027
+ setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
1028
+ });
1029
+ return;
1030
+ }
1031
+ } catch (err) {
1032
+ appendLogText(`Error: ${err && err.message ? err.message : "interaction failed"}`, "error");
1033
+ return;
1034
+ }
1035
+
1036
+ // While a native task is in flight, queue an additional user reminder
1037
+ // for the next LLM turn instead of starting a second NL task.
1038
+ if (pendingTaskRef.current) {
1039
+ const { enqueueUserPrompt } = require("../../code/context/userNudge");
1040
+ const { emptyExecutionState } = require("../../code/context/executionSegment");
1041
+ if (!props.state || typeof props.state !== "object") {
1042
+ appendLogText("Error: missing session state for user reminder", "error");
1043
+ return;
1044
+ }
1045
+ if (!props.state.executionState || typeof props.state.executionState !== "object") {
1046
+ props.state.executionState = emptyExecutionState();
1047
+ }
1048
+ const queued = enqueueUserPrompt(props.state.executionState, trimmed);
1049
+ appendLogText(
1050
+ queued.enqueued
1051
+ ? `Queued user reminder for next model turn: ${trimmed.slice(0, 120)}${trimmed.length > 120 ? "…" : ""}`
1052
+ : "Could not queue user reminder (empty).",
1053
+ "system",
1054
+ );
1055
+ return;
1056
+ }
1057
+
851
1058
  // Serialize executions so streaming tasks don't interleave.
852
1059
  runChainRef.current = runChainRef.current
853
1060
  .then(() => executeLine(value))
854
1061
  .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
855
- }, [draft, executeLine, appendLogText]);
1062
+ }, [
1063
+ draft,
1064
+ executeLine,
1065
+ appendLogText,
1066
+ appendLogLine,
1067
+ flushActiveMerge,
1068
+ flushTableBuffer,
1069
+ props.state,
1070
+ props.resumeAfterUserInteraction,
1071
+ refreshPlanUi,
1072
+ ]);
856
1073
 
857
1074
  useEffect(() => {
858
1075
  if (!stdout) return undefined;
@@ -865,6 +1082,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
865
1082
  return () => stdout.off("resize", update);
866
1083
  }, [stdout]);
867
1084
 
1085
+ useEffect(() => {
1086
+ refreshPlanUi();
1087
+ }, [refreshPlanUi]);
1088
+
868
1089
  // Drive the spinner + elapsed-timer redraws while a task is in flight.
869
1090
  useEffect(() => {
870
1091
  const statusType = inferStatusType(status.message, status.type);
@@ -878,7 +1099,13 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
878
1099
  return () => clearInterval(timer);
879
1100
  }, [status.message, status.type, status.showTimer]);
880
1101
 
881
- const statusText = useMemoStatusText(React, status, spinnerTick, getBackgroundSuffix());
1102
+ const statusText = useMemoStatusText(
1103
+ React,
1104
+ status,
1105
+ spinnerTick,
1106
+ getBackgroundSuffix(),
1107
+ !status.message ? (planUi.idleHint || "") : ""
1108
+ );
882
1109
 
883
1110
  // Top-level catches Ctrl+C / Ctrl+O, plus completion popup navigation
884
1111
  // while a slash/agent menu is open.
@@ -940,12 +1167,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
940
1167
  return h(Box, { flexDirection: "column", width: "100%" },
941
1168
  h(Box, { flexDirection: "column", width: "100%" },
942
1169
  ...(() => {
943
- // Re-render raw markdown at paint time so leftover ** / ### from
944
- // older append paths or nested `**code**` patterns still resolve.
1170
+ // Re-render raw markdown at paint time so leftover ** / ### / tables
1171
+ // from older append paths or nested `**code**` patterns still resolve.
945
1172
  const mdState = { inCodeBlock: false };
946
1173
  return logLines.map((item, idx) => {
947
1174
  let text = item.text || " ";
948
- if (MARKDOWN_LOG_KINDS.has(item.kind) && /(?:\*\*|__|^\s*#{1,6}\s|^\s*`{3})/m.test(text)) {
1175
+ if (MARKDOWN_LOG_KINDS.has(item.kind) && /(?:\*\*|__|^\s*#{1,6}\s|^\s*`{3}|^\s*\|)/m.test(text)) {
949
1176
  try {
950
1177
  const rendered = fmt.renderLogLinesWithMarkdownAnsi(text, mdState);
951
1178
  if (Array.isArray(rendered) && rendered.length > 0) {
@@ -987,6 +1214,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
987
1214
  renderMergeText(activeMerge)
988
1215
  ),
989
1216
  ) : null,
1217
+ planUi.visible && planUi.bandLines.length > 0
1218
+ ? h(Box, {
1219
+ flexDirection: "column",
1220
+ width: "100%",
1221
+ marginTop: 1,
1222
+ },
1223
+ ...planUi.bandLines.map((line, idx) => h(Text, {
1224
+ key: `plan-band-${idx}`,
1225
+ color: "magenta",
1226
+ dimColor: idx > 0,
1227
+ wrap: "truncate",
1228
+ }, line || " ")),
1229
+ )
1230
+ : null,
1231
+ interactionLines.length > 0
1232
+ ? h(Box, {
1233
+ flexDirection: "column",
1234
+ width: "100%",
1235
+ marginTop: 1,
1236
+ },
1237
+ ...interactionLines.map((line, idx) => h(Text, {
1238
+ key: `ask-${idx}`,
1239
+ color: "yellow",
1240
+ wrap: "truncate",
1241
+ }, line || " ")),
1242
+ )
1243
+ : null,
990
1244
  h(Box, { marginTop: 1, width: "100%" },
991
1245
  h(Text, { color: "gray" }, statusText),
992
1246
  h(Box, { flexGrow: 1 }),
@@ -1047,6 +1301,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1047
1301
  const pending = pendingTaskRef.current;
1048
1302
  if (pending && pending.abortController && !pending.abortController.signal.aborted) {
1049
1303
  try { pending.abortController.abort(); } catch { /* ignore */ }
1304
+ try {
1305
+ const { clearUserPrompts } = require("../../code/context/userNudge");
1306
+ if (props.state && props.state.executionState) {
1307
+ clearUserPrompts(props.state.executionState);
1308
+ }
1309
+ } catch { /* ignore */ }
1050
1310
  appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
1051
1311
  setStatus({
1052
1312
  message: "Cancelling...",
@@ -1190,10 +1450,13 @@ function collapseThinkingTail(text, maxChars = 80) {
1190
1450
  return `…${candidate.slice(-(limit - 1))}`;
1191
1451
  }
1192
1452
 
1193
- function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
1453
+ function computeStatusText(status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
1194
1454
  const message = String((status && status.message) || "");
1195
1455
  const suffix = String(backgroundSuffix || "");
1196
- if (!message) return `UCODE · Ready${suffix}`;
1456
+ if (!message) {
1457
+ const hint = String(idlePlanHint || "").trim();
1458
+ return hint ? `UCODE · Ready · ${hint}${suffix}` : `UCODE · Ready${suffix}`;
1459
+ }
1197
1460
  const type = inferStatusType(message, status && status.type);
1198
1461
  if (type === "done" || type === "success") {
1199
1462
  const clean = message.trim();
@@ -1213,11 +1476,11 @@ function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
1213
1476
  return `${indicator} ${message}${timerText}${suffix}`;
1214
1477
  }
1215
1478
 
1216
- function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "") {
1479
+ function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
1217
1480
  // Dependencies intentionally include startedAt so the timer ticks even
1218
1481
  // when the message string is unchanged.
1219
1482
  return React.useMemo(
1220
- () => computeStatusText(status, spinnerTick, backgroundSuffix),
1221
- [status, spinnerTick, backgroundSuffix]
1483
+ () => computeStatusText(status, spinnerTick, backgroundSuffix, idlePlanHint),
1484
+ [status, spinnerTick, backgroundSuffix, idlePlanHint]
1222
1485
  );
1223
1486
  }
@@ -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
- };