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.
- package/package.json +1 -1
- package/src/code/agent.js +350 -246
- package/src/code/commands.js +16 -0
- package/src/code/context/assembler.js +18 -13
- package/src/code/context/executionSegment.js +102 -119
- package/src/code/context/index.js +11 -1
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +405 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/promptLayers.js +21 -5
- package/src/code/context/stateCommit.js +2 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/dispatch.js +17 -1
- package/src/code/index.js +4 -0
- package/src/code/nativeRunner.js +589 -172
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +7 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +147 -18
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +394 -0
- package/src/code/runtime/taskRun.js +348 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +249 -0
- package/src/code/sessionStore.js +1 -10
- package/src/code/skills/injection.js +1 -0
- package/src/code/taskDecomposer.js +32 -8
- package/src/code/taskRoute.js +73 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/ui/format/index.js +25 -1
- package/src/ui/format/markdownRenderer.js +224 -2
- package/src/ui/ink/UcodeApp.js +268 -22
- package/src/code/context/featureFlag.js +0 -13
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -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
|
|
328
|
-
const raw = String(
|
|
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 =
|
|
378
|
+
renderedLines = raw.split(/\r?\n/);
|
|
335
379
|
}
|
|
336
380
|
} catch {
|
|
337
|
-
renderedLines =
|
|
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
|
-
|
|
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) =>
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
}, [
|
|
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(
|
|
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 ** / ###
|
|
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)
|
|
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
|
-
};
|