@oxecli/oxe 1.0.52 → 1.0.54
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 +4 -3
- package/dist/engine.js +19 -7
- package/dist/tools.js +31 -10
- package/dist/ui.js +188 -49
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { loadOrPrompt, default_reasoning_effort, default_model, max_action_chars, max_resume_history_items, runtimeOsSummary, } from "./config.js";
|
|
5
5
|
import { InferenceEngine, estimateTokens } from "./engine.js";
|
|
6
|
-
import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, toolOutputFailed, } from "./sessions.js";
|
|
6
|
+
import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
|
|
7
7
|
import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, formatToolAction, truncateEllipsis, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
@@ -26,7 +26,7 @@ export class CLI {
|
|
|
26
26
|
}
|
|
27
27
|
renderHeader(model = default_model) {
|
|
28
28
|
const version = packageInfo.version ?? "unknown";
|
|
29
|
-
const cellWidth =
|
|
29
|
+
const cellWidth = 22;
|
|
30
30
|
const centeredCell = (value, render) => {
|
|
31
31
|
const left = Math.max(0, Math.floor((cellWidth - value.length) / 2));
|
|
32
32
|
const right = Math.max(0, cellWidth - value.length - left);
|
|
@@ -373,7 +373,7 @@ export class CLI {
|
|
|
373
373
|
this.renderHeader(engine.modelName);
|
|
374
374
|
process.stdout.write("\n");
|
|
375
375
|
process.stdout.write(`\x1b[2mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
|
|
376
|
-
process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens
|
|
376
|
+
process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens)\x1b[0m\n\n`);
|
|
377
377
|
renderPanel("Conversation history restored", "Restored");
|
|
378
378
|
process.stdout.write("\n");
|
|
379
379
|
if (this.story.length)
|
|
@@ -457,6 +457,7 @@ export class CLI {
|
|
|
457
457
|
process.stdout.write(aiMarkdown("What else can I help you with?") + "\n");
|
|
458
458
|
}
|
|
459
459
|
engine.inQuery = false;
|
|
460
|
+
stripOrphanCalls(this.inputItems);
|
|
460
461
|
if (this.inputItems.length &&
|
|
461
462
|
this.inputItems[this.inputItems.length - 1]["role"] === "user") {
|
|
462
463
|
this.inputItems.pop();
|
package/dist/engine.js
CHANGED
|
@@ -169,9 +169,9 @@ export class InferenceEngine {
|
|
|
169
169
|
let thinkingStart = null;
|
|
170
170
|
const workStatus = new Spinner();
|
|
171
171
|
const workingStarted = Date.now();
|
|
172
|
-
workStatus.start(`Working
|
|
172
|
+
workStatus.start(`Working ${tickDuration(0)}`);
|
|
173
173
|
const workTimer = setInterval(() => {
|
|
174
|
-
workStatus.update(`Working
|
|
174
|
+
workStatus.update(`Working ${tickDuration((Date.now() - workingStarted) / 1000)}`);
|
|
175
175
|
}, 500);
|
|
176
176
|
let workingReported = false;
|
|
177
177
|
let response = null;
|
|
@@ -254,7 +254,7 @@ export class InferenceEngine {
|
|
|
254
254
|
if (thinkingStart === null) {
|
|
255
255
|
thinkingStart = Date.now();
|
|
256
256
|
status = new Spinner();
|
|
257
|
-
status.start(
|
|
257
|
+
status.start(`Thinking ${tickDuration(0)}`);
|
|
258
258
|
}
|
|
259
259
|
else {
|
|
260
260
|
status?.update(`Thinking ${tickDuration((Date.now() - thinkingStart) / 1000)}`);
|
|
@@ -307,9 +307,11 @@ export class InferenceEngine {
|
|
|
307
307
|
}
|
|
308
308
|
const usage = response?.usage;
|
|
309
309
|
if (usage) {
|
|
310
|
-
|
|
311
|
-
const outTok = usage?.output_tokens ??
|
|
312
|
-
|
|
310
|
+
const inTok = usage?.input_tokens ?? 0;
|
|
311
|
+
const outTok = usage?.output_tokens ?? 0;
|
|
312
|
+
const totalTok = usage?.total_tokens ?? inTok + outTok;
|
|
313
|
+
stats["input"] = (stats["input"] ?? 0) + inTok;
|
|
314
|
+
stats["tokens"] = (stats["tokens"] ?? 0) + totalTok;
|
|
313
315
|
const reasoningTok = usage?.output_tokens_details?.reasoning_tokens ?? 0;
|
|
314
316
|
stats["reasoning"] = (stats["reasoning"] ?? 0) + reasoningTok;
|
|
315
317
|
}
|
|
@@ -447,7 +449,17 @@ export class InferenceEngine {
|
|
|
447
449
|
input: 0,
|
|
448
450
|
};
|
|
449
451
|
const queryStart = Date.now();
|
|
450
|
-
const footerText = () =>
|
|
452
|
+
const footerText = () => {
|
|
453
|
+
const parts = [];
|
|
454
|
+
if (stats["thinking"] > 0) {
|
|
455
|
+
parts.push(`Thought for ${tickDuration(stats["thinking"])}`);
|
|
456
|
+
}
|
|
457
|
+
parts.push(`Worked for ${tickDuration((Date.now() - queryStart) / 1000)}`);
|
|
458
|
+
if (stats["tokens"] > 0) {
|
|
459
|
+
parts.push(`Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[2m tokens`);
|
|
460
|
+
}
|
|
461
|
+
return `\x1b[2m(${parts.join(" · ")})\x1b[0m`;
|
|
462
|
+
};
|
|
451
463
|
const summary = () => mutedMarkdown(footerText());
|
|
452
464
|
const attachFooter = (items) => {
|
|
453
465
|
const footer = footerText();
|
package/dist/tools.js
CHANGED
|
@@ -6,6 +6,17 @@ import { max_diff_source_chars, max_diff_lines, max_diff_context_lines, max_diff
|
|
|
6
6
|
import { toolLoadSkill } from "./skills.js";
|
|
7
7
|
export { toolLoadSkill };
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
|
+
// Path sanitization helper
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function sanitizePath(p) {
|
|
12
|
+
if (!p)
|
|
13
|
+
return "";
|
|
14
|
+
return String(p).trim().replace(/^["']|["']$/g, "");
|
|
15
|
+
}
|
|
16
|
+
function escapeRegExp(string) {
|
|
17
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
9
20
|
// Natural sort + diff helpers
|
|
10
21
|
// ---------------------------------------------------------------------------
|
|
11
22
|
export function naturalSortKey(s) {
|
|
@@ -215,7 +226,9 @@ function streamReadRange(p, start, end) {
|
|
|
215
226
|
return [i, numbered];
|
|
216
227
|
}
|
|
217
228
|
export function toolReadFile(pathName, startLine = 1, endLine) {
|
|
218
|
-
const p = pathName;
|
|
229
|
+
const p = sanitizePath(pathName);
|
|
230
|
+
if (!p)
|
|
231
|
+
return "Error: path must not be empty";
|
|
219
232
|
if (!fs.existsSync(p))
|
|
220
233
|
return `Error: file not found: ${pathName}`;
|
|
221
234
|
if (!fs.statSync(p).isFile())
|
|
@@ -302,7 +315,9 @@ export function toolReadFile(pathName, startLine = 1, endLine) {
|
|
|
302
315
|
// write_file
|
|
303
316
|
// ---------------------------------------------------------------------------
|
|
304
317
|
export function toolWriteFile(pathName, content) {
|
|
305
|
-
const p = pathName;
|
|
318
|
+
const p = sanitizePath(pathName);
|
|
319
|
+
if (!p)
|
|
320
|
+
return "Error: path must not be empty";
|
|
306
321
|
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
307
322
|
return `Error: ${pathName} is a directory`;
|
|
308
323
|
}
|
|
@@ -378,7 +393,9 @@ function findWsBlocks(text, needle) {
|
|
|
378
393
|
return blocks;
|
|
379
394
|
}
|
|
380
395
|
export function toolEditFile(pathName, oldString, newString, replaceAll = false) {
|
|
381
|
-
const p = pathName;
|
|
396
|
+
const p = sanitizePath(pathName);
|
|
397
|
+
if (!p)
|
|
398
|
+
return "Error: path must not be empty";
|
|
382
399
|
if (!fs.existsSync(p))
|
|
383
400
|
return `Error: file not found: ${pathName}`;
|
|
384
401
|
if (!fs.statSync(p).isFile())
|
|
@@ -467,20 +484,24 @@ export function toolBash(command, timeout = 60, cwd) {
|
|
|
467
484
|
if (Number.isNaN(t))
|
|
468
485
|
t = 60;
|
|
469
486
|
t = t > 0 ? Math.min(t, max_bash_timeout_seconds) : 60;
|
|
487
|
+
const workingDir = cwd ? sanitizePath(cwd) : undefined;
|
|
488
|
+
if (workingDir && !fs.existsSync(workingDir)) {
|
|
489
|
+
return Promise.resolve(`Error: working directory not found: ${cwd}`);
|
|
490
|
+
}
|
|
470
491
|
return new Promise((resolve) => {
|
|
471
492
|
let child;
|
|
472
493
|
try {
|
|
473
494
|
if (process.platform === "win32") {
|
|
474
495
|
child = spawn(command, {
|
|
475
496
|
shell: true,
|
|
476
|
-
cwd:
|
|
477
|
-
windowsHide:
|
|
497
|
+
cwd: workingDir,
|
|
498
|
+
windowsHide: true,
|
|
478
499
|
});
|
|
479
500
|
}
|
|
480
501
|
else {
|
|
481
502
|
child = spawn(command, {
|
|
482
503
|
shell: true,
|
|
483
|
-
cwd:
|
|
504
|
+
cwd: workingDir,
|
|
484
505
|
detached: true,
|
|
485
506
|
});
|
|
486
507
|
}
|
|
@@ -630,7 +651,7 @@ function rglobPruned(base, pattern) {
|
|
|
630
651
|
return out;
|
|
631
652
|
}
|
|
632
653
|
export function toolGlob(pattern, pathName = ".", limit = 200) {
|
|
633
|
-
const base = pathName;
|
|
654
|
+
const base = sanitizePath(pathName) || ".";
|
|
634
655
|
if (!fs.existsSync(base))
|
|
635
656
|
return `Error: path not found: ${pathName}`;
|
|
636
657
|
const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
|
|
@@ -652,15 +673,15 @@ export function toolGlob(pattern, pathName = ".", limit = 200) {
|
|
|
652
673
|
return result;
|
|
653
674
|
}
|
|
654
675
|
export function toolGrep(pattern, pathName = ".", glob = "*", limit = 200) {
|
|
655
|
-
const base = pathName;
|
|
676
|
+
const base = sanitizePath(pathName) || ".";
|
|
656
677
|
if (!fs.existsSync(base))
|
|
657
678
|
return `Error: path not found: ${pathName}`;
|
|
658
679
|
let regex;
|
|
659
680
|
try {
|
|
660
681
|
regex = new RegExp(pattern, "i");
|
|
661
682
|
}
|
|
662
|
-
catch
|
|
663
|
-
|
|
683
|
+
catch {
|
|
684
|
+
regex = new RegExp(escapeRegExp(pattern), "i");
|
|
664
685
|
}
|
|
665
686
|
glob = glob || "*";
|
|
666
687
|
const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
|
package/dist/ui.js
CHANGED
|
@@ -9,8 +9,6 @@ export function enableRawStdin() {
|
|
|
9
9
|
readline.emitKeypressEvents(process.stdin);
|
|
10
10
|
if (process.stdin.isTTY) {
|
|
11
11
|
process.stdin.setRawMode(true);
|
|
12
|
-
// Enable the kitty keyboard protocol (CSI u) so modified Enter (Shift /
|
|
13
|
-
// Ctrl+Enter) arrives as \x1b[13;<mod>u rather than a plain \r.
|
|
14
12
|
process.stdout.write("\x1b[>1u");
|
|
15
13
|
}
|
|
16
14
|
process.stdin.resume();
|
|
@@ -136,11 +134,9 @@ const KEYWORDS = new Set([
|
|
|
136
134
|
function highlightCodeLine(line, _lang = "") {
|
|
137
135
|
if (!line)
|
|
138
136
|
return "";
|
|
139
|
-
// Comments
|
|
140
137
|
if (/^\s*(\/\/|#|\/\*)/.test(line)) {
|
|
141
138
|
return `\x1b[2;37m${line}\x1b[0m`;
|
|
142
139
|
}
|
|
143
|
-
// Tokenize words, strings, numbers
|
|
144
140
|
return line.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+\.?\d*\b)|(\b[a-zA-Z_$][\w$]*\b)|(\/\/[^\n]*|#[^\n]*)/g, (m, str, num, ident, comment) => {
|
|
145
141
|
if (str)
|
|
146
142
|
return `\x1b[32m${str}\x1b[0m`;
|
|
@@ -153,6 +149,60 @@ function highlightCodeLine(line, _lang = "") {
|
|
|
153
149
|
return m;
|
|
154
150
|
});
|
|
155
151
|
}
|
|
152
|
+
function formatInlineMarkdown(text) {
|
|
153
|
+
let line = text;
|
|
154
|
+
line = line.replace(/\*\*\*([^*]+)\*\*\*/g, "\x1b[1;3m$1\x1b[0m");
|
|
155
|
+
line = line.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
|
|
156
|
+
line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
|
|
157
|
+
line = line.replace(/`([^`]+)`/g, "\x1b[1;36m`$1`\x1b[0m");
|
|
158
|
+
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
|
|
159
|
+
return line;
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Markdown Table Formatter
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function formatMarkdownTable(tableLines) {
|
|
165
|
+
if (tableLines.length < 2)
|
|
166
|
+
return tableLines;
|
|
167
|
+
const parseRow = (row) => row
|
|
168
|
+
.replace(/^\||\|$/g, "")
|
|
169
|
+
.split("|")
|
|
170
|
+
.map((c) => c.trim());
|
|
171
|
+
const header = parseRow(tableLines[0]);
|
|
172
|
+
const rows = tableLines.slice(2).map(parseRow);
|
|
173
|
+
const allRows = [header, ...rows];
|
|
174
|
+
const colWidths = [];
|
|
175
|
+
for (let c = 0; c < header.length; c++) {
|
|
176
|
+
let mw = 0;
|
|
177
|
+
for (const r of allRows) {
|
|
178
|
+
if (r[c])
|
|
179
|
+
mw = Math.max(mw, plainLen(formatInlineMarkdown(r[c])));
|
|
180
|
+
}
|
|
181
|
+
colWidths.push(Math.max(mw, 3));
|
|
182
|
+
}
|
|
183
|
+
const out = [];
|
|
184
|
+
const topBorder = `\x1b[90m┌${colWidths.map((w) => "─".repeat(w + 2)).join("┬")}┐\x1b[0m`;
|
|
185
|
+
const midBorder = `\x1b[90m├${colWidths.map((w) => "─".repeat(w + 2)).join("┼")}┤\x1b[0m`;
|
|
186
|
+
const botBorder = `\x1b[90m└${colWidths.map((w) => "─".repeat(w + 2)).join("┴")}┘\x1b[0m`;
|
|
187
|
+
out.push(topBorder);
|
|
188
|
+
const headCells = header.map((h, i) => {
|
|
189
|
+
const formatted = `\x1b[1;37m${formatInlineMarkdown(h)}\x1b[0m`;
|
|
190
|
+
const pad = colWidths[i] - plainLen(formatted);
|
|
191
|
+
return ` ${formatted}${" ".repeat(Math.max(pad, 0))} `;
|
|
192
|
+
});
|
|
193
|
+
out.push(`\x1b[90m│\x1b[0m${headCells.join("\x1b[90m│\x1b[0m")}\x1b[90m│\x1b[0m`);
|
|
194
|
+
out.push(midBorder);
|
|
195
|
+
for (const r of rows) {
|
|
196
|
+
const rowCells = header.map((_, i) => {
|
|
197
|
+
const cell = r[i] ? formatInlineMarkdown(r[i]) : "";
|
|
198
|
+
const pad = colWidths[i] - plainLen(cell);
|
|
199
|
+
return ` ${cell}${" ".repeat(Math.max(pad, 0))} `;
|
|
200
|
+
});
|
|
201
|
+
out.push(`\x1b[90m│\x1b[0m${rowCells.join("\x1b[90m│\x1b[0m")}\x1b[90m│\x1b[0m`);
|
|
202
|
+
}
|
|
203
|
+
out.push(botBorder);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
156
206
|
// ---------------------------------------------------------------------------
|
|
157
207
|
// Rich Markdown -> ANSI Terminal Renderer
|
|
158
208
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +213,7 @@ export function markdownToAnsi(text) {
|
|
|
163
213
|
let inCode = false;
|
|
164
214
|
let codeLang = "";
|
|
165
215
|
let codeBuffer = [];
|
|
216
|
+
let tableBuffer = [];
|
|
166
217
|
const flushCodeBlock = () => {
|
|
167
218
|
if (!codeBuffer.length && !codeLang)
|
|
168
219
|
return;
|
|
@@ -177,10 +228,18 @@ export function markdownToAnsi(text) {
|
|
|
177
228
|
codeBuffer = [];
|
|
178
229
|
codeLang = "";
|
|
179
230
|
};
|
|
231
|
+
const flushTable = () => {
|
|
232
|
+
if (!tableBuffer.length)
|
|
233
|
+
return;
|
|
234
|
+
const formatted = formatMarkdownTable(tableBuffer);
|
|
235
|
+
out.push(...formatted);
|
|
236
|
+
tableBuffer = [];
|
|
237
|
+
};
|
|
180
238
|
for (let i = 0; i < rawLines.length; i++) {
|
|
181
239
|
const raw = rawLines[i];
|
|
182
240
|
const fenceMatch = raw.match(/^(`{3,})(.*)$/);
|
|
183
241
|
if (fenceMatch) {
|
|
242
|
+
flushTable();
|
|
184
243
|
if (!inCode) {
|
|
185
244
|
inCode = true;
|
|
186
245
|
codeLang = fenceMatch[2].trim().toLowerCase();
|
|
@@ -197,44 +256,54 @@ export function markdownToAnsi(text) {
|
|
|
197
256
|
codeBuffer.push(raw);
|
|
198
257
|
continue;
|
|
199
258
|
}
|
|
259
|
+
// Markdown Table rows
|
|
260
|
+
if (/^\s*\|.*\|\s*$/.test(raw)) {
|
|
261
|
+
tableBuffer.push(raw.trim());
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
else if (tableBuffer.length) {
|
|
265
|
+
flushTable();
|
|
266
|
+
}
|
|
267
|
+
// Horizontal rules
|
|
268
|
+
if (/^(\*{3,}|-{3,}|_{3,})$/.test(raw.trim())) {
|
|
269
|
+
out.push(`\x1b[90m${"─".repeat(Math.min(width, 60))}\x1b[0m`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
200
272
|
let line = raw;
|
|
201
273
|
// Headers
|
|
202
274
|
const h1 = line.match(/^#\s+(.+)$/);
|
|
203
275
|
if (h1) {
|
|
204
|
-
out.push(`\x1b[1;36m# ${h1[1]}\x1b[0m`);
|
|
276
|
+
out.push(`\x1b[1;36m# ${formatInlineMarkdown(h1[1])}\x1b[0m`);
|
|
205
277
|
continue;
|
|
206
278
|
}
|
|
207
279
|
const h2 = line.match(/^##\s+(.+)$/);
|
|
208
280
|
if (h2) {
|
|
209
|
-
out.push(`\x1b[1;34m## ${h2[1]}\x1b[0m`);
|
|
281
|
+
out.push(`\x1b[1;34m## ${formatInlineMarkdown(h2[1])}\x1b[0m`);
|
|
210
282
|
continue;
|
|
211
283
|
}
|
|
212
284
|
const h3 = line.match(/^###\s+(.+)$/);
|
|
213
285
|
if (h3) {
|
|
214
|
-
out.push(`\x1b[1;37m### ${h3[1]}\x1b[0m`);
|
|
286
|
+
out.push(`\x1b[1;37m### ${formatInlineMarkdown(h3[1])}\x1b[0m`);
|
|
215
287
|
continue;
|
|
216
288
|
}
|
|
217
289
|
// Blockquote
|
|
218
290
|
const bq = line.match(/^>\s*(.+)$/);
|
|
219
291
|
if (bq) {
|
|
220
|
-
out.push(`\x1b[90m│\x1b[0m \x1b[3m${bq[1]}\x1b[0m`);
|
|
292
|
+
out.push(`\x1b[90m│\x1b[0m \x1b[3m${formatInlineMarkdown(bq[1])}\x1b[0m`);
|
|
221
293
|
continue;
|
|
222
294
|
}
|
|
223
295
|
// Unordered List item
|
|
224
|
-
line = line.replace(/^(\s*)[-*+]\s+(.+)$/,
|
|
296
|
+
line = line.replace(/^(\s*)[-*+]\s+(.+)$/, (_, indent, item) => {
|
|
297
|
+
return `${indent}\x1b[36m•\x1b[0m ${formatInlineMarkdown(item)}`;
|
|
298
|
+
});
|
|
225
299
|
// Ordered List item
|
|
226
|
-
line = line.replace(/^(\s*)(\d+)\.\s+(.+)$/,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
|
|
234
|
-
// Inline code (`code`)
|
|
235
|
-
line = line.replace(/`([^`]+)`/g, "\x1b[1;36m`$1`\x1b[0m");
|
|
236
|
-
// Links [text](url) -> text (url)
|
|
237
|
-
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
|
|
300
|
+
line = line.replace(/^(\s*)(\d+)\.\s+(.+)$/, (_, indent, num, item) => {
|
|
301
|
+
return `${indent}\x1b[36m${num}.\x1b[0m ${formatInlineMarkdown(item)}`;
|
|
302
|
+
});
|
|
303
|
+
// Inline formatting if not a list
|
|
304
|
+
if (!line.includes("\x1b[36m•\x1b[0m") && !line.includes(".\x1b[0m ")) {
|
|
305
|
+
line = formatInlineMarkdown(line);
|
|
306
|
+
}
|
|
238
307
|
// Word wrap paragraph line if too long
|
|
239
308
|
if (plainLen(line) > width && !line.startsWith(" ")) {
|
|
240
309
|
const words = line.split(" ");
|
|
@@ -256,9 +325,9 @@ export function markdownToAnsi(text) {
|
|
|
256
325
|
out.push(line);
|
|
257
326
|
}
|
|
258
327
|
}
|
|
259
|
-
|
|
328
|
+
flushTable();
|
|
329
|
+
if (inCode)
|
|
260
330
|
flushCodeBlock();
|
|
261
|
-
}
|
|
262
331
|
return out.join("\n");
|
|
263
332
|
}
|
|
264
333
|
export function aiMarkdown(text) {
|
|
@@ -286,11 +355,13 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
286
355
|
if (subtitle)
|
|
287
356
|
maxLineW = Math.max(maxLineW, plainLen(subtitle) + 4);
|
|
288
357
|
const termW = terminalWidth();
|
|
289
|
-
const boxW = expand
|
|
290
|
-
|
|
358
|
+
const boxW = expand
|
|
359
|
+
? Math.max(termW - 4, 20)
|
|
360
|
+
: Math.min(Math.max(maxLineW + 4, 20), termW - 4);
|
|
361
|
+
const innerW = Math.max(boxW - 4, 1);
|
|
291
362
|
const embed = (text, align) => {
|
|
292
363
|
if (!text)
|
|
293
|
-
return "─".repeat(boxW - 2);
|
|
364
|
+
return "─".repeat(Math.max(boxW - 2, 0));
|
|
294
365
|
const inner = ` ${text} `;
|
|
295
366
|
const fill = Math.max(boxW - 2 - plainLen(inner), 0);
|
|
296
367
|
if (align === "left") {
|
|
@@ -306,8 +377,15 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
306
377
|
for (let i = 0; i < styledLines.length; i++) {
|
|
307
378
|
const line = styledLines[i];
|
|
308
379
|
const plain = plainLines[i] ?? "";
|
|
309
|
-
|
|
310
|
-
|
|
380
|
+
if (plain.length > innerW) {
|
|
381
|
+
// Truncate cleanly if single line overflows max box width
|
|
382
|
+
const truncated = line.slice(0, innerW);
|
|
383
|
+
out.push(`\x1b[${borderStyle}m│\x1b[0m ${truncated} \x1b[${borderStyle}m│\x1b[0m`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const pad = Math.max(innerW - plain.length, 0);
|
|
387
|
+
out.push(`\x1b[${borderStyle}m│\x1b[0m ${line}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m`);
|
|
388
|
+
}
|
|
311
389
|
}
|
|
312
390
|
const bottom = `╰${embed(subtitle, "center")}╯`;
|
|
313
391
|
out.push(`\x1b[${borderStyle}m${bottom}\x1b[0m`);
|
|
@@ -351,19 +429,12 @@ export function renderTableString(rows, opts = {}) {
|
|
|
351
429
|
// Durations & Text Formatting
|
|
352
430
|
// ---------------------------------------------------------------------------
|
|
353
431
|
export function formatDuration(seconds) {
|
|
354
|
-
const total = Math.max(0, Math.round(seconds));
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
const secs =
|
|
359
|
-
|
|
360
|
-
if (hours)
|
|
361
|
-
parts.push(`${hours}h`);
|
|
362
|
-
if (minutes)
|
|
363
|
-
parts.push(`${minutes}min`);
|
|
364
|
-
if (secs || !parts.length)
|
|
365
|
-
parts.push(`${secs}s`);
|
|
366
|
-
return parts.join(" ");
|
|
432
|
+
const total = Math.max(0, Math.round(seconds * 10) / 10);
|
|
433
|
+
if (total < 60)
|
|
434
|
+
return `${total}s`;
|
|
435
|
+
const mins = Math.floor(total / 60);
|
|
436
|
+
const secs = Math.round(total % 60);
|
|
437
|
+
return `${mins}m ${secs}s`;
|
|
367
438
|
}
|
|
368
439
|
export function tickDuration(seconds) {
|
|
369
440
|
return `\x1b[22;37m${formatDuration(seconds)}\x1b[2m`;
|
|
@@ -516,19 +587,19 @@ export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
|
516
587
|
const start = args["start_line"];
|
|
517
588
|
const end = args["end_line"];
|
|
518
589
|
const span = start != null || end != null
|
|
519
|
-
? `
|
|
590
|
+
? ` lines ${start ?? 1}-${end ?? "end"}`
|
|
520
591
|
: "";
|
|
521
592
|
return phrase("Read", "Reading", "Failed to read", `${p}${span}`);
|
|
522
593
|
}
|
|
523
594
|
case "write_file":
|
|
524
|
-
return phrase("Wrote", "Writing", "Failed to write", `${p}
|
|
595
|
+
return phrase("Wrote", "Writing", "Failed to write", `${p} · ${String(args["content"] ?? "").length.toLocaleString()} chars`);
|
|
525
596
|
case "edit_file": {
|
|
526
|
-
const suffix = args["replace_all"] ? "
|
|
597
|
+
const suffix = args["replace_all"] ? " · replace all" : "";
|
|
527
598
|
return phrase("Edited", "Editing", "Failed to edit", `${p}${suffix}`);
|
|
528
599
|
}
|
|
529
600
|
case "bash": {
|
|
530
601
|
const cwd = args["cwd"];
|
|
531
|
-
const location = cwd ? `
|
|
602
|
+
const location = cwd ? ` in ${cwd}` : "";
|
|
532
603
|
const command = truncateEllipsis(String(args["command"] ?? ""), 400, "command");
|
|
533
604
|
if (status === "started")
|
|
534
605
|
return `Running command${location}: ${command}`;
|
|
@@ -657,7 +728,6 @@ function wrapRuns(runs, width) {
|
|
|
657
728
|
export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
|
|
658
729
|
const termW = terminalWidth();
|
|
659
730
|
const boxW = Math.max(termW - 4, 16);
|
|
660
|
-
// Content inside border: `│ ` on left (2 chars) and ` │` on right (2 chars)
|
|
661
731
|
const innerW = boxW - 4;
|
|
662
732
|
const runs = [];
|
|
663
733
|
runs.push({ text: prefix, style: "\x1b[1;36m" });
|
|
@@ -743,8 +813,6 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
743
813
|
}
|
|
744
814
|
}
|
|
745
815
|
const { rows, caretRow, caretCol } = wrapRuns(runs, innerW);
|
|
746
|
-
// Math for exact box alignment:
|
|
747
|
-
// Top: `╭─ ` (3 chars) + `label` (L chars) + ` ` (1 char) + topPad + `╮` (1 char) = boxW
|
|
748
816
|
const labelLen = plainLen(label);
|
|
749
817
|
const topPad = Math.max(0, boxW - labelLen - 5);
|
|
750
818
|
const top = `\x1b[90m╭─ \x1b[1m\x1b[36m${label}\x1b[0m\x1b[90m ${"─".repeat(topPad)}╮\x1b[0m`;
|
|
@@ -754,7 +822,6 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
754
822
|
const pad = Math.max(0, innerW - plain.length);
|
|
755
823
|
body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
|
|
756
824
|
}
|
|
757
|
-
// Bottom: `╰` (1 char) + `─` * (boxW - 2) + `╯` (1 char) = boxW
|
|
758
825
|
const bottom = `\x1b[90m╰${"─".repeat(Math.max(0, boxW - 2))}╯\x1b[0m`;
|
|
759
826
|
const frame = [top, ...body, bottom].join("\n");
|
|
760
827
|
const cursorRow = (caretRow === -1 ? 0 : caretRow) + 1;
|
|
@@ -932,6 +999,36 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
932
999
|
}
|
|
933
1000
|
pasteSpans = adjusted.filter(([s, e]) => s < e);
|
|
934
1001
|
};
|
|
1002
|
+
const deleteChar = () => {
|
|
1003
|
+
if (cursor >= buffer.length)
|
|
1004
|
+
return;
|
|
1005
|
+
if (histIdx !== hist.length) {
|
|
1006
|
+
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
1007
|
+
histIdx = hist.length;
|
|
1008
|
+
}
|
|
1009
|
+
const containing = pasteSpans.find(([s, e]) => s <= cursor && cursor < e);
|
|
1010
|
+
if (containing) {
|
|
1011
|
+
const [start, end] = containing;
|
|
1012
|
+
buffer = buffer.slice(0, start) + buffer.slice(end);
|
|
1013
|
+
const delta = start - end;
|
|
1014
|
+
pasteSpans = pasteSpans
|
|
1015
|
+
.filter(([s, e]) => s !== start || e !== end)
|
|
1016
|
+
.map(([s, e]) => (s >= end ? [s + delta, e + delta] : [s, e]));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1);
|
|
1020
|
+
};
|
|
1021
|
+
const deleteWordBefore = () => {
|
|
1022
|
+
if (cursor <= 0)
|
|
1023
|
+
return;
|
|
1024
|
+
const before = buffer.slice(0, cursor);
|
|
1025
|
+
const trimmed = before.replace(/\s+$/, "");
|
|
1026
|
+
const lastSpace = Math.max(trimmed.lastIndexOf(" "), trimmed.lastIndexOf("\t"), trimmed.lastIndexOf("\n"));
|
|
1027
|
+
const targetPos = lastSpace === -1 ? 0 : lastSpace + 1;
|
|
1028
|
+
buffer = buffer.slice(0, targetPos) + buffer.slice(cursor);
|
|
1029
|
+
cursor = targetPos;
|
|
1030
|
+
pasteSpans = [];
|
|
1031
|
+
};
|
|
935
1032
|
const moveLeft = () => {
|
|
936
1033
|
if (cursor <= 0)
|
|
937
1034
|
return;
|
|
@@ -1138,6 +1235,33 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1138
1235
|
settle(new Error("eof"), true);
|
|
1139
1236
|
return;
|
|
1140
1237
|
}
|
|
1238
|
+
if (key && key.ctrl && key.name === "a") {
|
|
1239
|
+
cursor = 0;
|
|
1240
|
+
repaint();
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (key && key.ctrl && key.name === "e") {
|
|
1244
|
+
cursor = buffer.length;
|
|
1245
|
+
repaint();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (key && key.ctrl && key.name === "u") {
|
|
1249
|
+
buffer = "";
|
|
1250
|
+
cursor = 0;
|
|
1251
|
+
pasteSpans = [];
|
|
1252
|
+
repaint();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (key && key.ctrl && key.name === "k") {
|
|
1256
|
+
buffer = buffer.slice(0, cursor);
|
|
1257
|
+
repaint();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (key && key.ctrl && key.name === "w") {
|
|
1261
|
+
deleteWordBefore();
|
|
1262
|
+
repaint();
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1141
1265
|
if (isNewlineKey(key) ||
|
|
1142
1266
|
isModifiedEnterInput(str, key) ||
|
|
1143
1267
|
(pasteBurst && isSubmitEnterKey(key))) {
|
|
@@ -1156,6 +1280,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
1156
1280
|
repaint();
|
|
1157
1281
|
return;
|
|
1158
1282
|
}
|
|
1283
|
+
if (key && (key.name === "delete" || key.sequence === "\x1b[3~")) {
|
|
1284
|
+
deleteChar();
|
|
1285
|
+
repaint();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (key && (key.name === "home" || key.sequence === "\x1b[H" || key.sequence === "\x1b[1~")) {
|
|
1289
|
+
cursor = 0;
|
|
1290
|
+
repaint();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (key && (key.name === "end" || key.sequence === "\x1b[F" || key.sequence === "\x1b[4~")) {
|
|
1294
|
+
cursor = buffer.length;
|
|
1295
|
+
repaint();
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1159
1298
|
if (key && key.name === "left") {
|
|
1160
1299
|
moveLeft();
|
|
1161
1300
|
repaint();
|