u-foo 2.5.13 → 2.5.15
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 +339 -24
- package/src/code/commands.js +61 -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 +698 -0
- package/src/code/context/executionSegment.js +314 -0
- package/src/code/context/featureFlag.js +13 -0
- package/src/code/context/index.js +18 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +159 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +412 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +4 -1
- package/src/code/index.js +6 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +187 -31
- package/src/code/repl.js +36 -32
- package/src/code/sessionStore.js +227 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +65 -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 +32 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +260 -44
- package/src/ui/format/markdownRenderer.js +215 -72
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +408 -55
- package/src/ui/ink/chatLogModel.js +102 -21
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -18,6 +18,34 @@ const { runInk } = require("../runInk");
|
|
|
18
18
|
const fmt = require("../format");
|
|
19
19
|
const { createMultilineInput } = require("./MultilineInput");
|
|
20
20
|
|
|
21
|
+
// Throttle for the live thinking-chain status line: rapid thinking_delta
|
|
22
|
+
// chunks would otherwise re-render the footer on every SSE event.
|
|
23
|
+
const THINKING_STATUS_THROTTLE_MS = 120;
|
|
24
|
+
|
|
25
|
+
// Log line kinds drive the color treatment of scrollback rows. Kind is pure
|
|
26
|
+
// presentation metadata — the stored text never changes.
|
|
27
|
+
const LOG_LINE_TEXT_PROPS = {
|
|
28
|
+
user: { color: "green", bold: true },
|
|
29
|
+
assistant: {},
|
|
30
|
+
system: { color: "gray", dimColor: true },
|
|
31
|
+
error: { color: "red" },
|
|
32
|
+
tool: {},
|
|
33
|
+
toolDetail: { color: "gray", dimColor: true },
|
|
34
|
+
bus: { color: "cyan" },
|
|
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
|
+
|
|
43
|
+
// Resolve a log line kind to ink <Text> props. Unknown/missing kinds (e.g.
|
|
44
|
+
// the banner, which already carries chalk ANSI styling) render uncolored.
|
|
45
|
+
function resolveLogLineTextProps(kind) {
|
|
46
|
+
return LOG_LINE_TEXT_PROPS[kind] || LOG_LINE_TEXT_PROPS.assistant;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
22
50
|
const { useEffect, useState, useCallback, useRef } = React;
|
|
23
51
|
const { Box, Text, useInput, useApp, useStdout } = ink;
|
|
@@ -74,11 +102,25 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
74
102
|
// visual row (i.e. moveCursorVertically returned moved=false).
|
|
75
103
|
const [inputHistory, setInputHistory] = useState([]);
|
|
76
104
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
105
|
+
const [completionIndex, setCompletionIndex] = useState(0);
|
|
106
|
+
const [completionWindowStart, setCompletionWindowStart] = useState(0);
|
|
107
|
+
const [completionSuppressedDraft, setCompletionSuppressedDraft] = useState(null);
|
|
108
|
+
const POPUP_PAGE_SIZE = 8;
|
|
77
109
|
const { exit } = useApp();
|
|
78
110
|
const { stdout } = useStdout();
|
|
79
111
|
const lineSeqRef = useRef(banner.length + 1);
|
|
80
112
|
const mergeIdRef = useRef(0);
|
|
81
113
|
const toolMergeScopeRef = useRef(0);
|
|
114
|
+
// thinkingTailRef accumulates raw thinking_delta text for the live
|
|
115
|
+
// status line; the collapsed tail is pushed through a throttled
|
|
116
|
+
// trailing flush (thinkingTimerRef) so fast streams don't re-render
|
|
117
|
+
// the footer on every chunk.
|
|
118
|
+
const thinkingTailRef = useRef("");
|
|
119
|
+
const thinkingFlushAtRef = useRef(0);
|
|
120
|
+
const thinkingTimerRef = useRef(null);
|
|
121
|
+
// Persist fence/open-code state across streamed assistant log lines so
|
|
122
|
+
// ``` blocks stay styled even when deltas arrive one line at a time.
|
|
123
|
+
const markdownStateRef = useRef({ inCodeBlock: false });
|
|
82
124
|
|
|
83
125
|
const targetAgent = agentSelectionMode && selectedAgentIndex >= 0
|
|
84
126
|
? agents[selectedAgentIndex]
|
|
@@ -180,6 +222,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
180
222
|
if (transition.moved) {
|
|
181
223
|
setHistoryIndex(transition.nextHistoryIndex);
|
|
182
224
|
setDraft(transition.nextValue);
|
|
225
|
+
setCompletionSuppressedDraft(transition.nextValue || null);
|
|
183
226
|
setDraftVersion((v) => v + 1);
|
|
184
227
|
return;
|
|
185
228
|
}
|
|
@@ -196,22 +239,31 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
196
239
|
}
|
|
197
240
|
}, [inputHistory, historyIndex, agents, agentSelectionMode, selectedAgentIndex]);
|
|
198
241
|
|
|
199
|
-
const onArrowUpAtStart = useCallback(() => {
|
|
200
|
-
//
|
|
201
|
-
//
|
|
242
|
+
const onArrowUpAtStart = useCallback((currentValue) => {
|
|
243
|
+
// While @-targeting an agent with an empty draft, Up clears the
|
|
244
|
+
// selection before walking input history — otherwise history eats the
|
|
245
|
+
// key and the ›@agent prefix sticks.
|
|
246
|
+
const inputValue = currentValue != null ? currentValue : draft;
|
|
247
|
+
if (fmt.shouldClearAgentSelectionOnUp({
|
|
248
|
+
agentSelectionMode,
|
|
249
|
+
inputValue,
|
|
250
|
+
})) {
|
|
251
|
+
setAgentSelectionMode(false);
|
|
252
|
+
setSelectedAgentIndex(-1);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// History: if we're already on the top visual row, walk back through
|
|
256
|
+
// the recent history before doing anything else.
|
|
202
257
|
if (inputHistory.length > 0) {
|
|
203
258
|
const nextIndex = Math.max(0, historyIndex - 1);
|
|
204
259
|
if (nextIndex !== historyIndex || draft !== inputHistory[nextIndex]) {
|
|
205
260
|
setHistoryIndex(nextIndex);
|
|
206
|
-
|
|
261
|
+
const nextValue = inputHistory[nextIndex] || "";
|
|
262
|
+
setDraft(nextValue);
|
|
263
|
+
setCompletionSuppressedDraft(nextValue || null);
|
|
207
264
|
setDraftVersion((v) => v + 1);
|
|
208
|
-
return;
|
|
209
265
|
}
|
|
210
266
|
}
|
|
211
|
-
if (agentSelectionMode) {
|
|
212
|
-
setAgentSelectionMode(false);
|
|
213
|
-
setSelectedAgentIndex(-1);
|
|
214
|
-
}
|
|
215
267
|
}, [inputHistory, historyIndex, draft, agentSelectionMode]);
|
|
216
268
|
|
|
217
269
|
const onArrowSideAtEmpty = useCallback((direction) => {
|
|
@@ -225,11 +277,73 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
225
277
|
setSelectedAgentIndex(next);
|
|
226
278
|
}, [agents, agentSelectionMode, selectedAgentIndex]);
|
|
227
279
|
|
|
228
|
-
const
|
|
280
|
+
const { UCODE_COMMAND_REGISTRY, UCODE_COMMAND_TREE } = require("../../code/commands");
|
|
281
|
+
const { listSessionSummaries } = require("../../code/sessionStore");
|
|
282
|
+
const { suggestUcodeModels, applyUcodeModelCommand } = require("../../code/modelCommand");
|
|
283
|
+
let resumeSessions = [];
|
|
284
|
+
try {
|
|
285
|
+
resumeSessions = listSessionSummaries(props.workspaceRoot || process.cwd(), { limit: 40 });
|
|
286
|
+
} catch {
|
|
287
|
+
resumeSessions = [];
|
|
288
|
+
}
|
|
289
|
+
const modelSuggestions = suggestUcodeModels(props.state || {});
|
|
290
|
+
|
|
291
|
+
const completions = fmt.buildCompletions({
|
|
292
|
+
text: draft,
|
|
293
|
+
agents: agents.map((a) => String((a && (a.fullId || a.id || a.nickname)) || "")).filter(Boolean),
|
|
294
|
+
agentLabels: agents.map((a) => getAgentLabel(a)),
|
|
295
|
+
commands: UCODE_COMMAND_REGISTRY,
|
|
296
|
+
commandTree: UCODE_COMMAND_TREE,
|
|
297
|
+
argumentLists: {
|
|
298
|
+
"/resume": resumeSessions,
|
|
299
|
+
"/model": modelSuggestions,
|
|
300
|
+
},
|
|
301
|
+
limit: 20,
|
|
302
|
+
});
|
|
303
|
+
const completionsOpen = completions.length > 0 && draft !== completionSuppressedDraft;
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (completions.length === 0) {
|
|
307
|
+
if (completionIndex !== 0) setCompletionIndex(0);
|
|
308
|
+
if (completionWindowStart !== 0) setCompletionWindowStart(0);
|
|
309
|
+
} else if (completionIndex >= completions.length) {
|
|
310
|
+
setCompletionIndex(completions.length - 1);
|
|
311
|
+
setCompletionWindowStart(Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
312
|
+
}
|
|
313
|
+
}, [completions.length, completionIndex, completionWindowStart]);
|
|
314
|
+
|
|
315
|
+
const acceptCompletion = useCallback(() => {
|
|
316
|
+
if (!completionsOpen) return false;
|
|
317
|
+
const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
|
|
318
|
+
if (item) {
|
|
319
|
+
setDraft(item.replace);
|
|
320
|
+
setCompletionSuppressedDraft(item.hasChildren ? null : item.replace);
|
|
321
|
+
setDraftVersion((v) => v + 1);
|
|
322
|
+
}
|
|
323
|
+
setCompletionIndex(0);
|
|
324
|
+
return true;
|
|
325
|
+
}, [completionsOpen, completions, completionIndex]);
|
|
326
|
+
|
|
327
|
+
const appendLogLine = useCallback((text, kind = "assistant") => {
|
|
328
|
+
const raw = String(text == null ? "" : text);
|
|
329
|
+
let renderedLines = [raw];
|
|
330
|
+
if (MARKDOWN_LOG_KINDS.has(kind)) {
|
|
331
|
+
try {
|
|
332
|
+
renderedLines = fmt.renderLogLinesWithMarkdownAnsi(raw, markdownStateRef.current);
|
|
333
|
+
if (!Array.isArray(renderedLines) || renderedLines.length === 0) {
|
|
334
|
+
renderedLines = [raw];
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
renderedLines = [raw];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
229
340
|
setLogLines((prev) => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
341
|
+
const next = prev.slice();
|
|
342
|
+
for (const line of renderedLines) {
|
|
343
|
+
const id = `l-${lineSeqRef.current}`;
|
|
344
|
+
lineSeqRef.current += 1;
|
|
345
|
+
next.push({ id, text: String(line || ""), kind });
|
|
346
|
+
}
|
|
233
347
|
return next.length > 1000 ? next.slice(-1000) : next;
|
|
234
348
|
});
|
|
235
349
|
}, []);
|
|
@@ -246,7 +360,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
246
360
|
const flushActiveMerge = useCallback(() => {
|
|
247
361
|
setActiveMerge((current) => {
|
|
248
362
|
if (!current) return null;
|
|
249
|
-
appendLogLine(renderMergeText(current));
|
|
363
|
+
appendLogLine(renderMergeText(current), "tool");
|
|
250
364
|
return null;
|
|
251
365
|
});
|
|
252
366
|
}, [appendLogLine, renderMergeText]);
|
|
@@ -257,7 +371,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
257
371
|
const resObj = payload && typeof payload === "object" ? payload : (entry && entry.result) || {};
|
|
258
372
|
const phase = String((entry && entry.phase) || "").trim().toLowerCase();
|
|
259
373
|
const isError = phase === "error" || resObj.ok === false;
|
|
260
|
-
const detail =
|
|
374
|
+
const detail = fmt.normalizeToolLogDetail(tool, entry && entry.args, resObj);
|
|
261
375
|
const errorText = String((entry && entry.error) || resObj.error || "").trim();
|
|
262
376
|
const toolEntry = fmt.normalizeToolMergeEntry({ tool, detail, isError, errorText });
|
|
263
377
|
|
|
@@ -273,7 +387,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
273
387
|
});
|
|
274
388
|
}, []);
|
|
275
389
|
|
|
276
|
-
const appendLogText = useCallback((text) => {
|
|
390
|
+
const appendLogText = useCallback((text, kind = "assistant") => {
|
|
277
391
|
// Multi-line text → split into separate log entries so <Static> keys
|
|
278
392
|
// stay stable when streaming arrives line-by-line. Always promote any
|
|
279
393
|
// in-flight tool group first so it freezes above the new text.
|
|
@@ -281,7 +395,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
281
395
|
if (!raw) return;
|
|
282
396
|
flushActiveMerge();
|
|
283
397
|
const lines = raw.split(/\r?\n/);
|
|
284
|
-
for (const line of lines) appendLogLine(line);
|
|
398
|
+
for (const line of lines) appendLogLine(line, kind);
|
|
285
399
|
}, [appendLogLine, flushActiveMerge]);
|
|
286
400
|
|
|
287
401
|
const expandLastMerge = useCallback(() => {
|
|
@@ -299,7 +413,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
299
413
|
const lines = fmt.buildMergedToolExpandedLines(candidate.entries);
|
|
300
414
|
for (let i = 0; i < lines.length; i += 1) {
|
|
301
415
|
const branch = i === lines.length - 1 ? "└" : "│";
|
|
302
|
-
appendLogLine(`${branch} ${lines[i]}
|
|
416
|
+
appendLogLine(`${branch} ${lines[i]}`, "toolDetail");
|
|
303
417
|
}
|
|
304
418
|
candidate.expanded = true;
|
|
305
419
|
if (active && active.id === candidate.id) setActiveMerge(null);
|
|
@@ -315,7 +429,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
315
429
|
if (!normalized) return;
|
|
316
430
|
toolMergeScopeRef.current += 1;
|
|
317
431
|
flushActiveMerge();
|
|
318
|
-
appendLogLine(`› ${normalized}
|
|
432
|
+
appendLogLine(`› ${normalized}`, "user");
|
|
319
433
|
|
|
320
434
|
const runtimeWorkspace = String(
|
|
321
435
|
(props.state && props.state.workspaceRoot) || props.workspaceRoot || process.cwd()
|
|
@@ -325,7 +439,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
325
439
|
try {
|
|
326
440
|
result = props.runSingleCommand(normalized, runtimeWorkspace);
|
|
327
441
|
} catch (err) {
|
|
328
|
-
appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}
|
|
442
|
+
appendLogText(`Error: ${err && err.message ? err.message : "command parse failed"}`, "error");
|
|
329
443
|
return;
|
|
330
444
|
}
|
|
331
445
|
if (!result || typeof result !== "object") return;
|
|
@@ -342,6 +456,37 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
342
456
|
case "error":
|
|
343
457
|
appendLogText(result.output || "");
|
|
344
458
|
return;
|
|
459
|
+
case "status": {
|
|
460
|
+
try {
|
|
461
|
+
const { summarizeSessionUsage, formatSessionUsageStatus } = require("../../code/usageStore");
|
|
462
|
+
const usageSummary = summarizeSessionUsage({
|
|
463
|
+
workspaceRoot: runtimeWorkspace,
|
|
464
|
+
sessionId: (props.state && props.state.sessionId) || "",
|
|
465
|
+
});
|
|
466
|
+
appendLogText(formatSessionUsageStatus(usageSummary), "system");
|
|
467
|
+
} catch (err) {
|
|
468
|
+
appendLogText(`Error: ${err && err.message ? err.message : "status failed"}`, "error");
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
case "model": {
|
|
473
|
+
const applied = applyUcodeModelCommand(props.state || {}, result);
|
|
474
|
+
appendLogText(applied.output || "", applied.ok ? "system" : "error");
|
|
475
|
+
if (applied.ok && result.action === "set" && typeof props.persistSessionState === "function") {
|
|
476
|
+
try {
|
|
477
|
+
const persisted = props.persistSessionState(props.state);
|
|
478
|
+
if (persisted && persisted.ok === false) {
|
|
479
|
+
appendLogText(
|
|
480
|
+
`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}`,
|
|
481
|
+
"error"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
// persist is best-effort after a successful model switch
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
345
490
|
case "ubus": {
|
|
346
491
|
setStatus({ message: "Checking bus messages...", type: "typing", showTimer: false, startedAt: Date.now() });
|
|
347
492
|
try {
|
|
@@ -350,26 +495,26 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
350
495
|
workspaceRoot: runtimeWorkspace,
|
|
351
496
|
onMessageReceived: (msg) => {
|
|
352
497
|
const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
|
|
353
|
-
appendLogText(`${nickname}: ${(msg && msg.task) || ""}
|
|
498
|
+
appendLogText(`${nickname}: ${(msg && msg.task) || ""}`, "bus");
|
|
354
499
|
},
|
|
355
500
|
});
|
|
356
501
|
if (!ubusResult || !ubusResult.ok) {
|
|
357
|
-
appendLogText(`Error: ${(ubusResult && ubusResult.error) || "ubus failed"}
|
|
502
|
+
appendLogText(`Error: ${(ubusResult && ubusResult.error) || "ubus failed"}`, "error");
|
|
358
503
|
return;
|
|
359
504
|
}
|
|
360
505
|
const exchanges = Array.isArray(ubusResult.messageExchanges) ? ubusResult.messageExchanges : [];
|
|
361
506
|
if (exchanges.length > 0) {
|
|
362
507
|
for (const exchange of exchanges) {
|
|
363
508
|
const nickname = extractAgentNickname(exchange && exchange.from) || (exchange && exchange.from) || "bus";
|
|
364
|
-
appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}
|
|
509
|
+
appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`, "bus");
|
|
365
510
|
}
|
|
366
511
|
} else if (Number(ubusResult.handled) === 0) {
|
|
367
|
-
appendLogText("ubus: no pending messages.");
|
|
512
|
+
appendLogText("ubus: no pending messages.", "system");
|
|
368
513
|
}
|
|
369
514
|
if (typeof props.persistSessionState === "function") {
|
|
370
515
|
const persisted = props.persistSessionState(props.state);
|
|
371
516
|
if (!persisted || persisted.ok === false) {
|
|
372
|
-
appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}
|
|
517
|
+
appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`, "error");
|
|
373
518
|
}
|
|
374
519
|
}
|
|
375
520
|
} finally {
|
|
@@ -379,15 +524,39 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
379
524
|
}
|
|
380
525
|
case "resume": {
|
|
381
526
|
if (typeof props.resumeSessionState !== "function") {
|
|
382
|
-
appendLogText("Error: resume unsupported");
|
|
527
|
+
appendLogText("Error: resume unsupported", "error");
|
|
383
528
|
return;
|
|
384
529
|
}
|
|
385
530
|
const resumed = props.resumeSessionState(props.state, result.sessionId, runtimeWorkspace);
|
|
386
531
|
if (!resumed || !resumed.ok) {
|
|
387
|
-
appendLogText(`Error: ${(resumed && resumed.error) || "resume failed"}
|
|
532
|
+
appendLogText(`Error: ${(resumed && resumed.error) || "resume failed"}`, "error");
|
|
388
533
|
return;
|
|
389
534
|
}
|
|
390
|
-
|
|
535
|
+
// Rebuild the visible log from the restored session transcript so
|
|
536
|
+
// the user sees prior turns instead of only a status toast.
|
|
537
|
+
markdownStateRef.current = { inCodeBlock: false };
|
|
538
|
+
const history = fmt.buildUcodeSessionLogEntries(
|
|
539
|
+
Array.isArray(props.state && props.state.nlMessages) ? props.state.nlMessages : [],
|
|
540
|
+
{ markdownState: markdownStateRef.current, idPrefix: "h", startSeq: 0 },
|
|
541
|
+
);
|
|
542
|
+
const bannerEntries = banner.concat([""]).map((line, idx) => ({
|
|
543
|
+
id: `b-${idx}`,
|
|
544
|
+
text: line,
|
|
545
|
+
}));
|
|
546
|
+
const notice = {
|
|
547
|
+
id: `h-resume-${Date.now().toString(36)}`,
|
|
548
|
+
text: `Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`,
|
|
549
|
+
kind: "system",
|
|
550
|
+
};
|
|
551
|
+
const nextLines = bannerEntries.concat(history.entries).concat([notice]);
|
|
552
|
+
setLogLines(nextLines.length > 1000 ? nextLines.slice(-1000) : nextLines);
|
|
553
|
+
lineSeqRef.current = Math.max(
|
|
554
|
+
bannerEntries.length + 1,
|
|
555
|
+
Number(history.nextSeq) || 0,
|
|
556
|
+
nextLines.length,
|
|
557
|
+
);
|
|
558
|
+
setActiveMerge(null);
|
|
559
|
+
lastMergeRef.current = null;
|
|
391
560
|
return;
|
|
392
561
|
}
|
|
393
562
|
case "tool": {
|
|
@@ -413,7 +582,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
413
582
|
backgroundTasksRef.current.set(jobId, taskRecord);
|
|
414
583
|
bumpBackground();
|
|
415
584
|
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
416
|
-
appendLogText(`[${jobId}] started in background
|
|
585
|
+
appendLogText(`[${jobId}] started in background.`, "system");
|
|
417
586
|
|
|
418
587
|
const bgState = {
|
|
419
588
|
workspaceRoot: props.state && props.state.workspaceRoot,
|
|
@@ -434,13 +603,13 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
434
603
|
taskRecord.finishedAt = Date.now();
|
|
435
604
|
taskRecord.summary = String(props.formatNlResult(nlResult, false) || "").trim();
|
|
436
605
|
const title = taskRecord.status === "done" ? "done" : "failed";
|
|
437
|
-
appendLogText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}
|
|
606
|
+
appendLogText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`, "system");
|
|
438
607
|
})
|
|
439
608
|
.catch((err) => {
|
|
440
609
|
taskRecord.status = "failed";
|
|
441
610
|
taskRecord.finishedAt = Date.now();
|
|
442
611
|
taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
|
|
443
|
-
appendLogText(`[${jobId}] failed: ${taskRecord.summary}
|
|
612
|
+
appendLogText(`[${jobId}] failed: ${taskRecord.summary}`, "system");
|
|
444
613
|
})
|
|
445
614
|
.finally(() => {
|
|
446
615
|
bumpBackground();
|
|
@@ -458,6 +627,16 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
458
627
|
showTimer: true,
|
|
459
628
|
startedAt,
|
|
460
629
|
});
|
|
630
|
+
const cancelThinkingFlush = () => {
|
|
631
|
+
if (thinkingTimerRef.current) {
|
|
632
|
+
clearTimeout(thinkingTimerRef.current);
|
|
633
|
+
thinkingTimerRef.current = null;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const flushThinkingStatus = () => {
|
|
637
|
+
thinkingFlushAtRef.current = Date.now();
|
|
638
|
+
setNlStatus(collapseThinkingTail(thinkingTailRef.current) || "Thinking...");
|
|
639
|
+
};
|
|
461
640
|
setNlStatus("Waiting for model...");
|
|
462
641
|
let streamBuf = "";
|
|
463
642
|
let sawStreamText = false;
|
|
@@ -469,10 +648,28 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
469
648
|
signal: abortController.signal,
|
|
470
649
|
onPhase: (event) => {
|
|
471
650
|
if (!event || typeof event !== "object") return;
|
|
472
|
-
if (event.type === "request_start")
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
else if (event.type === "
|
|
651
|
+
if (event.type === "request_start") {
|
|
652
|
+
cancelThinkingFlush();
|
|
653
|
+
setNlStatus("Waiting for model...");
|
|
654
|
+
} else if (event.type === "thinking_delta") {
|
|
655
|
+
thinkingTailRef.current += String(event.text || "");
|
|
656
|
+
const elapsed = Date.now() - thinkingFlushAtRef.current;
|
|
657
|
+
if (elapsed >= THINKING_STATUS_THROTTLE_MS) {
|
|
658
|
+
cancelThinkingFlush();
|
|
659
|
+
flushThinkingStatus();
|
|
660
|
+
} else if (!thinkingTimerRef.current) {
|
|
661
|
+
// Trailing flush guarantees the final tail lands even
|
|
662
|
+
// when the stream ends inside a throttle window.
|
|
663
|
+
thinkingTimerRef.current = setTimeout(() => {
|
|
664
|
+
thinkingTimerRef.current = null;
|
|
665
|
+
flushThinkingStatus();
|
|
666
|
+
}, THINKING_STATUS_THROTTLE_MS - elapsed);
|
|
667
|
+
}
|
|
668
|
+
} else if (event.type === "text_delta") {
|
|
669
|
+
cancelThinkingFlush();
|
|
670
|
+
setNlStatus("Generating response...");
|
|
671
|
+
} else if (event.type === "tool_request") {
|
|
672
|
+
cancelThinkingFlush();
|
|
476
673
|
const label = fmt.TOOL_LABELS[String(event.name || "").toLowerCase()] ||
|
|
477
674
|
`Calling ${event.name}`;
|
|
478
675
|
setNlStatus(`${label}...`);
|
|
@@ -509,10 +706,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
509
706
|
},
|
|
510
707
|
});
|
|
511
708
|
} catch (err) {
|
|
512
|
-
appendLogText(`Error: ${err && err.message ? err.message : "agent loop failed"}
|
|
709
|
+
appendLogText(`Error: ${err && err.message ? err.message : "agent loop failed"}`, "error");
|
|
513
710
|
return;
|
|
514
711
|
} finally {
|
|
515
712
|
pendingTaskRef.current = null;
|
|
713
|
+
cancelThinkingFlush();
|
|
714
|
+
thinkingTailRef.current = "";
|
|
516
715
|
setStatus({ message: "", type: "thinking", showTimer: false, startedAt: 0 });
|
|
517
716
|
}
|
|
518
717
|
if (streamBuf) {
|
|
@@ -534,7 +733,8 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
534
733
|
const persisted = props.persistSessionState(props.state);
|
|
535
734
|
if (persisted && persisted.ok === false) {
|
|
536
735
|
appendLogText(
|
|
537
|
-
`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}
|
|
736
|
+
`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${persisted.error || "unknown error"}`,
|
|
737
|
+
"error"
|
|
538
738
|
);
|
|
539
739
|
}
|
|
540
740
|
} catch {
|
|
@@ -579,7 +779,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
579
779
|
signal: abortController.signal,
|
|
580
780
|
onMessageReceived: (msg) => {
|
|
581
781
|
const nickname = extractAgentNickname(msg && msg.from) || (msg && msg.from) || "bus";
|
|
582
|
-
appendLogText(`${nickname}: ${(msg && msg.task) || ""}
|
|
782
|
+
appendLogText(`${nickname}: ${(msg && msg.task) || ""}`, "bus");
|
|
583
783
|
setStatus({
|
|
584
784
|
message: "Working on task...",
|
|
585
785
|
type: "thinking",
|
|
@@ -593,7 +793,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
593
793
|
const nextError = String((ubusResult && ubusResult.error) || "ubus failed");
|
|
594
794
|
if (nextError !== autoBusErrorRef.current) {
|
|
595
795
|
autoBusErrorRef.current = nextError;
|
|
596
|
-
appendLogText(`Error: ${nextError}
|
|
796
|
+
appendLogText(`Error: ${nextError}`, "error");
|
|
597
797
|
}
|
|
598
798
|
return;
|
|
599
799
|
}
|
|
@@ -602,12 +802,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
602
802
|
const exchanges = Array.isArray(ubusResult.messageExchanges) ? ubusResult.messageExchanges : [];
|
|
603
803
|
for (const exchange of exchanges) {
|
|
604
804
|
const nickname = extractAgentNickname(exchange && exchange.from) || (exchange && exchange.from) || "bus";
|
|
605
|
-
appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}
|
|
805
|
+
appendLogText(`@${nickname} ${(exchange && exchange.reply) || ""}`, "bus");
|
|
606
806
|
}
|
|
607
807
|
if (Number(ubusResult.handled) > 0 && typeof props.persistSessionState === "function") {
|
|
608
808
|
const persisted = props.persistSessionState(props.state);
|
|
609
809
|
if (!persisted || persisted.ok === false) {
|
|
610
|
-
appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}
|
|
810
|
+
appendLogText(`Error: failed to persist session ${(props.state && props.state.sessionId) || ""}: ${(persisted && persisted.error) || "unknown error"}`, "error");
|
|
611
811
|
}
|
|
612
812
|
}
|
|
613
813
|
} finally {
|
|
@@ -627,7 +827,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
627
827
|
autoBusQueuedRef.current = true;
|
|
628
828
|
runChainRef.current = runChainRef.current
|
|
629
829
|
.then(() => runAutoBusOnce())
|
|
630
|
-
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : "ubus failed"}
|
|
830
|
+
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : "ubus failed"}`, "error"))
|
|
631
831
|
.finally(() => {
|
|
632
832
|
autoBusQueuedRef.current = false;
|
|
633
833
|
});
|
|
@@ -651,7 +851,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
651
851
|
// Serialize executions so streaming tasks don't interleave.
|
|
652
852
|
runChainRef.current = runChainRef.current
|
|
653
853
|
.then(() => executeLine(value))
|
|
654
|
-
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}
|
|
854
|
+
.catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
|
|
655
855
|
}, [draft, executeLine, appendLogText]);
|
|
656
856
|
|
|
657
857
|
useEffect(() => {
|
|
@@ -680,18 +880,107 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
680
880
|
|
|
681
881
|
const statusText = useMemoStatusText(React, status, spinnerTick, getBackgroundSuffix());
|
|
682
882
|
|
|
683
|
-
// Top-level
|
|
684
|
-
//
|
|
883
|
+
// Top-level catches Ctrl+C / Ctrl+O, plus completion popup navigation
|
|
884
|
+
// while a slash/agent menu is open.
|
|
685
885
|
useInput((input, key) => {
|
|
686
886
|
if (key.ctrl && input === "c") { exit(); return; }
|
|
687
887
|
if (key.ctrl && input === "o") { expandLastMerge(); return; }
|
|
888
|
+
if (!completionsOpen) return;
|
|
889
|
+
if (key.upArrow) {
|
|
890
|
+
setCompletionIndex((i) => {
|
|
891
|
+
const next = (i - 1 + completions.length) % completions.length;
|
|
892
|
+
setCompletionWindowStart((ws) => {
|
|
893
|
+
if (next < ws) return next;
|
|
894
|
+
if (next === completions.length - 1) {
|
|
895
|
+
return Math.max(0, completions.length - POPUP_PAGE_SIZE);
|
|
896
|
+
}
|
|
897
|
+
return ws;
|
|
898
|
+
});
|
|
899
|
+
return next;
|
|
900
|
+
});
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (key.downArrow) {
|
|
904
|
+
setCompletionIndex((i) => {
|
|
905
|
+
const next = (i + 1) % completions.length;
|
|
906
|
+
setCompletionWindowStart((ws) => {
|
|
907
|
+
if (next === 0) return 0;
|
|
908
|
+
if (next >= ws + POPUP_PAGE_SIZE) return next - POPUP_PAGE_SIZE + 1;
|
|
909
|
+
return ws;
|
|
910
|
+
});
|
|
911
|
+
return next;
|
|
912
|
+
});
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (key.return) {
|
|
916
|
+
// Leaf completions (e.g. /resume <session>) run immediately on Enter.
|
|
917
|
+
// Parents with children only fill the draft so the next menu can open.
|
|
918
|
+
const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
|
|
919
|
+
if (item && !item.hasChildren) {
|
|
920
|
+
const cmd = String(item.replace || "").trim();
|
|
921
|
+
setCompletionIndex(0);
|
|
922
|
+
setCompletionSuppressedDraft(null);
|
|
923
|
+
if (cmd) submit(cmd);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
acceptCompletion();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (key.tab) {
|
|
930
|
+
acceptCompletion();
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (key.escape) {
|
|
934
|
+
setCompletionSuppressedDraft(null);
|
|
935
|
+
setDraft("");
|
|
936
|
+
setDraftVersion((v) => v + 1);
|
|
937
|
+
}
|
|
688
938
|
}, { isActive: interactive });
|
|
689
939
|
|
|
690
940
|
return h(Box, { flexDirection: "column", width: "100%" },
|
|
691
941
|
h(Box, { flexDirection: "column", width: "100%" },
|
|
692
|
-
...
|
|
693
|
-
|
|
694
|
-
|
|
942
|
+
...(() => {
|
|
943
|
+
// Re-render raw markdown at paint time so leftover ** / ### from
|
|
944
|
+
// older append paths or nested `**code**` patterns still resolve.
|
|
945
|
+
const mdState = { inCodeBlock: false };
|
|
946
|
+
return logLines.map((item, idx) => {
|
|
947
|
+
let text = item.text || " ";
|
|
948
|
+
if (MARKDOWN_LOG_KINDS.has(item.kind) && /(?:\*\*|__|^\s*#{1,6}\s|^\s*`{3})/m.test(text)) {
|
|
949
|
+
try {
|
|
950
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(text, mdState);
|
|
951
|
+
if (Array.isArray(rendered) && rendered.length > 0) {
|
|
952
|
+
text = rendered.length === 1 ? rendered[0] : rendered.join("\n");
|
|
953
|
+
}
|
|
954
|
+
} catch {
|
|
955
|
+
// keep original
|
|
956
|
+
}
|
|
957
|
+
} else if (MARKDOWN_LOG_KINDS.has(item.kind) && mdState.inCodeBlock) {
|
|
958
|
+
try {
|
|
959
|
+
const rendered = fmt.renderLogLinesWithMarkdownAnsi(text, mdState);
|
|
960
|
+
if (Array.isArray(rendered) && rendered[0] != null) text = rendered[0];
|
|
961
|
+
} catch {
|
|
962
|
+
// keep original
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const textEl = h(Text, { ...resolveLogLineTextProps(item.kind) }, text || " ");
|
|
966
|
+
// Give user turns a blank line above/below so › prompts don't
|
|
967
|
+
// sit flush against system/tool rows. Multi-line user blocks
|
|
968
|
+
// only pad the outer edges.
|
|
969
|
+
if (item.kind === "user") {
|
|
970
|
+
const prev = logLines[idx - 1];
|
|
971
|
+
const next = logLines[idx + 1];
|
|
972
|
+
const marginTop = !prev || prev.kind !== "user" ? 1 : 0;
|
|
973
|
+
const marginBottom = !next || next.kind !== "user" ? 1 : 0;
|
|
974
|
+
return h(Box, {
|
|
975
|
+
key: item.id,
|
|
976
|
+
width: "100%",
|
|
977
|
+
marginTop,
|
|
978
|
+
marginBottom,
|
|
979
|
+
}, textEl);
|
|
980
|
+
}
|
|
981
|
+
return h(Text, { key: item.id, ...resolveLogLineTextProps(item.kind) }, text || " ");
|
|
982
|
+
});
|
|
983
|
+
})()
|
|
695
984
|
),
|
|
696
985
|
activeMerge ? h(Box, null,
|
|
697
986
|
h(Text, { color: activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
|
|
@@ -703,20 +992,62 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
703
992
|
h(Box, { flexGrow: 1 }),
|
|
704
993
|
h(Text, { color: "gray" }, `v${fmt.UCODE_VERSION}`),
|
|
705
994
|
),
|
|
995
|
+
completionsOpen ? (() => {
|
|
996
|
+
const start = Math.min(completionWindowStart, Math.max(0, completions.length - POPUP_PAGE_SIZE));
|
|
997
|
+
const end = Math.min(completions.length, start + POPUP_PAGE_SIZE);
|
|
998
|
+
const visible = completions.slice(start, end);
|
|
999
|
+
const cols = Math.max(8, size.cols || 80);
|
|
1000
|
+
// Frame the popup with a top rule; MultilineInput's borderTop is the
|
|
1001
|
+
// matching bottom rule, so we intentionally omit a trailing ─ here.
|
|
1002
|
+
return h(Box, { flexDirection: "column", width: "100%" },
|
|
1003
|
+
h(Text, { color: "gray" }, "─".repeat(cols)),
|
|
1004
|
+
...visible.map((s, idxInWindow) => {
|
|
1005
|
+
const idx = start + idxInWindow;
|
|
1006
|
+
const selected = idx === completionIndex;
|
|
1007
|
+
// Keep label+description in one Text. Splitting into sibling
|
|
1008
|
+
// Text nodes with wrap:"truncate" lets Yoga shrink the label
|
|
1009
|
+
// and mid-cut commands (e.g. "/help" → "/he p").
|
|
1010
|
+
const line = s.description
|
|
1011
|
+
? `${s.label} ${s.description}`
|
|
1012
|
+
: String(s.label || "");
|
|
1013
|
+
return h(Box, { key: `cmp-${idx}`, width: "100%" },
|
|
1014
|
+
h(Text, {
|
|
1015
|
+
color: selected ? "cyan" : "gray",
|
|
1016
|
+
inverse: selected,
|
|
1017
|
+
wrap: "truncate",
|
|
1018
|
+
}, line),
|
|
1019
|
+
);
|
|
1020
|
+
}),
|
|
1021
|
+
);
|
|
1022
|
+
})() : null,
|
|
706
1023
|
h(Box, { width: "100%" },
|
|
707
1024
|
h(MultilineInput, {
|
|
708
1025
|
value: draft,
|
|
709
1026
|
valueVersion: draftVersion,
|
|
710
|
-
onChange: (next) =>
|
|
711
|
-
|
|
1027
|
+
onChange: (next) => {
|
|
1028
|
+
if (completionSuppressedDraft !== null && next !== completionSuppressedDraft) {
|
|
1029
|
+
setCompletionSuppressedDraft(null);
|
|
1030
|
+
}
|
|
1031
|
+
setDraft(next);
|
|
1032
|
+
},
|
|
1033
|
+
onSubmit: (value) => {
|
|
1034
|
+
setCompletionSuppressedDraft(null);
|
|
1035
|
+
submit(value);
|
|
1036
|
+
},
|
|
712
1037
|
onCancel: () => {
|
|
1038
|
+
if (completionsOpen) {
|
|
1039
|
+
setCompletionSuppressedDraft(null);
|
|
1040
|
+
setDraft("");
|
|
1041
|
+
setDraftVersion((v) => v + 1);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
713
1044
|
// If a task is in flight, Esc requests cancellation. Otherwise
|
|
714
1045
|
// it clears the agent selection (matches blessed). The text
|
|
715
1046
|
// value is left alone so the user doesn't lose what they typed.
|
|
716
1047
|
const pending = pendingTaskRef.current;
|
|
717
1048
|
if (pending && pending.abortController && !pending.abortController.signal.aborted) {
|
|
718
1049
|
try { pending.abortController.abort(); } catch { /* ignore */ }
|
|
719
|
-
appendLogLine("⚙ Cancellation requested. Stopping the current task...");
|
|
1050
|
+
appendLogLine("⚙ Cancellation requested. Stopping the current task...", "system");
|
|
720
1051
|
setStatus({
|
|
721
1052
|
message: "Cancelling...",
|
|
722
1053
|
type: "waiting",
|
|
@@ -736,11 +1067,12 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
736
1067
|
onArrowRightAtEmpty: () => onArrowSideAtEmpty("right"),
|
|
737
1068
|
width: Math.max(20, (size.cols || 80) - 4),
|
|
738
1069
|
interactive,
|
|
1070
|
+
interceptArrowsAndEnter: completionsOpen,
|
|
739
1071
|
placeholder: "",
|
|
740
1072
|
promptPrefix: targetAgent ? `›@${getAgentLabel(targetAgent)} ` : "› ",
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
1073
|
+
// Completions render ABOVE the input. Only the Agents footer is
|
|
1074
|
+
// below — counting popup rows here parks the hardware cursor up
|
|
1075
|
+
// into the menu (ghost block on /status etc.).
|
|
744
1076
|
linesBelowInput: 1,
|
|
745
1077
|
// During model/tool activity ucode redraws the status line every
|
|
746
1078
|
// spinner frame. Keeping the hardware cursor hidden avoids a
|
|
@@ -813,7 +1145,7 @@ function runUcodeInkTui(props = {}) {
|
|
|
813
1145
|
});
|
|
814
1146
|
}
|
|
815
1147
|
|
|
816
|
-
module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText };
|
|
1148
|
+
module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText, collapseThinkingTail, resolveLogLineTextProps };
|
|
817
1149
|
|
|
818
1150
|
function inferStatusType(text = "", requestedType = "") {
|
|
819
1151
|
const type = String(requestedType || "").trim().toLowerCase();
|
|
@@ -837,6 +1169,27 @@ function inferStatusType(text = "", requestedType = "") {
|
|
|
837
1169
|
* combination while a task is in flight, mirroring updateStatus() in the
|
|
838
1170
|
* blessed implementation.
|
|
839
1171
|
*/
|
|
1172
|
+
function collapseThinkingTail(text, maxChars = 80) {
|
|
1173
|
+
const collapsed = String(text || "").replace(/\s+/g, " ").trim();
|
|
1174
|
+
const parsed = Number(maxChars);
|
|
1175
|
+
const limit = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 80;
|
|
1176
|
+
if (!collapsed) return "";
|
|
1177
|
+
|
|
1178
|
+
// Prefer the latest markdown emphasis / section so the status line shows the
|
|
1179
|
+
// current thought instead of a mid-word tail of an earlier heading.
|
|
1180
|
+
let candidate = collapsed;
|
|
1181
|
+
const boldParts = collapsed.match(/\*\*[^*]+\*\*/g);
|
|
1182
|
+
if (boldParts && boldParts.length > 0) {
|
|
1183
|
+
candidate = boldParts[boldParts.length - 1].replace(/\*/g, "").trim() || candidate;
|
|
1184
|
+
} else {
|
|
1185
|
+
const clauses = collapsed.split(/(?<=[.!?。!?])\s+/).map((part) => part.trim()).filter(Boolean);
|
|
1186
|
+
if (clauses.length > 1) candidate = clauses[clauses.length - 1];
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (candidate.length <= limit) return candidate;
|
|
1190
|
+
return `…${candidate.slice(-(limit - 1))}`;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
840
1193
|
function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
|
|
841
1194
|
const message = String((status && status.message) || "");
|
|
842
1195
|
const suffix = String(backgroundSuffix || "");
|