u-foo 2.5.14 → 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.
- package/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- 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 +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -29,10 +29,17 @@ const LOG_LINE_TEXT_PROPS = {
|
|
|
29
29
|
assistant: {},
|
|
30
30
|
system: { color: "gray", dimColor: true },
|
|
31
31
|
error: { color: "red" },
|
|
32
|
+
tool: {},
|
|
32
33
|
toolDetail: { color: "gray", dimColor: true },
|
|
33
34
|
bus: { color: "cyan" },
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
// Only assistant prose gets markdown. Error rows are app-generated
|
|
38
|
+
// (`Error: …`) and already painted red via resolveLogLineTextProps — running
|
|
39
|
+
// them through the MD Error: line rule would wrap chalk ANSI and break the
|
|
40
|
+
// plain-text body the Ink color prop expects.
|
|
41
|
+
const MARKDOWN_LOG_KINDS = new Set(["assistant"]);
|
|
42
|
+
|
|
36
43
|
// Resolve a log line kind to ink <Text> props. Unknown/missing kinds (e.g.
|
|
37
44
|
// the banner, which already carries chalk ANSI styling) render uncolored.
|
|
38
45
|
function resolveLogLineTextProps(kind) {
|
|
@@ -50,6 +57,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
50
57
|
engine: (props.state && props.state.engine) || "ufoo-core",
|
|
51
58
|
workspaceRoot: props.workspaceRoot,
|
|
52
59
|
sessionId: (props.state && props.state.sessionId) || "",
|
|
60
|
+
planMode: Boolean(
|
|
61
|
+
props.state
|
|
62
|
+
&& props.state.executionState
|
|
63
|
+
&& props.state.executionState.planMode
|
|
64
|
+
),
|
|
53
65
|
});
|
|
54
66
|
|
|
55
67
|
return function UcodeApp() {
|
|
@@ -68,6 +80,15 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
68
80
|
showTimer: false,
|
|
69
81
|
startedAt: 0,
|
|
70
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([]);
|
|
71
92
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
72
93
|
const [size, setSize] = useState({ cols: 0, rows: 0 });
|
|
73
94
|
const [agents, setAgents] = useState([]);
|
|
@@ -95,6 +116,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
95
116
|
// visual row (i.e. moveCursorVertically returned moved=false).
|
|
96
117
|
const [inputHistory, setInputHistory] = useState([]);
|
|
97
118
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
119
|
+
const [completionIndex, setCompletionIndex] = useState(0);
|
|
120
|
+
const [completionWindowStart, setCompletionWindowStart] = useState(0);
|
|
121
|
+
const [completionSuppressedDraft, setCompletionSuppressedDraft] = useState(null);
|
|
122
|
+
const POPUP_PAGE_SIZE = 8;
|
|
98
123
|
const { exit } = useApp();
|
|
99
124
|
const { stdout } = useStdout();
|
|
100
125
|
const lineSeqRef = useRef(banner.length + 1);
|
|
@@ -107,6 +132,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
107
132
|
const thinkingTailRef = useRef("");
|
|
108
133
|
const thinkingFlushAtRef = useRef(0);
|
|
109
134
|
const thinkingTimerRef = useRef(null);
|
|
135
|
+
// Persist fence/open-code state across streamed assistant log lines so
|
|
136
|
+
// ``` blocks stay styled even when deltas arrive one line at a time.
|
|
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());
|
|
110
141
|
|
|
111
142
|
const targetAgent = agentSelectionMode && selectedAgentIndex >= 0
|
|
112
143
|
? agents[selectedAgentIndex]
|
|
@@ -114,6 +145,33 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
114
145
|
|
|
115
146
|
const bumpBackground = useCallback(() => setBackgroundVersion((v) => v + 1), []);
|
|
116
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
|
+
|
|
117
175
|
const getBackgroundSuffix = useCallback(() => {
|
|
118
176
|
const tasks = backgroundTasksRef.current;
|
|
119
177
|
if (!tasks || tasks.size === 0) return "";
|
|
@@ -208,6 +266,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
208
266
|
if (transition.moved) {
|
|
209
267
|
setHistoryIndex(transition.nextHistoryIndex);
|
|
210
268
|
setDraft(transition.nextValue);
|
|
269
|
+
setCompletionSuppressedDraft(transition.nextValue || null);
|
|
211
270
|
setDraftVersion((v) => v + 1);
|
|
212
271
|
return;
|
|
213
272
|
}
|
|
@@ -224,22 +283,31 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
224
283
|
}
|
|
225
284
|
}, [inputHistory, historyIndex, agents, agentSelectionMode, selectedAgentIndex]);
|
|
226
285
|
|
|
227
|
-
const onArrowUpAtStart = useCallback(() => {
|
|
228
|
-
//
|
|
229
|
-
//
|
|
286
|
+
const onArrowUpAtStart = useCallback((currentValue) => {
|
|
287
|
+
// While @-targeting an agent with an empty draft, Up clears the
|
|
288
|
+
// selection before walking input history — otherwise history eats the
|
|
289
|
+
// key and the ›@agent prefix sticks.
|
|
290
|
+
const inputValue = currentValue != null ? currentValue : draft;
|
|
291
|
+
if (fmt.shouldClearAgentSelectionOnUp({
|
|
292
|
+
agentSelectionMode,
|
|
293
|
+
inputValue,
|
|
294
|
+
})) {
|
|
295
|
+
setAgentSelectionMode(false);
|
|
296
|
+
setSelectedAgentIndex(-1);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// History: if we're already on the top visual row, walk back through
|
|
300
|
+
// the recent history before doing anything else.
|
|
230
301
|
if (inputHistory.length > 0) {
|
|
231
302
|
const nextIndex = Math.max(0, historyIndex - 1);
|
|
232
303
|
if (nextIndex !== historyIndex || draft !== inputHistory[nextIndex]) {
|
|
233
304
|
setHistoryIndex(nextIndex);
|
|
234
|
-
|
|
305
|
+
const nextValue = inputHistory[nextIndex] || "";
|
|
306
|
+
setDraft(nextValue);
|
|
307
|
+
setCompletionSuppressedDraft(nextValue || null);
|
|
235
308
|
setDraftVersion((v) => v + 1);
|
|
236
|
-
return;
|
|
237
309
|
}
|
|
238
310
|
}
|
|
239
|
-
if (agentSelectionMode) {
|
|
240
|
-
setAgentSelectionMode(false);
|
|
241
|
-
setSelectedAgentIndex(-1);
|
|
242
|
-
}
|
|
243
311
|
}, [inputHistory, historyIndex, draft, agentSelectionMode]);
|
|
244
312
|
|
|
245
313
|
const onArrowSideAtEmpty = useCallback((direction) => {
|
|
@@ -253,15 +321,94 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
253
321
|
setSelectedAgentIndex(next);
|
|
254
322
|
}, [agents, agentSelectionMode, selectedAgentIndex]);
|
|
255
323
|
|
|
256
|
-
const
|
|
324
|
+
const { UCODE_COMMAND_REGISTRY, UCODE_COMMAND_TREE } = require("../../code/commands");
|
|
325
|
+
const { listSessionSummaries } = require("../../code/sessionStore");
|
|
326
|
+
const { suggestUcodeModels, applyUcodeModelCommand } = require("../../code/modelCommand");
|
|
327
|
+
let resumeSessions = [];
|
|
328
|
+
try {
|
|
329
|
+
resumeSessions = listSessionSummaries(props.workspaceRoot || process.cwd(), { limit: 40 });
|
|
330
|
+
} catch {
|
|
331
|
+
resumeSessions = [];
|
|
332
|
+
}
|
|
333
|
+
const modelSuggestions = suggestUcodeModels(props.state || {});
|
|
334
|
+
|
|
335
|
+
const completions = fmt.buildCompletions({
|
|
336
|
+
text: draft,
|
|
337
|
+
agents: agents.map((a) => String((a && (a.fullId || a.id || a.nickname)) || "")).filter(Boolean),
|
|
338
|
+
agentLabels: agents.map((a) => getAgentLabel(a)),
|
|
339
|
+
commands: UCODE_COMMAND_REGISTRY,
|
|
340
|
+
commandTree: UCODE_COMMAND_TREE,
|
|
341
|
+
argumentLists: {
|
|
342
|
+
"/resume": resumeSessions,
|
|
343
|
+
"/model": modelSuggestions,
|
|
344
|
+
},
|
|
345
|
+
limit: 20,
|
|
346
|
+
});
|
|
347
|
+
const completionsOpen = completions.length > 0 && draft !== completionSuppressedDraft;
|
|
348
|
+
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (completions.length === 0) {
|
|
351
|
+
if (completionIndex !== 0) setCompletionIndex(0);
|
|
352
|
+
if (completionWindowStart !== 0) setCompletionWindowStart(0);
|
|
353
|
+
} else if (completionIndex >= completions.length) {
|
|
354
|
+
setCompletionIndex(completions.length - 1);
|
|
355
|
+
setCompletionWindowStart(Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
356
|
+
}
|
|
357
|
+
}, [completions.length, completionIndex, completionWindowStart]);
|
|
358
|
+
|
|
359
|
+
const acceptCompletion = useCallback(() => {
|
|
360
|
+
if (!completionsOpen) return false;
|
|
361
|
+
const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
|
|
362
|
+
if (item) {
|
|
363
|
+
setDraft(item.replace);
|
|
364
|
+
setCompletionSuppressedDraft(item.hasChildren ? null : item.replace);
|
|
365
|
+
setDraftVersion((v) => v + 1);
|
|
366
|
+
}
|
|
367
|
+
setCompletionIndex(0);
|
|
368
|
+
return true;
|
|
369
|
+
}, [completionsOpen, completions, completionIndex]);
|
|
370
|
+
|
|
371
|
+
const pushRenderedLogLines = useCallback((rawText, kind = "assistant") => {
|
|
372
|
+
const raw = String(rawText == null ? "" : rawText);
|
|
373
|
+
let renderedLines = [raw];
|
|
374
|
+
if (MARKDOWN_LOG_KINDS.has(kind)) {
|
|
375
|
+
try {
|
|
376
|
+
renderedLines = fmt.renderLogLinesWithMarkdownAnsi(raw, markdownStateRef.current);
|
|
377
|
+
if (!Array.isArray(renderedLines) || renderedLines.length === 0) {
|
|
378
|
+
renderedLines = raw.split(/\r?\n/);
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
renderedLines = raw.split(/\r?\n/);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
257
384
|
setLogLines((prev) => {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
385
|
+
const next = prev.slice();
|
|
386
|
+
for (const line of renderedLines) {
|
|
387
|
+
const id = `l-${lineSeqRef.current}`;
|
|
388
|
+
lineSeqRef.current += 1;
|
|
389
|
+
next.push({ id, text: String(line || ""), kind });
|
|
390
|
+
}
|
|
261
391
|
return next.length > 1000 ? next.slice(-1000) : next;
|
|
262
392
|
});
|
|
263
393
|
}, []);
|
|
264
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
|
+
|
|
265
412
|
const renderMergeText = useCallback((merge) => {
|
|
266
413
|
if (!merge || !Array.isArray(merge.entries)) return "";
|
|
267
414
|
return fmt.buildToolMergeRowText(merge.entries);
|
|
@@ -274,7 +421,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
274
421
|
const flushActiveMerge = useCallback(() => {
|
|
275
422
|
setActiveMerge((current) => {
|
|
276
423
|
if (!current) return null;
|
|
277
|
-
appendLogLine(renderMergeText(current));
|
|
424
|
+
appendLogLine(renderMergeText(current), "tool");
|
|
278
425
|
return null;
|
|
279
426
|
});
|
|
280
427
|
}, [appendLogLine, renderMergeText]);
|
|
@@ -285,7 +432,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
285
432
|
const resObj = payload && typeof payload === "object" ? payload : (entry && entry.result) || {};
|
|
286
433
|
const phase = String((entry && entry.phase) || "").trim().toLowerCase();
|
|
287
434
|
const isError = phase === "error" || resObj.ok === false;
|
|
288
|
-
const detail =
|
|
435
|
+
const detail = fmt.normalizeToolLogDetail(tool, entry && entry.args, resObj);
|
|
289
436
|
const errorText = String((entry && entry.error) || resObj.error || "").trim();
|
|
290
437
|
const toolEntry = fmt.normalizeToolMergeEntry({ tool, detail, isError, errorText });
|
|
291
438
|
|
|
@@ -305,12 +452,14 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
305
452
|
// Multi-line text → split into separate log entries so <Static> keys
|
|
306
453
|
// stay stable when streaming arrives line-by-line. Always promote any
|
|
307
454
|
// in-flight tool group first so it freezes above the new text.
|
|
455
|
+
// Table rows are re-batched inside appendLogLine before markdown render.
|
|
308
456
|
const raw = String(text == null ? "" : text);
|
|
309
457
|
if (!raw) return;
|
|
310
458
|
flushActiveMerge();
|
|
311
459
|
const lines = raw.split(/\r?\n/);
|
|
312
460
|
for (const line of lines) appendLogLine(line, kind);
|
|
313
|
-
|
|
461
|
+
if (MARKDOWN_LOG_KINDS.has(kind)) flushTableBuffer();
|
|
462
|
+
}, [appendLogLine, flushActiveMerge, flushTableBuffer]);
|
|
314
463
|
|
|
315
464
|
const expandLastMerge = useCallback(() => {
|
|
316
465
|
// Try the active group first; fall back to the most recent frozen one.
|
|
@@ -370,6 +519,61 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
370
519
|
case "error":
|
|
371
520
|
appendLogText(result.output || "");
|
|
372
521
|
return;
|
|
522
|
+
case "status": {
|
|
523
|
+
try {
|
|
524
|
+
const { summarizeSessionUsage, formatSessionUsageStatus } = require("../../code/usageStore");
|
|
525
|
+
const usageSummary = summarizeSessionUsage({
|
|
526
|
+
workspaceRoot: runtimeWorkspace,
|
|
527
|
+
sessionId: (props.state && props.state.sessionId) || "",
|
|
528
|
+
});
|
|
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
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
appendLogText(`Error: ${err && err.message ? err.message : "status failed"}`, "error");
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
case "model": {
|
|
544
|
+
const applied = applyUcodeModelCommand(props.state || {}, result);
|
|
545
|
+
appendLogText(applied.output || "", applied.ok ? "system" : "error");
|
|
546
|
+
if (applied.ok && result.action === "set" && typeof props.persistSessionState === "function") {
|
|
547
|
+
try {
|
|
548
|
+
const persisted = props.persistSessionState(props.state);
|
|
549
|
+
if (persisted && persisted.ok === false) {
|
|
550
|
+
appendLogText(
|
|
551
|
+
`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}`,
|
|
552
|
+
"error"
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
// persist is best-effort after a successful model switch
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
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
|
+
}
|
|
373
577
|
case "ubus": {
|
|
374
578
|
setStatus({ message: "Checking bus messages...", type: "typing", showTimer: false, startedAt: Date.now() });
|
|
375
579
|
try {
|
|
@@ -415,7 +619,32 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
415
619
|
appendLogText(`Error: ${(resumed && resumed.error) || "resume failed"}`, "error");
|
|
416
620
|
return;
|
|
417
621
|
}
|
|
418
|
-
|
|
622
|
+
// Rebuild the visible log from the restored session transcript so
|
|
623
|
+
// the user sees prior turns instead of only a status toast.
|
|
624
|
+
markdownStateRef.current = { inCodeBlock: false };
|
|
625
|
+
tableBufRef.current = fmt.createMarkdownTableBuffer();
|
|
626
|
+
const history = fmt.buildUcodeSessionLogEntries(
|
|
627
|
+
Array.isArray(props.state && props.state.nlMessages) ? props.state.nlMessages : [],
|
|
628
|
+
{ markdownState: markdownStateRef.current, idPrefix: "h", startSeq: 0 },
|
|
629
|
+
);
|
|
630
|
+
const bannerEntries = banner.concat([""]).map((line, idx) => ({
|
|
631
|
+
id: `b-${idx}`,
|
|
632
|
+
text: line,
|
|
633
|
+
}));
|
|
634
|
+
const notice = {
|
|
635
|
+
id: `h-resume-${Date.now().toString(36)}`,
|
|
636
|
+
text: `Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`,
|
|
637
|
+
kind: "system",
|
|
638
|
+
};
|
|
639
|
+
const nextLines = bannerEntries.concat(history.entries).concat([notice]);
|
|
640
|
+
setLogLines(nextLines.length > 1000 ? nextLines.slice(-1000) : nextLines);
|
|
641
|
+
lineSeqRef.current = Math.max(
|
|
642
|
+
bannerEntries.length + 1,
|
|
643
|
+
Number(history.nextSeq) || 0,
|
|
644
|
+
nextLines.length,
|
|
645
|
+
);
|
|
646
|
+
setActiveMerge(null);
|
|
647
|
+
lastMergeRef.current = null;
|
|
419
648
|
return;
|
|
420
649
|
}
|
|
421
650
|
case "tool": {
|
|
@@ -480,12 +709,18 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
480
709
|
const startedAt = Date.now();
|
|
481
710
|
const abortController = new AbortController();
|
|
482
711
|
pendingTaskRef.current = { abortController, startedAt };
|
|
483
|
-
const setNlStatus = (msg) =>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
};
|
|
489
724
|
const cancelThinkingFlush = () => {
|
|
490
725
|
if (thinkingTimerRef.current) {
|
|
491
726
|
clearTimeout(thinkingTimerRef.current);
|
|
@@ -561,6 +796,9 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
561
796
|
setNlStatus(`${label}...`);
|
|
562
797
|
dropLeadingStreamBlank = true;
|
|
563
798
|
}
|
|
799
|
+
if (entry.tool === "plan_graph" || entry.phase === "end" || entry.phase === "result") {
|
|
800
|
+
refreshPlanUi();
|
|
801
|
+
}
|
|
564
802
|
logToolHint(entry, entry.result);
|
|
565
803
|
},
|
|
566
804
|
});
|
|
@@ -571,12 +809,14 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
571
809
|
pendingTaskRef.current = null;
|
|
572
810
|
cancelThinkingFlush();
|
|
573
811
|
thinkingTailRef.current = "";
|
|
812
|
+
refreshPlanUi();
|
|
574
813
|
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
575
814
|
}
|
|
576
815
|
if (streamBuf) {
|
|
577
816
|
if (/[^\s]/.test(streamBuf)) sawStreamText = true;
|
|
578
817
|
appendLogLine(streamBuf);
|
|
579
818
|
}
|
|
819
|
+
flushTableBuffer();
|
|
580
820
|
// Skip the summary echo when the model already streamed its
|
|
581
821
|
// response in full — otherwise the user sees the same text twice.
|
|
582
822
|
// Mirrors the shouldSkipSummary check in tui.js.
|
|
@@ -604,7 +844,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
604
844
|
default:
|
|
605
845
|
if (result.output) appendLogText(result.output);
|
|
606
846
|
}
|
|
607
|
-
}, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge]);
|
|
847
|
+
}, [appendLogLine, appendLogText, exit, props, logToolHint, flushActiveMerge, flushTableBuffer, refreshPlanUi]);
|
|
608
848
|
// ^ `props` is captured by the createUcodeApp closure on a single mount,
|
|
609
849
|
// so its reference is stable across renders even though it looks like a
|
|
610
850
|
// changing dep to React's exhaustive-deps lint.
|
|
@@ -707,11 +947,129 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
707
947
|
setHistoryIndex(next.length);
|
|
708
948
|
return next;
|
|
709
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
|
+
|
|
710
1058
|
// Serialize executions so streaming tasks don't interleave.
|
|
711
1059
|
runChainRef.current = runChainRef.current
|
|
712
1060
|
.then(() => executeLine(value))
|
|
713
1061
|
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
|
|
714
|
-
}, [
|
|
1062
|
+
}, [
|
|
1063
|
+
draft,
|
|
1064
|
+
executeLine,
|
|
1065
|
+
appendLogText,
|
|
1066
|
+
appendLogLine,
|
|
1067
|
+
flushActiveMerge,
|
|
1068
|
+
flushTableBuffer,
|
|
1069
|
+
props.state,
|
|
1070
|
+
props.resumeAfterUserInteraction,
|
|
1071
|
+
refreshPlanUi,
|
|
1072
|
+
]);
|
|
715
1073
|
|
|
716
1074
|
useEffect(() => {
|
|
717
1075
|
if (!stdout) return undefined;
|
|
@@ -724,6 +1082,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
724
1082
|
return () => stdout.off("resize", update);
|
|
725
1083
|
}, [stdout]);
|
|
726
1084
|
|
|
1085
|
+
useEffect(() => {
|
|
1086
|
+
refreshPlanUi();
|
|
1087
|
+
}, [refreshPlanUi]);
|
|
1088
|
+
|
|
727
1089
|
// Drive the spinner + elapsed-timer redraws while a task is in flight.
|
|
728
1090
|
useEffect(() => {
|
|
729
1091
|
const statusType = inferStatusType(status.message, status.type);
|
|
@@ -737,44 +1099,214 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
737
1099
|
return () => clearInterval(timer);
|
|
738
1100
|
}, [status.message, status.type, status.showTimer]);
|
|
739
1101
|
|
|
740
|
-
const statusText = useMemoStatusText(
|
|
1102
|
+
const statusText = useMemoStatusText(
|
|
1103
|
+
React,
|
|
1104
|
+
status,
|
|
1105
|
+
spinnerTick,
|
|
1106
|
+
getBackgroundSuffix(),
|
|
1107
|
+
!status.message ? (planUi.idleHint || "") : ""
|
|
1108
|
+
);
|
|
741
1109
|
|
|
742
|
-
// Top-level
|
|
743
|
-
//
|
|
1110
|
+
// Top-level catches Ctrl+C / Ctrl+O, plus completion popup navigation
|
|
1111
|
+
// while a slash/agent menu is open.
|
|
744
1112
|
useInput((input, key) => {
|
|
745
1113
|
if (key.ctrl && input === "c") { exit(); return; }
|
|
746
1114
|
if (key.ctrl && input === "o") { expandLastMerge(); return; }
|
|
1115
|
+
if (!completionsOpen) return;
|
|
1116
|
+
if (key.upArrow) {
|
|
1117
|
+
setCompletionIndex((i) => {
|
|
1118
|
+
const next = (i - 1 + completions.length) % completions.length;
|
|
1119
|
+
setCompletionWindowStart((ws) => {
|
|
1120
|
+
if (next < ws) return next;
|
|
1121
|
+
if (next === completions.length - 1) {
|
|
1122
|
+
return Math.max(0, completions.length - POPUP_PAGE_SIZE);
|
|
1123
|
+
}
|
|
1124
|
+
return ws;
|
|
1125
|
+
});
|
|
1126
|
+
return next;
|
|
1127
|
+
});
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (key.downArrow) {
|
|
1131
|
+
setCompletionIndex((i) => {
|
|
1132
|
+
const next = (i + 1) % completions.length;
|
|
1133
|
+
setCompletionWindowStart((ws) => {
|
|
1134
|
+
if (next === 0) return 0;
|
|
1135
|
+
if (next >= ws + POPUP_PAGE_SIZE) return next - POPUP_PAGE_SIZE + 1;
|
|
1136
|
+
return ws;
|
|
1137
|
+
});
|
|
1138
|
+
return next;
|
|
1139
|
+
});
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (key.return) {
|
|
1143
|
+
// Leaf completions (e.g. /resume <session>) run immediately on Enter.
|
|
1144
|
+
// Parents with children only fill the draft so the next menu can open.
|
|
1145
|
+
const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
|
|
1146
|
+
if (item && !item.hasChildren) {
|
|
1147
|
+
const cmd = String(item.replace || "").trim();
|
|
1148
|
+
setCompletionIndex(0);
|
|
1149
|
+
setCompletionSuppressedDraft(null);
|
|
1150
|
+
if (cmd) submit(cmd);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
acceptCompletion();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (key.tab) {
|
|
1157
|
+
acceptCompletion();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (key.escape) {
|
|
1161
|
+
setCompletionSuppressedDraft(null);
|
|
1162
|
+
setDraft("");
|
|
1163
|
+
setDraftVersion((v) => v + 1);
|
|
1164
|
+
}
|
|
747
1165
|
}, { isActive: interactive });
|
|
748
1166
|
|
|
749
1167
|
return h(Box, { flexDirection: "column", width: "100%" },
|
|
750
1168
|
h(Box, { flexDirection: "column", width: "100%" },
|
|
751
|
-
...
|
|
752
|
-
|
|
753
|
-
|
|
1169
|
+
...(() => {
|
|
1170
|
+
// Re-render raw markdown at paint time so leftover ** / ### / tables
|
|
1171
|
+
// from older append paths or nested `**code**` patterns still resolve.
|
|
1172
|
+
const mdState = { inCodeBlock: false };
|
|
1173
|
+
return logLines.map((item, idx) => {
|
|
1174
|
+
let text = item.text || " ";
|
|
1175
|
+
if (MARKDOWN_LOG_KINDS.has(item.kind) && /(?:\*\*|__|^\s*#{1,6}\s|^\s*`{3}|^\s*\|)/m.test(text)) {
|
|
1176
|
+
try {
|
|
1177
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(text, mdState);
|
|
1178
|
+
if (Array.isArray(rendered) && rendered.length > 0) {
|
|
1179
|
+
text = rendered.length === 1 ? rendered[0] : rendered.join("\n");
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
// keep original
|
|
1183
|
+
}
|
|
1184
|
+
} else if (MARKDOWN_LOG_KINDS.has(item.kind) && mdState.inCodeBlock) {
|
|
1185
|
+
try {
|
|
1186
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(text, mdState);
|
|
1187
|
+
if (Array.isArray(rendered) && rendered[0] != null) text = rendered[0];
|
|
1188
|
+
} catch {
|
|
1189
|
+
// keep original
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const textEl = h(Text, { ...resolveLogLineTextProps(item.kind) }, text || " ");
|
|
1193
|
+
// Give user turns a blank line above/below so › prompts don't
|
|
1194
|
+
// sit flush against system/tool rows. Multi-line user blocks
|
|
1195
|
+
// only pad the outer edges.
|
|
1196
|
+
if (item.kind === "user") {
|
|
1197
|
+
const prev = logLines[idx - 1];
|
|
1198
|
+
const next = logLines[idx + 1];
|
|
1199
|
+
const marginTop = !prev || prev.kind !== "user" ? 1 : 0;
|
|
1200
|
+
const marginBottom = !next || next.kind !== "user" ? 1 : 0;
|
|
1201
|
+
return h(Box, {
|
|
1202
|
+
key: item.id,
|
|
1203
|
+
width: "100%",
|
|
1204
|
+
marginTop,
|
|
1205
|
+
marginBottom,
|
|
1206
|
+
}, textEl);
|
|
1207
|
+
}
|
|
1208
|
+
return h(Text, { key: item.id, ...resolveLogLineTextProps(item.kind) }, text || " ");
|
|
1209
|
+
});
|
|
1210
|
+
})()
|
|
754
1211
|
),
|
|
755
1212
|
activeMerge ? h(Box, null,
|
|
756
1213
|
h(Text, { color: activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
|
|
757
1214
|
renderMergeText(activeMerge)
|
|
758
1215
|
),
|
|
759
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,
|
|
760
1244
|
h(Box, { marginTop: 1, width: "100%" },
|
|
761
1245
|
h(Text, { color: "gray" }, statusText),
|
|
762
1246
|
h(Box, { flexGrow: 1 }),
|
|
763
1247
|
h(Text, { color: "gray" }, `v${fmt.UCODE_VERSION}`),
|
|
764
1248
|
),
|
|
1249
|
+
completionsOpen ? (() => {
|
|
1250
|
+
const start = Math.min(completionWindowStart, Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
1251
|
+
const end = Math.min(completions.length, start + POPUP_PAGE_SIZE);
|
|
1252
|
+
const visible = completions.slice(start, end);
|
|
1253
|
+
const cols = Math.max(8, size.cols || 80);
|
|
1254
|
+
// Frame the popup with a top rule; MultilineInput's borderTop is the
|
|
1255
|
+
// matching bottom rule, so we intentionally omit a trailing ─ here.
|
|
1256
|
+
return h(Box, { flexDirection: "column", width: "100%" },
|
|
1257
|
+
h(Text, { color: "gray" }, "─".repeat(cols)),
|
|
1258
|
+
...visible.map((s, idxInWindow) => {
|
|
1259
|
+
const idx = start + idxInWindow;
|
|
1260
|
+
const selected = idx === completionIndex;
|
|
1261
|
+
// Keep label+description in one Text. Splitting into sibling
|
|
1262
|
+
// Text nodes with wrap:"truncate" lets Yoga shrink the label
|
|
1263
|
+
// and mid-cut commands (e.g. "/help" → "/he p").
|
|
1264
|
+
const line = s.description
|
|
1265
|
+
? `${s.label} ${s.description}`
|
|
1266
|
+
: String(s.label || "");
|
|
1267
|
+
return h(Box, { key: `cmp-${idx}`, width: "100%" },
|
|
1268
|
+
h(Text, {
|
|
1269
|
+
color: selected ? "cyan" : "gray",
|
|
1270
|
+
inverse: selected,
|
|
1271
|
+
wrap: "truncate",
|
|
1272
|
+
}, line),
|
|
1273
|
+
);
|
|
1274
|
+
}),
|
|
1275
|
+
);
|
|
1276
|
+
})() : null,
|
|
765
1277
|
h(Box, { width: "100%" },
|
|
766
1278
|
h(MultilineInput, {
|
|
767
1279
|
value: draft,
|
|
768
1280
|
valueVersion: draftVersion,
|
|
769
|
-
onChange: (next) =>
|
|
770
|
-
|
|
1281
|
+
onChange: (next) => {
|
|
1282
|
+
if (completionSuppressedDraft !== null && next !== completionSuppressedDraft) {
|
|
1283
|
+
setCompletionSuppressedDraft(null);
|
|
1284
|
+
}
|
|
1285
|
+
setDraft(next);
|
|
1286
|
+
},
|
|
1287
|
+
onSubmit: (value) => {
|
|
1288
|
+
setCompletionSuppressedDraft(null);
|
|
1289
|
+
submit(value);
|
|
1290
|
+
},
|
|
771
1291
|
onCancel: () => {
|
|
1292
|
+
if (completionsOpen) {
|
|
1293
|
+
setCompletionSuppressedDraft(null);
|
|
1294
|
+
setDraft("");
|
|
1295
|
+
setDraftVersion((v) => v + 1);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
772
1298
|
// If a task is in flight, Esc requests cancellation. Otherwise
|
|
773
1299
|
// it clears the agent selection (matches blessed). The text
|
|
774
1300
|
// value is left alone so the user doesn't lose what they typed.
|
|
775
1301
|
const pending = pendingTaskRef.current;
|
|
776
1302
|
if (pending && pending.abortController && !pending.abortController.signal.aborted) {
|
|
777
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 */ }
|
|
778
1310
|
appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
|
|
779
1311
|
setStatus({
|
|
780
1312
|
message: "Cancelling...",
|
|
@@ -795,11 +1327,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
795
1327
|
onArrowRightAtEmpty: () => onArrowSideAtEmpty("right"),
|
|
796
1328
|
width: Math.max(20, (size.cols || 80) - 4),
|
|
797
1329
|
interactive,
|
|
1330
|
+
interceptArrowsAndEnter: completionsOpen,
|
|
798
1331
|
placeholder: "",
|
|
799
1332
|
promptPrefix: targetAgent ? `›@${getAgentLabel(targetAgent)} ` : "› ",
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
1333
|
+
// Completions render ABOVE the input. Only the Agents footer is
|
|
1334
|
+
// below — counting popup rows here parks the hardware cursor up
|
|
1335
|
+
// into the menu (ghost block on /status etc.).
|
|
803
1336
|
linesBelowInput: 1,
|
|
804
1337
|
// During model/tool activity ucode redraws the status line every
|
|
805
1338
|
// spinner frame. Keeping the hardware cursor hidden avoids a
|
|
@@ -900,14 +1433,30 @@ function collapseThinkingTail(text, maxChars = 80) {
|
|
|
900
1433
|
const collapsed = String(text || "").replace(/\s+/g, " ").trim();
|
|
901
1434
|
const parsed = Number(maxChars);
|
|
902
1435
|
const limit = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 80;
|
|
903
|
-
if (collapsed
|
|
904
|
-
|
|
1436
|
+
if (!collapsed) return "";
|
|
1437
|
+
|
|
1438
|
+
// Prefer the latest markdown emphasis / section so the status line shows the
|
|
1439
|
+
// current thought instead of a mid-word tail of an earlier heading.
|
|
1440
|
+
let candidate = collapsed;
|
|
1441
|
+
const boldParts = collapsed.match(/\*\*[^*]+\*\*/g);
|
|
1442
|
+
if (boldParts && boldParts.length > 0) {
|
|
1443
|
+
candidate = boldParts[boldParts.length - 1].replace(/\*/g, "").trim() || candidate;
|
|
1444
|
+
} else {
|
|
1445
|
+
const clauses = collapsed.split(/(?<=[.!?。!?])\s+/).map((part) => part.trim()).filter(Boolean);
|
|
1446
|
+
if (clauses.length > 1) candidate = clauses[clauses.length - 1];
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (candidate.length <= limit) return candidate;
|
|
1450
|
+
return `…${candidate.slice(-(limit - 1))}`;
|
|
905
1451
|
}
|
|
906
1452
|
|
|
907
|
-
function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
|
|
1453
|
+
function computeStatusText(status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
|
|
908
1454
|
const message = String((status && status.message) || "");
|
|
909
1455
|
const suffix = String(backgroundSuffix || "");
|
|
910
|
-
if (!message)
|
|
1456
|
+
if (!message) {
|
|
1457
|
+
const hint = String(idlePlanHint || "").trim();
|
|
1458
|
+
return hint ? `UCODE · Ready · ${hint}${suffix}` : `UCODE · Ready${suffix}`;
|
|
1459
|
+
}
|
|
911
1460
|
const type = inferStatusType(message, status && status.type);
|
|
912
1461
|
if (type === "done" || type === "success") {
|
|
913
1462
|
const clean = message.trim();
|
|
@@ -927,11 +1476,11 @@ function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
|
|
|
927
1476
|
return `${indicator} ${message}${timerText}${suffix}`;
|
|
928
1477
|
}
|
|
929
1478
|
|
|
930
|
-
function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "") {
|
|
1479
|
+
function useMemoStatusText(React, status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
|
|
931
1480
|
// Dependencies intentionally include startedAt so the timer ticks even
|
|
932
1481
|
// when the message string is unchanged.
|
|
933
1482
|
return React.useMemo(
|
|
934
|
-
() => computeStatusText(status, spinnerTick, backgroundSuffix),
|
|
935
|
-
[status, spinnerTick, backgroundSuffix]
|
|
1483
|
+
() => computeStatusText(status, spinnerTick, backgroundSuffix, idlePlanHint),
|
|
1484
|
+
[status, spinnerTick, backgroundSuffix, idlePlanHint]
|
|
936
1485
|
);
|
|
937
1486
|
}
|