@oxecli/oxe 1.0.85 → 1.0.87
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/dist/cli.js +35 -18
- package/dist/engine.js +20 -63
- package/dist/ui.js +167 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { loadOrPrompt, default_reasoning_effort, max_action_chars, max_resume_history_items, runtimeOsSummary, max_context_tokens, context_overhead_margin, } from "./config.js";
|
|
5
5
|
import { InferenceEngine, estimateTokens } from "./engine.js";
|
|
6
6
|
import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
|
|
7
|
-
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
|
|
7
|
+
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, plainLen, truncateStyled, terminalWidth, TOOL_RESULT_MAX_LINES, MAX_COMMAND_DISPLAY_CHARS, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, dockAppendContent, dockSetInactive, dockTearDown, } from "./ui.js";
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const packageInfo = require("../package.json");
|
|
@@ -49,14 +49,32 @@ export class CLI {
|
|
|
49
49
|
const label = started && String(started).trim();
|
|
50
50
|
const body = String(text).trim();
|
|
51
51
|
if (label && /^[⎿╰]/.test(stripAnsi(body))) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// The result body was formatted against the terminal width at tool time,
|
|
53
|
+
// but the window may be narrower on resume — re-truncate defensively so
|
|
54
|
+
// long persisted lines never run off the screen.
|
|
55
|
+
const lineMax = Math.max(terminalWidth() - 4, 20);
|
|
56
|
+
const allLines = body.split("\n");
|
|
57
|
+
const lines = allLines
|
|
58
|
+
.slice(0, TOOL_RESULT_MAX_LINES)
|
|
59
|
+
.map((ln) => plainLen(ln) > lineMax ? truncateStyled(ln, lineMax) + "…\x1b[0m" : ln);
|
|
60
|
+
if (allLines.length > TOOL_RESULT_MAX_LINES) {
|
|
61
|
+
lines.push("\x1b[90m…\x1b[0m");
|
|
62
|
+
}
|
|
63
|
+
const labelLine = plainLen(label) > lineMax ? truncateStyled(label, lineMax) + "…\x1b[0m" : label;
|
|
64
|
+
process.stdout.write(`${labelLine}\n${lines.join("\n")}\n`);
|
|
65
|
+
if (diff) {
|
|
66
|
+
const diffLines = diff.split("\n").map((ln) => plainLen(ln) > lineMax ? truncateStyled(ln, lineMax) + "…\x1b[0m" : ln);
|
|
67
|
+
process.stdout.write(diffLines.join("\n") + "\n");
|
|
68
|
+
}
|
|
55
69
|
return;
|
|
56
70
|
}
|
|
57
71
|
const icon = status === "failed" ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
|
|
58
72
|
const style = status === "failed" ? "\x1b[31m" : "\x1b[32m";
|
|
59
|
-
|
|
73
|
+
const truncated = truncateEllipsis(body, Math.min(max_action_chars, MAX_COMMAND_DISPLAY_CHARS), "text");
|
|
74
|
+
// truncateEllipsis appends a "…(text truncated: N chars total)" suffix, so
|
|
75
|
+
// re-truncate to the current width to keep the legacy action on one row.
|
|
76
|
+
const displayMax = Math.max(terminalWidth() - 6, 20);
|
|
77
|
+
process.stdout.write(`${icon} ${style}${truncateStyled(truncated, displayMax)}\x1b[0m\n`);
|
|
60
78
|
}
|
|
61
79
|
printAssistantBlock(text) {
|
|
62
80
|
const rendered = aiMarkdown(text);
|
|
@@ -73,6 +91,7 @@ export class CLI {
|
|
|
73
91
|
process.stdout.write("\n");
|
|
74
92
|
}
|
|
75
93
|
async showHelp() {
|
|
94
|
+
dockTearDown();
|
|
76
95
|
const rows = COMMAND_HELP.map(([cmd, desc]) => {
|
|
77
96
|
const styled = cmd.replace(/(<[^>]+>)/g, "\x1b[36m$1\x1b[0m");
|
|
78
97
|
return {
|
|
@@ -86,7 +105,7 @@ export class CLI {
|
|
|
86
105
|
titleAlign: "left",
|
|
87
106
|
colGap: 3,
|
|
88
107
|
});
|
|
89
|
-
process.stdout.write(
|
|
108
|
+
process.stdout.write(panel + "\n");
|
|
90
109
|
if (process.stdin.isTTY) {
|
|
91
110
|
enableRawStdin();
|
|
92
111
|
try {
|
|
@@ -99,7 +118,7 @@ export class CLI {
|
|
|
99
118
|
disableRawStdin();
|
|
100
119
|
}
|
|
101
120
|
const n = panel.split("\n").length;
|
|
102
|
-
process.stdout.write(`\x1b[${n
|
|
121
|
+
process.stdout.write(`\x1b[${n}A\r\x1b[J`);
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
async awaitRawCloseKey() {
|
|
@@ -291,7 +310,6 @@ export class CLI {
|
|
|
291
310
|
async pickConversation() {
|
|
292
311
|
const recs = listSessions();
|
|
293
312
|
if (!recs.length) {
|
|
294
|
-
process.stdout.write("\n");
|
|
295
313
|
renderPanel("No saved conversations yet. Type a prompt to start one.", "Conversations");
|
|
296
314
|
return null;
|
|
297
315
|
}
|
|
@@ -300,10 +318,11 @@ export class CLI {
|
|
|
300
318
|
return null;
|
|
301
319
|
}
|
|
302
320
|
enableRawStdin();
|
|
321
|
+
dockTearDown();
|
|
303
322
|
let selected = 0;
|
|
304
323
|
let lastRows = 0;
|
|
305
324
|
const draw = () => {
|
|
306
|
-
const frame =
|
|
325
|
+
const frame = this.renderPickerPanel(recs, selected);
|
|
307
326
|
const n = frame.split("\n").length;
|
|
308
327
|
if (lastRows > 0)
|
|
309
328
|
process.stdout.write(`\x1b[${lastRows - 1}A\r\x1b[J`);
|
|
@@ -341,13 +360,11 @@ export class CLI {
|
|
|
341
360
|
async resumeConversation(engine, num) {
|
|
342
361
|
const sid = parseInt(num, 10);
|
|
343
362
|
if (Number.isNaN(sid)) {
|
|
344
|
-
process.stdout.write("\n");
|
|
345
363
|
renderPanel(`Invalid conversation number: ${num}`, "Error", "", true, "31");
|
|
346
364
|
return;
|
|
347
365
|
}
|
|
348
366
|
const rec = loadSession(sid);
|
|
349
367
|
if (!rec) {
|
|
350
|
-
process.stdout.write("\n");
|
|
351
368
|
renderPanel(`Conversation ${sid} not found.`, "Error", "", true, "31");
|
|
352
369
|
return;
|
|
353
370
|
}
|
|
@@ -358,10 +375,11 @@ export class CLI {
|
|
|
358
375
|
this.sessionId = sid;
|
|
359
376
|
engine.reasoningEffort = String(rec["reasoning_effort"] ?? default_reasoning_effort);
|
|
360
377
|
clearScreen();
|
|
378
|
+
dockSetInactive();
|
|
361
379
|
this.renderHeader();
|
|
362
380
|
process.stdout.write("\n");
|
|
363
381
|
process.stdout.write(`\x1b[90mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
|
|
364
|
-
process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n
|
|
382
|
+
process.stdout.write(`\x1b[90m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n`);
|
|
365
383
|
renderPanel("Conversation history restored", "Restored");
|
|
366
384
|
process.stdout.write("\n");
|
|
367
385
|
if (this.story.length)
|
|
@@ -403,6 +421,7 @@ export class CLI {
|
|
|
403
421
|
this.story = [];
|
|
404
422
|
this.sessionId = null;
|
|
405
423
|
clearScreen();
|
|
424
|
+
dockSetInactive();
|
|
406
425
|
this.renderHeader();
|
|
407
426
|
continue;
|
|
408
427
|
}
|
|
@@ -424,15 +443,14 @@ export class CLI {
|
|
|
424
443
|
const effort = parts[1]?.toLowerCase() ?? "";
|
|
425
444
|
if (["none", "low", "high"].includes(effort)) {
|
|
426
445
|
engine.reasoningEffort = effort;
|
|
427
|
-
|
|
446
|
+
dockAppendContent(`\x1b[32m✓\x1b[0m \x1b[90mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[90m.\x1b[0m\n`);
|
|
428
447
|
}
|
|
429
448
|
else {
|
|
430
|
-
process.stdout.write("\n");
|
|
431
449
|
renderPanel("Usage: /effort none|low|high", "Error", "", true, "31");
|
|
432
450
|
}
|
|
433
451
|
continue;
|
|
434
452
|
}
|
|
435
|
-
|
|
453
|
+
dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
|
|
436
454
|
queryStarted = true;
|
|
437
455
|
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
438
456
|
queryStarted = false;
|
|
@@ -448,7 +466,7 @@ export class CLI {
|
|
|
448
466
|
if (err?.message === "interrupt") {
|
|
449
467
|
const wasActive = queryStarted || engine.inQuery;
|
|
450
468
|
if (wasActive && !engine.queryHasOutput && !engine.queryCalledTool) {
|
|
451
|
-
|
|
469
|
+
dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
|
|
452
470
|
}
|
|
453
471
|
engine.inQuery = false;
|
|
454
472
|
stripOrphanCalls(this.inputItems);
|
|
@@ -470,8 +488,7 @@ export class CLI {
|
|
|
470
488
|
break;
|
|
471
489
|
}
|
|
472
490
|
this.interruptPending = true;
|
|
473
|
-
|
|
474
|
-
process.stdout.write("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
491
|
+
dockAppendContent("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
|
|
475
492
|
continue;
|
|
476
493
|
}
|
|
477
494
|
throw err;
|
package/dist/engine.js
CHANGED
|
@@ -2,7 +2,7 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { max_output_tokens, max_empty_retries, max_agent_steps, max_context_tokens, context_overhead_margin, compact_keep_recent_turns, max_summary_source_chars, max_read_file_stored_chars, } from "./config.js";
|
|
3
3
|
import { getSystemPrompt } from "./system.js";
|
|
4
4
|
import { buildTools, truncateToolOutput, toolReadFile, toolWriteFile, toolEditFile, toolBash, toolGlob, toolGrep, toolLoadSkill, } from "./tools.js";
|
|
5
|
-
import { mutedMarkdown, aiMarkdown, tickDuration,
|
|
5
|
+
import { mutedMarkdown, aiMarkdown, tickDuration, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, Spinner, hideCursor, dockAppendContent, dockReplaceTransient, dockTransientStart, } from "./ui.js";
|
|
6
6
|
import { reportUsage } from "./api.js";
|
|
7
7
|
import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
|
|
8
8
|
import { takePendingDiffOutput } from "./tools.js";
|
|
@@ -43,8 +43,6 @@ export class InferenceEngine {
|
|
|
43
43
|
reasoningEffort;
|
|
44
44
|
keyData;
|
|
45
45
|
client;
|
|
46
|
-
workActive = false;
|
|
47
|
-
workRows = 0;
|
|
48
46
|
inQuery = false;
|
|
49
47
|
activeAbort = null;
|
|
50
48
|
interrupted = false;
|
|
@@ -146,22 +144,6 @@ export class InferenceEngine {
|
|
|
146
144
|
incompleteReason(response) {
|
|
147
145
|
return response?.incomplete_details?.reason ?? null;
|
|
148
146
|
}
|
|
149
|
-
overwriteWorkLine(text) {
|
|
150
|
-
for (let i = 0; i < this.workRows; i++) {
|
|
151
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
152
|
-
}
|
|
153
|
-
process.stdout.write(mutedMarkdown(text) + "\n\n");
|
|
154
|
-
this.workRows = displayRows(mutedMarkdown(text)) + 1;
|
|
155
|
-
}
|
|
156
|
-
clearWorkLine() {
|
|
157
|
-
if (!this.workActive || this.workRows < 1)
|
|
158
|
-
return;
|
|
159
|
-
for (let i = 0; i < this.workRows; i++) {
|
|
160
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
161
|
-
}
|
|
162
|
-
this.workActive = false;
|
|
163
|
-
this.workRows = 0;
|
|
164
|
-
}
|
|
165
147
|
async streamOnce(inputItems, stats, story, previousResponseId) {
|
|
166
148
|
let content = "";
|
|
167
149
|
let committedLen = 0;
|
|
@@ -169,6 +151,7 @@ export class InferenceEngine {
|
|
|
169
151
|
let sawReasoning = false;
|
|
170
152
|
let status = null;
|
|
171
153
|
let thinkingStart = null;
|
|
154
|
+
dockTransientStart();
|
|
172
155
|
const workStatus = new Spinner();
|
|
173
156
|
const workingStarted = Date.now();
|
|
174
157
|
workStatus.startWithRender(() => `Working ${tickDuration((Date.now() - workingStarted) / 1000)}`);
|
|
@@ -187,14 +170,7 @@ export class InferenceEngine {
|
|
|
187
170
|
const elapsed = (Date.now() - workingStarted) / 1000;
|
|
188
171
|
const t = tickDuration(elapsed);
|
|
189
172
|
story.push({ type: "worked", text: `Worked for ${t}` });
|
|
190
|
-
|
|
191
|
-
this.overwriteWorkLine(`Worked for ${t}`);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
process.stdout.write(mutedMarkdown(`Worked for ${t}`) + "\n\n");
|
|
195
|
-
this.workActive = true;
|
|
196
|
-
this.workRows = displayRows(mutedMarkdown(`Worked for ${t}`)) + 1;
|
|
197
|
-
}
|
|
173
|
+
dockReplaceTransient(mutedMarkdown(`Worked for ${t}`) + "\n");
|
|
198
174
|
};
|
|
199
175
|
const finishThinking = (report) => {
|
|
200
176
|
if (thinkingStart === null)
|
|
@@ -208,8 +184,7 @@ export class InferenceEngine {
|
|
|
208
184
|
if (report) {
|
|
209
185
|
const t = tickDuration(elapsed);
|
|
210
186
|
story.push({ type: "thought", text: `Thought for ${t}` });
|
|
211
|
-
|
|
212
|
-
this.workActive = false;
|
|
187
|
+
dockReplaceTransient(mutedMarkdown(`Thought for ${t}`) + "\n");
|
|
213
188
|
}
|
|
214
189
|
thinkingStart = null;
|
|
215
190
|
};
|
|
@@ -248,6 +223,7 @@ export class InferenceEngine {
|
|
|
248
223
|
etype === "response.reasoning.summary.delta") {
|
|
249
224
|
sawReasoning = true;
|
|
250
225
|
silenceWorking();
|
|
226
|
+
dockTransientStart();
|
|
251
227
|
if (thinkingStart === null) {
|
|
252
228
|
thinkingStart = Date.now();
|
|
253
229
|
status = new Spinner();
|
|
@@ -288,9 +264,7 @@ export class InferenceEngine {
|
|
|
288
264
|
workStatus.stop();
|
|
289
265
|
const stillThinking = thinkingStart !== null;
|
|
290
266
|
finishThinking(stillThinking);
|
|
291
|
-
if (this.interrupted
|
|
292
|
-
this.clearWorkLine();
|
|
293
|
-
else
|
|
267
|
+
if (!this.interrupted || this.queryHasOutput)
|
|
294
268
|
reportWorking();
|
|
295
269
|
if (content.slice(committedLen))
|
|
296
270
|
printAiChunk(content.slice(committedLen));
|
|
@@ -398,6 +372,7 @@ export class InferenceEngine {
|
|
|
398
372
|
if (!this.contextOverBudget(conversation))
|
|
399
373
|
return [conversation, false];
|
|
400
374
|
const started = Date.now();
|
|
375
|
+
dockTransientStart();
|
|
401
376
|
const comp = new Spinner();
|
|
402
377
|
comp.start(`Compacting conversation ${tickDuration(0)}`);
|
|
403
378
|
const compTimer = setInterval(() => {
|
|
@@ -416,14 +391,11 @@ export class InferenceEngine {
|
|
|
416
391
|
persistCompactionSummary(inputItems, compacted[0]);
|
|
417
392
|
const text = `Compacted conversation in ${tickDuration((Date.now() - started) / 1000)}`;
|
|
418
393
|
story.push({ type: "compacted", text });
|
|
419
|
-
|
|
420
|
-
process.stdout.write(mutedMarkdown(text) + "\n\n");
|
|
394
|
+
dockReplaceTransient(mutedMarkdown(text) + "\n");
|
|
421
395
|
return [compacted, true];
|
|
422
396
|
}
|
|
423
397
|
async executeQuery(userPrompt, inputItems, story, pasteSpans) {
|
|
424
398
|
this.inQuery = true;
|
|
425
|
-
this.workActive = false;
|
|
426
|
-
this.workRows = 0;
|
|
427
399
|
this.interrupted = false;
|
|
428
400
|
this.queryHasOutput = false;
|
|
429
401
|
this.queryCalledTool = false;
|
|
@@ -553,36 +525,24 @@ export class InferenceEngine {
|
|
|
553
525
|
continue;
|
|
554
526
|
}
|
|
555
527
|
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
this.workActive = false;
|
|
559
|
-
this.workRows = 0;
|
|
528
|
+
dockAppendContent(aiMarkdown(emptyMessage) + "\n");
|
|
529
|
+
dockAppendContent(summary() + "\n");
|
|
560
530
|
return;
|
|
561
531
|
}
|
|
562
532
|
if (!calls.length) {
|
|
563
|
-
// Exactly one blank row between the message and the footer summary.
|
|
564
|
-
const trailing = (text.match(/\n+$/) ?? [""])[0].length;
|
|
565
|
-
process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)) + summary() + "\n");
|
|
566
533
|
story.push({ type: "footer", text: footerText() });
|
|
567
534
|
attachFooter(conversation);
|
|
568
535
|
attachFooter(inputItems);
|
|
569
|
-
|
|
570
|
-
this.workRows = 0;
|
|
536
|
+
dockAppendContent(summary() + "\n");
|
|
571
537
|
return;
|
|
572
538
|
}
|
|
573
|
-
if (text.trim()) {
|
|
574
|
-
// Ensure exactly one blank row between the assistant message and the
|
|
575
|
-
// first tool call, no matter how many newlines the message ended with.
|
|
576
|
-
const trailing = (text.match(/\n+$/) ?? [""])[0].length;
|
|
577
|
-
process.stdout.write("\n".repeat(Math.max(0, 2 - trailing)));
|
|
578
|
-
}
|
|
579
539
|
for (const c of calls) {
|
|
580
|
-
this.workActive = false;
|
|
581
540
|
this.queryCalledTool = true;
|
|
582
541
|
const started = toolCallLabel(c.name, c.arguments);
|
|
583
542
|
// Print the tool label the instant the command starts, then show a
|
|
584
543
|
// "╰─ Working..." dots line that swaps to the real result in place.
|
|
585
|
-
|
|
544
|
+
dockAppendContent(`${started}\n`);
|
|
545
|
+
dockTransientStart();
|
|
586
546
|
const toolSpinner = new Spinner();
|
|
587
547
|
toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
|
|
588
548
|
const rawResult = await this.runTool(c.name, c.arguments);
|
|
@@ -602,12 +562,13 @@ export class InferenceEngine {
|
|
|
602
562
|
});
|
|
603
563
|
// Swap the "╰─ Working..." line for the real result. If the tool
|
|
604
564
|
// produced a diff it renders directly below the action line with no
|
|
605
|
-
// gap; otherwise
|
|
606
|
-
toolSpinner.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
565
|
+
// gap; otherwise the dock gap separates the action from what follows.
|
|
566
|
+
toolSpinner.stop();
|
|
567
|
+
dockReplaceTransient(action + "\n");
|
|
568
|
+
if (diff) {
|
|
569
|
+
dockTransientStart();
|
|
570
|
+
dockReplaceTransient(diff + "\n");
|
|
571
|
+
}
|
|
611
572
|
const truncatedResult = c.name === "read_file"
|
|
612
573
|
? truncateToolOutput(rawResult, max_read_file_stored_chars)
|
|
613
574
|
: truncateToolOutput(rawResult);
|
|
@@ -634,10 +595,6 @@ export class InferenceEngine {
|
|
|
634
595
|
stripOrphanCalls(inputItems);
|
|
635
596
|
}
|
|
636
597
|
catch (err) {
|
|
637
|
-
if (this.interrupted) {
|
|
638
|
-
this.workActive = false;
|
|
639
|
-
this.workRows = 0;
|
|
640
|
-
}
|
|
641
598
|
if (this.interrupted)
|
|
642
599
|
throw new Error("interrupt");
|
|
643
600
|
if (err?.message === "interrupt" || err?.message === "eof") {
|
package/dist/ui.js
CHANGED
|
@@ -411,7 +411,7 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
411
411
|
return out.join("\n");
|
|
412
412
|
}
|
|
413
413
|
export function renderPanel(content, title = "", subtitle = "", expand = true, borderStyle = "90", titleAlign = "center") {
|
|
414
|
-
|
|
414
|
+
dockAppendContent(panelString(content, title, subtitle, expand, borderStyle, titleAlign) + "\n");
|
|
415
415
|
}
|
|
416
416
|
// ---------------------------------------------------------------------------
|
|
417
417
|
// Table Panel Rendering
|
|
@@ -462,7 +462,7 @@ export function tickDuration(seconds) {
|
|
|
462
462
|
export function printAiChunk(chunk) {
|
|
463
463
|
if (!chunk)
|
|
464
464
|
return;
|
|
465
|
-
|
|
465
|
+
dockAppendContent(markdownToAnsi(chunk));
|
|
466
466
|
}
|
|
467
467
|
const FENCE_MARKER_RE = /`{3,}/g;
|
|
468
468
|
export function safeCommitPoint(text) {
|
|
@@ -497,6 +497,134 @@ export function displayRows(text) {
|
|
|
497
497
|
return rows;
|
|
498
498
|
}
|
|
499
499
|
// ---------------------------------------------------------------------------
|
|
500
|
+
// Persistent Prompt Dock
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// The prompt box stays on screen at all times (docked). The cursor is parked
|
|
503
|
+
// at the box's TOP row; incoming content is written there and the box is
|
|
504
|
+
// re-rendered below it, so the box is pushed down by content while exactly
|
|
505
|
+
// one blank row is preserved between the latest content and the box.
|
|
506
|
+
//
|
|
507
|
+
// dockRel: how many rows the cursor sits above boxTop. 0 = parked at boxTop,
|
|
508
|
+
// 1 = one row up (the gap row), where transient spinners render.
|
|
509
|
+
// dockGap: true when a blank row already exists above the current row (after
|
|
510
|
+
// the box is torn down), so askBottomPrompt does not add a second one.
|
|
511
|
+
let docked = false;
|
|
512
|
+
let dockRel = 0;
|
|
513
|
+
let dockBoxRows = 0;
|
|
514
|
+
let dockGap = false;
|
|
515
|
+
let dockFrameLines = [];
|
|
516
|
+
export function dockIsDocked() {
|
|
517
|
+
return docked;
|
|
518
|
+
}
|
|
519
|
+
export function dockHasGap() {
|
|
520
|
+
return dockGap;
|
|
521
|
+
}
|
|
522
|
+
export function dockSetFrame(frame, totalRows) {
|
|
523
|
+
dockFrameLines = frame.split("\n");
|
|
524
|
+
dockBoxRows = totalRows;
|
|
525
|
+
}
|
|
526
|
+
export function dockSetInactive() {
|
|
527
|
+
docked = false;
|
|
528
|
+
dockRel = 0;
|
|
529
|
+
dockBoxRows = 0;
|
|
530
|
+
dockGap = false;
|
|
531
|
+
dockFrameLines = [];
|
|
532
|
+
}
|
|
533
|
+
export function dockEnterDocked() {
|
|
534
|
+
docked = true;
|
|
535
|
+
dockRel = 0;
|
|
536
|
+
dockGap = false;
|
|
537
|
+
}
|
|
538
|
+
/** Erase the docked box and leave the cursor at boxTop with the gap above it,
|
|
539
|
+
* so a pager/replay can write there and a later prompt re-docks cleanly. */
|
|
540
|
+
export function dockTearDown() {
|
|
541
|
+
const wasDocked = docked;
|
|
542
|
+
dockEraseBox();
|
|
543
|
+
if (wasDocked) {
|
|
544
|
+
dockGap = true;
|
|
545
|
+
dockBoxRows = 0;
|
|
546
|
+
dockFrameLines = [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** Park the cursor on the gap row (above the box) for a transient spinner. */
|
|
550
|
+
export function dockTransientStart() {
|
|
551
|
+
if (!docked || dockBoxRows < 1)
|
|
552
|
+
return;
|
|
553
|
+
if (dockRel === 0) {
|
|
554
|
+
process.stdout.write("\x1b[1A\r");
|
|
555
|
+
dockRel = 1;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function dockEraseBox() {
|
|
559
|
+
if (!docked || dockBoxRows < 1)
|
|
560
|
+
return;
|
|
561
|
+
process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
|
|
562
|
+
for (let i = 0; i < dockBoxRows; i++) {
|
|
563
|
+
process.stdout.write("\r\x1b[K");
|
|
564
|
+
if (i < dockBoxRows - 1)
|
|
565
|
+
process.stdout.write("\x1b[1A");
|
|
566
|
+
}
|
|
567
|
+
docked = false;
|
|
568
|
+
dockRel = 0;
|
|
569
|
+
dockGap = true;
|
|
570
|
+
}
|
|
571
|
+
function dockEraseBoxKeep() {
|
|
572
|
+
if (!docked || dockBoxRows < 1)
|
|
573
|
+
return;
|
|
574
|
+
process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
|
|
575
|
+
for (let i = 0; i < dockBoxRows; i++) {
|
|
576
|
+
process.stdout.write("\r\x1b[K");
|
|
577
|
+
if (i < dockBoxRows - 1)
|
|
578
|
+
process.stdout.write("\x1b[1A");
|
|
579
|
+
}
|
|
580
|
+
process.stdout.write(`\x1b[${dockRel}A`);
|
|
581
|
+
docked = false;
|
|
582
|
+
dockRel = 0;
|
|
583
|
+
dockGap = true;
|
|
584
|
+
}
|
|
585
|
+
/** Write exactly one blank row then the box frame below the current row, and
|
|
586
|
+
* park the cursor at the box top. */
|
|
587
|
+
function dockRenderBox() {
|
|
588
|
+
if (dockBoxRows < 1)
|
|
589
|
+
return;
|
|
590
|
+
process.stdout.write("\n\n" + dockFrameLines.join("\n"));
|
|
591
|
+
process.stdout.write(`\x1b[${dockBoxRows - 1}A\r`);
|
|
592
|
+
dockRel = 0;
|
|
593
|
+
docked = true;
|
|
594
|
+
dockGap = false;
|
|
595
|
+
}
|
|
596
|
+
/** Commit content at the box top, pushing the box down, preserving exactly one
|
|
597
|
+
* blank row between the content and the box. */
|
|
598
|
+
export function dockAppendContent(text) {
|
|
599
|
+
if (!text)
|
|
600
|
+
return;
|
|
601
|
+
if (!docked) {
|
|
602
|
+
process.stdout.write(text);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
dockEraseBox();
|
|
606
|
+
process.stdout.write(text);
|
|
607
|
+
const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
|
|
608
|
+
if (tb > 0)
|
|
609
|
+
process.stdout.write(`\x1b[${tb}A`);
|
|
610
|
+
dockRenderBox();
|
|
611
|
+
}
|
|
612
|
+
/** Replace a transient spinner row (the gap row) with content, then re-dock. */
|
|
613
|
+
export function dockReplaceTransient(text) {
|
|
614
|
+
if (!text)
|
|
615
|
+
return;
|
|
616
|
+
if (!docked) {
|
|
617
|
+
process.stdout.write(text);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
dockEraseBoxKeep();
|
|
621
|
+
process.stdout.write(text);
|
|
622
|
+
const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
|
|
623
|
+
if (tb > 0)
|
|
624
|
+
process.stdout.write(`\x1b[${tb}A`);
|
|
625
|
+
dockRenderBox();
|
|
626
|
+
}
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
500
628
|
// Cursor & Spinner
|
|
501
629
|
// ---------------------------------------------------------------------------
|
|
502
630
|
let cursorHidden = false;
|
|
@@ -681,10 +809,13 @@ const TOOL_DISPLAY_NAMES = {
|
|
|
681
809
|
grep: "Grep",
|
|
682
810
|
load_skill: "Load",
|
|
683
811
|
};
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
812
|
+
export const TOOL_RESULT_MAX_LINES = 6;
|
|
813
|
+
/** Cap on the visible command/arg shown in a tool label, so the entry always
|
|
814
|
+
* fits on a single row regardless of terminal width. */
|
|
815
|
+
export const MAX_COMMAND_DISPLAY_CHARS = 100;
|
|
816
|
+
/** First line of a tool entry: `Bash(rm -f "…")` — cyan tool name, primary arg
|
|
817
|
+
* in parens. Newlines collapse to spaces and the arg is width-capped so the
|
|
818
|
+
* label never wraps past one row. */
|
|
688
819
|
export function toolCallLabel(name, argumentsJson) {
|
|
689
820
|
let args = {};
|
|
690
821
|
try {
|
|
@@ -713,11 +844,12 @@ export function toolCallLabel(name, argumentsJson) {
|
|
|
713
844
|
arg = String(args["skill_name"] ?? "");
|
|
714
845
|
break;
|
|
715
846
|
}
|
|
716
|
-
arg = arg.trim();
|
|
847
|
+
arg = arg.trim().replace(/\s*\r?\n\s*/g, " ");
|
|
717
848
|
if (!arg)
|
|
718
849
|
return `\x1b[1;36m${display}\x1b[0m`;
|
|
719
|
-
|
|
720
|
-
|
|
850
|
+
const cap = Math.max(Math.min(terminalWidth() - plainLen(display) - 4, MAX_COMMAND_DISPLAY_CHARS), 20);
|
|
851
|
+
if (plainLen(arg) > cap)
|
|
852
|
+
arg = truncateStyled(arg, cap) + "…";
|
|
721
853
|
return `\x1b[1;36m${display}\x1b[0m(${arg})`;
|
|
722
854
|
}
|
|
723
855
|
/**
|
|
@@ -737,17 +869,15 @@ export function formatToolResult(rawResult, failed) {
|
|
|
737
869
|
if (failed && (!clean || clean === "(no output)") && code) {
|
|
738
870
|
return `${corner}\x1b[31mexit code: ${code}\x1b[0m`;
|
|
739
871
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
872
|
+
// Truncate to the terminal width (rows OR per-line length, whichever trips
|
|
873
|
+
// first) so long tool output — e.g. Glob paths — never runs off the screen.
|
|
874
|
+
const termW = terminalWidth();
|
|
875
|
+
const lineMax = Math.max(termW - 4, 20);
|
|
876
|
+
const allLines = clean.split("\n");
|
|
877
|
+
const lines = allLines
|
|
746
878
|
.slice(0, TOOL_RESULT_MAX_LINES)
|
|
747
|
-
.map((ln) => ln.length >
|
|
748
|
-
|
|
749
|
-
: ln);
|
|
750
|
-
if (clipped || body.split("\n").length > TOOL_RESULT_MAX_LINES)
|
|
879
|
+
.map((ln) => (ln.length > lineMax ? ln.slice(0, lineMax) + "…" : ln));
|
|
880
|
+
if (allLines.length > TOOL_RESULT_MAX_LINES)
|
|
751
881
|
lines.push("…");
|
|
752
882
|
const styleOpen = failed ? "\x1b[31m" : "\x1b[90m";
|
|
753
883
|
const inner = lines
|
|
@@ -1058,7 +1188,15 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1058
1188
|
process.stdin.setRawMode(false);
|
|
1059
1189
|
if (pasteBurstTimer)
|
|
1060
1190
|
clearTimeout(pasteBurstTimer);
|
|
1061
|
-
|
|
1191
|
+
if (isErr) {
|
|
1192
|
+
clearBox();
|
|
1193
|
+
dockSetInactive();
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
process.stdout.write(`\x1b[${lastCursorRow}A\r`);
|
|
1197
|
+
dockSetFrame((prevFrameLines ?? []).join("\n"), lastTotalRows);
|
|
1198
|
+
dockEnterDocked();
|
|
1199
|
+
}
|
|
1062
1200
|
process.stdin.removeListener("keypress", onKeypress);
|
|
1063
1201
|
process.stdin.pause();
|
|
1064
1202
|
showCursor();
|
|
@@ -1071,7 +1209,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1071
1209
|
const { frame, cursorRow, cursorCol, totalRows } = renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints);
|
|
1072
1210
|
const newLines = frame.split("\n");
|
|
1073
1211
|
if (isFirst) {
|
|
1074
|
-
|
|
1212
|
+
if (dockIsDocked()) {
|
|
1213
|
+
dockEraseBox();
|
|
1214
|
+
}
|
|
1215
|
+
else if (!dockHasGap()) {
|
|
1216
|
+
process.stdout.write("\n");
|
|
1217
|
+
}
|
|
1075
1218
|
for (let i = 0; i < newLines.length; i++) {
|
|
1076
1219
|
process.stdout.write(newLines[i]);
|
|
1077
1220
|
if (i < newLines.length - 1)
|
|
@@ -1080,6 +1223,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1080
1223
|
prevFrameLines = newLines;
|
|
1081
1224
|
lastCursorRow = cursorRow;
|
|
1082
1225
|
lastTotalRows = totalRows;
|
|
1226
|
+
dockSetFrame(frame, totalRows);
|
|
1227
|
+
dockEnterDocked();
|
|
1083
1228
|
const up = totalRows - 1 - cursorRow;
|
|
1084
1229
|
process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
|
|
1085
1230
|
}
|
|
@@ -1103,6 +1248,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
|
|
|
1103
1248
|
prevFrameLines = newLines;
|
|
1104
1249
|
lastCursorRow = cursorRow;
|
|
1105
1250
|
lastTotalRows = totalRows;
|
|
1251
|
+
dockSetFrame(frame, totalRows);
|
|
1106
1252
|
}
|
|
1107
1253
|
};
|
|
1108
1254
|
const shiftSpansAfterInsert = (pos, delta) => {
|