@oxecli/oxe 1.0.5 → 1.0.7
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/config.js +6 -1
- package/dist/engine.js +87 -49
- package/dist/ui.js +182 -19
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Spinner } from "./ui.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Paths
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -162,15 +163,19 @@ export async function loadOrPrompt() {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
process.stdout.write("\n");
|
|
165
|
-
|
|
166
|
+
const authSpinner = new Spinner();
|
|
167
|
+
authSpinner.start("Authenticating key with Oxe Cloud…");
|
|
166
168
|
const validation = await validateOxeApiKey(api_key);
|
|
169
|
+
authSpinner.stop();
|
|
167
170
|
if (validation.valid) {
|
|
168
171
|
key_data = validation.key_data || {};
|
|
172
|
+
process.stdout.write("\n");
|
|
169
173
|
process.stdout.write(markupToAnsi(`[green]✓ Oxe API Key verified[/green] [dim](${String(key_data["name"] ?? "Desktop")})[/dim]`) + "\n");
|
|
170
174
|
process.env.OXE_API_KEY = api_key;
|
|
171
175
|
break;
|
|
172
176
|
}
|
|
173
177
|
else {
|
|
178
|
+
process.stdout.write("\n");
|
|
174
179
|
process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n`);
|
|
175
180
|
process.stdout.write("\n");
|
|
176
181
|
api_key = "";
|
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 { SYSTEM_PROMPT } from "./system.js";
|
|
4
4
|
import { buildTools, truncateToolOutput, TOOL_IMPLEMENTATIONS } from "./tools.js";
|
|
5
|
-
import { mutedMarkdown, aiMarkdown, tickDuration, safeCommitPoint, printAiChunk, formatToolAction, } from "./ui.js";
|
|
5
|
+
import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, formatToolAction, renderPanel, Spinner, hideCursor, showCursor, } from "./ui.js";
|
|
6
6
|
import { reportUsage } from "./api.js";
|
|
7
7
|
import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -35,30 +35,6 @@ const objectId = (() => {
|
|
|
35
35
|
};
|
|
36
36
|
})();
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
|
-
// Simple status line helper
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
class Status {
|
|
41
|
-
text;
|
|
42
|
-
timer = null;
|
|
43
|
-
started;
|
|
44
|
-
constructor(initial) {
|
|
45
|
-
this.text = initial;
|
|
46
|
-
this.started = Date.now();
|
|
47
|
-
}
|
|
48
|
-
start() {
|
|
49
|
-
process.stdout.write("\r\x1b[2K" + mutedMarkdown(this.text) + "\n");
|
|
50
|
-
}
|
|
51
|
-
update(text) {
|
|
52
|
-
this.text = text;
|
|
53
|
-
process.stdout.write("\r\x1b[2K" + mutedMarkdown(text) + "\x1b[1A");
|
|
54
|
-
}
|
|
55
|
-
stop() {
|
|
56
|
-
if (this.timer)
|
|
57
|
-
clearInterval(this.timer);
|
|
58
|
-
this.timer = null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
38
|
// InferenceEngine
|
|
63
39
|
// ---------------------------------------------------------------------------
|
|
64
40
|
export class InferenceEngine {
|
|
@@ -66,6 +42,10 @@ export class InferenceEngine {
|
|
|
66
42
|
reasoningEffort;
|
|
67
43
|
keyData;
|
|
68
44
|
client;
|
|
45
|
+
// "Worked for Xs" line is overwritten in place across tool iterations
|
|
46
|
+
// (mirrors Python's _work_active / _overwrite_work_line).
|
|
47
|
+
workActive = false;
|
|
48
|
+
workRows = 2;
|
|
69
49
|
inQuery = false;
|
|
70
50
|
temperature;
|
|
71
51
|
storedResponseIds = [];
|
|
@@ -76,7 +56,8 @@ export class InferenceEngine {
|
|
|
76
56
|
this.client = new OpenAI({
|
|
77
57
|
apiKey: config["api_key"],
|
|
78
58
|
baseURL: config["base_url"],
|
|
79
|
-
timeout
|
|
59
|
+
// OpenAI SDK `timeout` is in MILLISECONDS; config stores seconds.
|
|
60
|
+
timeout: (config["timeout"] ?? 120) * 1000,
|
|
80
61
|
maxRetries: config["max_retries"] ?? 2,
|
|
81
62
|
});
|
|
82
63
|
this.temperature = config["temperature"] ?? null;
|
|
@@ -136,28 +117,70 @@ export class InferenceEngine {
|
|
|
136
117
|
incompleteReason(response) {
|
|
137
118
|
return response?.incomplete_details?.reason ?? null;
|
|
138
119
|
}
|
|
120
|
+
/** Replace the previously-printed "Worked for Xs" line in place. */
|
|
121
|
+
overwriteWorkLine(text) {
|
|
122
|
+
for (let i = 0; i < this.workRows; i++) {
|
|
123
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
124
|
+
}
|
|
125
|
+
process.stdout.write(mutedMarkdown(text) + "\n\n");
|
|
126
|
+
this.workRows = displayRows(text) + 1;
|
|
127
|
+
}
|
|
139
128
|
async streamOnce(inputItems, stats, story, previousResponseId) {
|
|
140
129
|
let content = "";
|
|
141
130
|
let committedLen = 0;
|
|
142
131
|
let pending = "";
|
|
143
132
|
let sawReasoning = false;
|
|
144
133
|
let status = null;
|
|
145
|
-
|
|
146
|
-
const workStatus = new
|
|
147
|
-
|
|
134
|
+
let thinkingStart = null;
|
|
135
|
+
const workStatus = new Spinner();
|
|
136
|
+
const workingStarted = Date.now();
|
|
137
|
+
workStatus.start("Working for `0s`");
|
|
138
|
+
const workTimer = setInterval(() => {
|
|
139
|
+
workStatus.update(`Working for ${tickDuration((Date.now() - workingStarted) / 1000)}`);
|
|
140
|
+
}, 500);
|
|
141
|
+
let workingReported = false;
|
|
148
142
|
let response = null;
|
|
149
143
|
let etype = null;
|
|
144
|
+
const silenceWorking = () => {
|
|
145
|
+
clearInterval(workTimer);
|
|
146
|
+
workStatus.stop();
|
|
147
|
+
// Reasoning took over; suppress any later "Worked for Xs" report for this
|
|
148
|
+
// stream (mirrors Python's working_started = None in silence_working()).
|
|
149
|
+
workingReported = true;
|
|
150
|
+
};
|
|
151
|
+
const reportWorking = () => {
|
|
152
|
+
clearInterval(workTimer);
|
|
153
|
+
workStatus.stop();
|
|
154
|
+
if (workingReported)
|
|
155
|
+
return;
|
|
156
|
+
workingReported = true;
|
|
157
|
+
const elapsed = (Date.now() - workingStarted) / 1000;
|
|
158
|
+
const t = tickDuration(elapsed);
|
|
159
|
+
story.push({ type: "worked", text: `Worked for ${t}` });
|
|
160
|
+
if (this.workActive) {
|
|
161
|
+
this.overwriteWorkLine(`Worked for ${t}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
process.stdout.write(mutedMarkdown(`Worked for ${t}`) + "\n\n");
|
|
165
|
+
this.workActive = true;
|
|
166
|
+
this.workRows = displayRows(`Worked for ${t}`) + 1;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
150
169
|
const finishThinking = (report) => {
|
|
170
|
+
if (thinkingStart === null)
|
|
171
|
+
return;
|
|
172
|
+
const elapsed = (Date.now() - thinkingStart) / 1000;
|
|
151
173
|
if (status) {
|
|
152
174
|
status.stop();
|
|
153
175
|
status = null;
|
|
154
176
|
}
|
|
155
177
|
if (report) {
|
|
156
|
-
const elapsed = (Date.now() - thinkingStart) / 1000;
|
|
157
178
|
const t = tickDuration(elapsed);
|
|
158
179
|
story.push({ type: "thought", text: `Thought for ${t}` });
|
|
159
|
-
process.stdout.write(
|
|
180
|
+
process.stdout.write(mutedMarkdown(`Thought for ${t}`) + "\n\n");
|
|
181
|
+
this.workActive = false;
|
|
160
182
|
}
|
|
183
|
+
thinkingStart = null;
|
|
161
184
|
};
|
|
162
185
|
try {
|
|
163
186
|
const kwargs = {
|
|
@@ -189,13 +212,14 @@ export class InferenceEngine {
|
|
|
189
212
|
if (etype === "response.reasoning_text.delta" ||
|
|
190
213
|
etype === "response.reasoning.summary.delta") {
|
|
191
214
|
sawReasoning = true;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
215
|
+
silenceWorking();
|
|
216
|
+
if (thinkingStart === null) {
|
|
217
|
+
thinkingStart = Date.now();
|
|
218
|
+
status = new Spinner();
|
|
219
|
+
status.start("Thinking for `0s`");
|
|
195
220
|
}
|
|
196
221
|
else {
|
|
197
|
-
|
|
198
|
-
status.update(`Thinking for ${tickDuration(elapsed)}`);
|
|
222
|
+
status?.update(`Thinking for ${tickDuration((Date.now() - thinkingStart) / 1000)}`);
|
|
199
223
|
}
|
|
200
224
|
}
|
|
201
225
|
else if (etype === "response.reasoning_text.done" ||
|
|
@@ -203,6 +227,7 @@ export class InferenceEngine {
|
|
|
203
227
|
finishThinking(true);
|
|
204
228
|
}
|
|
205
229
|
else if (etype === "response.output_text.delta") {
|
|
230
|
+
reportWorking();
|
|
206
231
|
finishThinking(true);
|
|
207
232
|
content += event.delta;
|
|
208
233
|
pending += event.delta;
|
|
@@ -223,8 +248,13 @@ export class InferenceEngine {
|
|
|
223
248
|
}
|
|
224
249
|
}
|
|
225
250
|
finally {
|
|
251
|
+
clearInterval(workTimer);
|
|
226
252
|
workStatus.stop();
|
|
227
|
-
|
|
253
|
+
// If the stream ended while still reasoning (no *.done event), still report
|
|
254
|
+
// the elapsed "Thought for Xs" (mirrors Python's `still_thinking` handling).
|
|
255
|
+
const stillThinking = thinkingStart !== null;
|
|
256
|
+
finishThinking(stillThinking);
|
|
257
|
+
reportWorking();
|
|
228
258
|
if (content.slice(committedLen))
|
|
229
259
|
printAiChunk(content.slice(committedLen));
|
|
230
260
|
}
|
|
@@ -331,13 +361,17 @@ export class InferenceEngine {
|
|
|
331
361
|
if (!this.contextOverBudget(conversation))
|
|
332
362
|
return [conversation, false];
|
|
333
363
|
const started = Date.now();
|
|
334
|
-
const comp = new
|
|
335
|
-
comp.start();
|
|
364
|
+
const comp = new Spinner();
|
|
365
|
+
comp.start("Compacting conversation for `0s`");
|
|
366
|
+
const compTimer = setInterval(() => {
|
|
367
|
+
comp.update(`Compacting conversation for ${tickDuration((Date.now() - started) / 1000)}`);
|
|
368
|
+
}, 500);
|
|
336
369
|
let compacted;
|
|
337
370
|
try {
|
|
338
371
|
compacted = await this.compactHistory(conversation);
|
|
339
372
|
}
|
|
340
373
|
finally {
|
|
374
|
+
clearInterval(compTimer);
|
|
341
375
|
comp.stop();
|
|
342
376
|
}
|
|
343
377
|
if (!compacted)
|
|
@@ -345,11 +379,14 @@ export class InferenceEngine {
|
|
|
345
379
|
persistCompactionSummary(inputItems, compacted[0]);
|
|
346
380
|
const text = `Compacted conversation in ${tickDuration((Date.now() - started) / 1000)}`;
|
|
347
381
|
story.push({ type: "compacted", text });
|
|
348
|
-
|
|
382
|
+
this.workActive = false;
|
|
383
|
+
process.stdout.write(mutedMarkdown(text) + "\n");
|
|
384
|
+
process.stdout.write("\n");
|
|
349
385
|
return [compacted, true];
|
|
350
386
|
}
|
|
351
387
|
async executeQuery(userPrompt, inputItems, story, pasteSpans) {
|
|
352
388
|
this.inQuery = true;
|
|
389
|
+
hideCursor();
|
|
353
390
|
inputItems.push({
|
|
354
391
|
role: "user",
|
|
355
392
|
content: userPrompt,
|
|
@@ -383,7 +420,6 @@ export class InferenceEngine {
|
|
|
383
420
|
};
|
|
384
421
|
let prevId = null;
|
|
385
422
|
let pending = conversation;
|
|
386
|
-
let resetInQuery = true;
|
|
387
423
|
try {
|
|
388
424
|
for (let step = 0; step < max_agent_steps; step++) {
|
|
389
425
|
let text = "";
|
|
@@ -417,13 +453,11 @@ export class InferenceEngine {
|
|
|
417
453
|
respId = res.responseId;
|
|
418
454
|
}
|
|
419
455
|
catch (err2) {
|
|
420
|
-
process.stdout.write("\n");
|
|
421
456
|
renderErrorPanel(`API Error: ${err2}`);
|
|
422
457
|
return;
|
|
423
458
|
}
|
|
424
459
|
}
|
|
425
460
|
else {
|
|
426
|
-
process.stdout.write("\n");
|
|
427
461
|
renderErrorPanel(`API Error: ${err}`);
|
|
428
462
|
return;
|
|
429
463
|
}
|
|
@@ -457,7 +491,7 @@ export class InferenceEngine {
|
|
|
457
491
|
continue;
|
|
458
492
|
}
|
|
459
493
|
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
|
460
|
-
process.stdout.write(
|
|
494
|
+
process.stdout.write(aiMarkdown(emptyMessage) + "\n");
|
|
461
495
|
process.stdout.write("\n" + summary() + "\n");
|
|
462
496
|
return;
|
|
463
497
|
}
|
|
@@ -471,6 +505,7 @@ export class InferenceEngine {
|
|
|
471
505
|
if (text.trim())
|
|
472
506
|
process.stdout.write("\n");
|
|
473
507
|
for (const c of calls) {
|
|
508
|
+
this.workActive = false;
|
|
474
509
|
const started = formatToolAction(c.name, c.arguments, "started");
|
|
475
510
|
process.stdout.write(`\x1b[2m${started}\x1b[0m\n`);
|
|
476
511
|
if (c.name === "edit_file" || c.name === "write_file")
|
|
@@ -495,9 +530,11 @@ export class InferenceEngine {
|
|
|
495
530
|
pending.push(outputItem);
|
|
496
531
|
}
|
|
497
532
|
}
|
|
498
|
-
process.stdout.write("\n
|
|
499
|
-
|
|
500
|
-
"[
|
|
533
|
+
process.stdout.write("\n");
|
|
534
|
+
renderPanel("[bold yellow]⚠ Max tool-call iterations reached for this turn.[/bold yellow]\n" +
|
|
535
|
+
"[yellow]The work so far is saved. If you want the agent to keep going, type " +
|
|
536
|
+
"[bold]continue[/bold] and the next step will resume from where it left off.[/yellow]", "Warning", "", false, "33");
|
|
537
|
+
process.stdout.write("\n");
|
|
501
538
|
if (retryPrompts.length)
|
|
502
539
|
dropRetryPrompts(conversation, inputItems, retryPrompts);
|
|
503
540
|
stripOrphanCalls(conversation);
|
|
@@ -507,11 +544,11 @@ export class InferenceEngine {
|
|
|
507
544
|
if (err?.message === "interrupt" || err?.message === "eof") {
|
|
508
545
|
throw err;
|
|
509
546
|
}
|
|
510
|
-
process.stdout.write("\n");
|
|
511
547
|
renderErrorPanel(`Runtime Exception: ${err}`);
|
|
512
548
|
}
|
|
513
549
|
finally {
|
|
514
550
|
this.inQuery = false;
|
|
551
|
+
showCursor();
|
|
515
552
|
if (stats["tokens"] > 0 && this.keyData) {
|
|
516
553
|
const userId = this.keyData["user_id"];
|
|
517
554
|
if (userId) {
|
|
@@ -530,6 +567,7 @@ export class InferenceEngine {
|
|
|
530
567
|
}
|
|
531
568
|
function renderErrorPanel(msg) {
|
|
532
569
|
const text = msg.replace(/\[bold yellow\]|\[yellow\]|\[\/.*?\]/g, "");
|
|
533
|
-
|
|
570
|
+
renderPanel(text, "Error", "", true, "31");
|
|
571
|
+
process.stdout.write("\n");
|
|
534
572
|
}
|
|
535
573
|
export { renderErrorPanel };
|
package/dist/ui.js
CHANGED
|
@@ -210,8 +210,14 @@ export function tickDuration(seconds) {
|
|
|
210
210
|
// ---------------------------------------------------------------------------
|
|
211
211
|
const FENCE_MARKER_RE = /`{3,}/g;
|
|
212
212
|
export function printAiChunk(chunk) {
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
// rich's Markdown renderer ignores leading blank lines; a commit boundary in
|
|
214
|
+
// "…\n\n…" text can leave the second chunk starting with "\n", so strip it to
|
|
215
|
+
// avoid double-printing a blank row (mirrors the original's output).
|
|
216
|
+
const trimmed = chunk.replace(/^\n+/, "");
|
|
217
|
+
if (!trimmed)
|
|
218
|
+
return;
|
|
219
|
+
process.stdout.write(markdownToAnsi(trimmed) + "\n");
|
|
220
|
+
if (/^```/m.test(trimmed))
|
|
215
221
|
process.stdout.write("\n");
|
|
216
222
|
}
|
|
217
223
|
export function safeCommitPoint(text) {
|
|
@@ -233,7 +239,7 @@ export function truncateEllipsis(text, maxChars, label = "text") {
|
|
|
233
239
|
export function displayRows(text) {
|
|
234
240
|
if (!text)
|
|
235
241
|
return 1;
|
|
236
|
-
const plain = text.replace(/`/g, "");
|
|
242
|
+
const plain = text.replace(/\x1b\[[0-9;]*m/g, "").replace(/`/g, "");
|
|
237
243
|
const width = Math.max(terminalWidth() || 80, 1);
|
|
238
244
|
let rows = 0;
|
|
239
245
|
const lines = plain.split("\n");
|
|
@@ -245,6 +251,92 @@ export function displayRows(text) {
|
|
|
245
251
|
return rows;
|
|
246
252
|
}
|
|
247
253
|
// ---------------------------------------------------------------------------
|
|
254
|
+
// Cursor + spinner helpers (mirror rich's console.status spinner="dots")
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
let cursorHidden = false;
|
|
257
|
+
export function hideCursor() {
|
|
258
|
+
if (!cursorHidden) {
|
|
259
|
+
process.stdout.write("\x1b[?25l");
|
|
260
|
+
cursorHidden = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
export function showCursor() {
|
|
264
|
+
if (cursorHidden) {
|
|
265
|
+
process.stdout.write("\x1b[?25h");
|
|
266
|
+
cursorHidden = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
270
|
+
/**
|
|
271
|
+
* A transient status line with an animated dot spinner, updated in place
|
|
272
|
+
* (no new lines appended on every tick). On stop() the line is erased.
|
|
273
|
+
* Mirrors rich's console.status(..., spinner="dots").
|
|
274
|
+
*/
|
|
275
|
+
export class Spinner {
|
|
276
|
+
timer = null;
|
|
277
|
+
frame = 0;
|
|
278
|
+
rows = 0;
|
|
279
|
+
text = "";
|
|
280
|
+
enabled;
|
|
281
|
+
constructor(enabled = true) {
|
|
282
|
+
this.enabled = enabled;
|
|
283
|
+
}
|
|
284
|
+
start(text) {
|
|
285
|
+
if (!this.enabled)
|
|
286
|
+
return;
|
|
287
|
+
if (this.timer) {
|
|
288
|
+
this.update(text);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.text = text;
|
|
292
|
+
this.frame = 0;
|
|
293
|
+
this.rows = 0;
|
|
294
|
+
this.timer = setInterval(() => {
|
|
295
|
+
this.frame++;
|
|
296
|
+
this.draw();
|
|
297
|
+
}, 120);
|
|
298
|
+
this.draw();
|
|
299
|
+
}
|
|
300
|
+
update(text) {
|
|
301
|
+
this.text = text;
|
|
302
|
+
if (this.timer)
|
|
303
|
+
this.draw();
|
|
304
|
+
}
|
|
305
|
+
draw() {
|
|
306
|
+
const rendered = mutedMarkdown(`${SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length]} ${this.text}`);
|
|
307
|
+
const newRows = Math.max(1, displayRows(rendered));
|
|
308
|
+
const clearRows = Math.max(this.rows, newRows, 1);
|
|
309
|
+
if (this.rows > 0)
|
|
310
|
+
process.stdout.write(`\x1b[${this.rows}A`);
|
|
311
|
+
for (let i = 0; i < clearRows; i++) {
|
|
312
|
+
process.stdout.write("\r\x1b[2K");
|
|
313
|
+
if (i < clearRows - 1)
|
|
314
|
+
process.stdout.write("\n");
|
|
315
|
+
}
|
|
316
|
+
if (clearRows > 0)
|
|
317
|
+
process.stdout.write(`\x1b[${clearRows - 1}A\r`);
|
|
318
|
+
process.stdout.write(rendered);
|
|
319
|
+
this.rows = newRows;
|
|
320
|
+
}
|
|
321
|
+
stop() {
|
|
322
|
+
if (this.timer) {
|
|
323
|
+
clearInterval(this.timer);
|
|
324
|
+
this.timer = null;
|
|
325
|
+
}
|
|
326
|
+
if (this.enabled && this.rows > 0) {
|
|
327
|
+
process.stdout.write(`\x1b[${this.rows}A`);
|
|
328
|
+
for (let i = 0; i < this.rows; i++) {
|
|
329
|
+
process.stdout.write("\r\x1b[2K");
|
|
330
|
+
if (i < this.rows - 1)
|
|
331
|
+
process.stdout.write("\n");
|
|
332
|
+
}
|
|
333
|
+
if (this.rows > 0)
|
|
334
|
+
process.stdout.write(`\x1b[${this.rows - 1}A`);
|
|
335
|
+
}
|
|
336
|
+
this.rows = 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
248
340
|
// Tool action formatter
|
|
249
341
|
// ---------------------------------------------------------------------------
|
|
250
342
|
export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
@@ -357,23 +449,84 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
357
449
|
const w = terminalWidth();
|
|
358
450
|
const borderW = Math.max(w - 4, 10);
|
|
359
451
|
const [row, col] = cursorLineCol(buffer, cursor);
|
|
360
|
-
// Build the inner content
|
|
361
|
-
//
|
|
362
|
-
let
|
|
452
|
+
// Build the inner content: prefix, then the text with the `▏` block cursor
|
|
453
|
+
// always drawn at the cursor position (mirrors the original rich frame).
|
|
454
|
+
let display;
|
|
363
455
|
if (!buffer) {
|
|
364
|
-
|
|
456
|
+
display = `\x1b[1m▏\x1b[0m\x1b[2m${PROMPT_PLACEHOLDER}\x1b[0m`;
|
|
365
457
|
}
|
|
366
458
|
else {
|
|
367
459
|
const segs = splitBlocks(buffer, pasteSpans);
|
|
368
|
-
|
|
460
|
+
// Recompute each segment's buffer range [a, b) so we can locate the cursor.
|
|
461
|
+
const ranges = [];
|
|
462
|
+
{
|
|
463
|
+
let pos = 0;
|
|
464
|
+
const pts = new Set([0, buffer.length]);
|
|
465
|
+
for (const [s, e] of pasteSpans) {
|
|
466
|
+
pts.add(s);
|
|
467
|
+
pts.add(e);
|
|
468
|
+
}
|
|
469
|
+
const sorted = [...pts].sort((a, b) => a - b);
|
|
470
|
+
const isPaste = (a, b) => pasteSpans.some(([s, e]) => s <= a && b <= e);
|
|
471
|
+
const used = new Set();
|
|
472
|
+
for (const seg of segs) {
|
|
473
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
474
|
+
const a = sorted[i];
|
|
475
|
+
const b = sorted[i + 1];
|
|
476
|
+
if (a === b || used.has(a))
|
|
477
|
+
continue;
|
|
478
|
+
used.add(a);
|
|
479
|
+
ranges.push([a, b]);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Determine the target segment + inside offset (mirrors Python).
|
|
485
|
+
let target = -1;
|
|
486
|
+
let inside = 0;
|
|
487
|
+
if (cursor >= buffer.length) {
|
|
488
|
+
target = segs.length - 1;
|
|
489
|
+
inside = ranges.length ? ranges[ranges.length - 1][1] - ranges[ranges.length - 1][0] : 0;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
493
|
+
const [a, b] = ranges[i];
|
|
494
|
+
if (a <= cursor && cursor <= b) {
|
|
495
|
+
target = i;
|
|
496
|
+
inside = cursor - a;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (target === -1) {
|
|
501
|
+
target = segs.length - 1;
|
|
502
|
+
inside = ranges.length ? ranges[ranges.length - 1][1] - ranges[ranges.length - 1][0] : 0;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
display = "";
|
|
369
506
|
for (let i = 0; i < segs.length; i++) {
|
|
370
507
|
const [, kind, disp] = segs[i];
|
|
371
508
|
if (i && !endsWithWs(segs[i - 1][2]))
|
|
372
509
|
display += " ";
|
|
373
|
-
|
|
510
|
+
if (i === target) {
|
|
511
|
+
if (kind === "collapsed") {
|
|
512
|
+
display += `\x1b[1m\x1b[36m${disp}\x1b[0m`;
|
|
513
|
+
if (!endsWithWs(disp))
|
|
514
|
+
display += " ";
|
|
515
|
+
display += `\x1b[1m▏\x1b[0m`;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
const bold = i === target;
|
|
519
|
+
display += disp.slice(0, inside);
|
|
520
|
+
display += `\x1b[1m▏\x1b[0m`;
|
|
521
|
+
display += disp.slice(inside);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
display += kind === "collapsed" ? `\x1b[1m\x1b[36m${disp}\x1b[0m` : disp;
|
|
526
|
+
}
|
|
374
527
|
}
|
|
375
|
-
content = `\x1b[1m${prefix}\x1b[0m ${display}`;
|
|
376
528
|
}
|
|
529
|
+
const content = `\x1b[1m${prefix}\x1b[0m ${display}`;
|
|
377
530
|
const lines = content.split("\n");
|
|
378
531
|
// Top border with the label embedded on the left.
|
|
379
532
|
const topPad = Math.max(borderW - label.length - 2, 0);
|
|
@@ -387,10 +540,12 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
387
540
|
}
|
|
388
541
|
const bottom = `\x1b[90m╰${"─".repeat(borderW)}╯\x1b[0m`;
|
|
389
542
|
const frame = [top, ...body, bottom].join("\n");
|
|
390
|
-
// Cursor placement: one row below the top border.
|
|
391
|
-
//
|
|
543
|
+
// Cursor placement: one row below the top border. Preceding columns are
|
|
544
|
+
// `│ ` (2) + prefix (prefix.length) + ` ` (1) = prefix.length+3, so the first
|
|
545
|
+
// display char (the `▏` block cursor) sits at column prefix.length+4 relative
|
|
546
|
+
// to line column `col`.
|
|
392
547
|
const cursorRow = row + 1;
|
|
393
|
-
const cursorCol = col + prefix.length +
|
|
548
|
+
const cursorCol = col + prefix.length + 4;
|
|
394
549
|
const totalRows = body.length + 2;
|
|
395
550
|
return { frame, cursorRow, cursorCol, totalRows };
|
|
396
551
|
}
|
|
@@ -418,6 +573,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
418
573
|
if (process.stdin.isTTY)
|
|
419
574
|
process.stdin.setRawMode(true);
|
|
420
575
|
process.stdin.resume();
|
|
576
|
+
hideCursor();
|
|
421
577
|
let done = false;
|
|
422
578
|
let lastCursorRow = 0;
|
|
423
579
|
let lastTotalRows = 0;
|
|
@@ -426,19 +582,22 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
426
582
|
return;
|
|
427
583
|
done = true;
|
|
428
584
|
process.stdin.setRawMode(false);
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
585
|
+
showCursor();
|
|
586
|
+
// Erase the prompt region: a leading blank line + the box. Cursor sits at
|
|
587
|
+
// lastCursorRow inside the box; move up to the box's top border, erase the
|
|
588
|
+
// box rows, then erase the leading blank line, leaving the cursor on that
|
|
589
|
+
// blank row. The caller's single leading newline then yields exactly ONE
|
|
590
|
+
// blank row before the echoed user prompt.
|
|
433
591
|
process.stdout.write(`\x1b[${lastCursorRow}A`);
|
|
434
592
|
for (let i = 0; i < lastTotalRows; i++) {
|
|
435
593
|
process.stdout.write("\r\x1b[K");
|
|
436
594
|
if (i < lastTotalRows - 1)
|
|
437
595
|
process.stdout.write("\n");
|
|
438
596
|
}
|
|
439
|
-
// Cursor is now on the last box row; move back up to the box's top row.
|
|
440
597
|
if (lastTotalRows > 1)
|
|
441
598
|
process.stdout.write(`\x1b[${lastTotalRows - 1}A`);
|
|
599
|
+
// Clear the leading blank line above the box.
|
|
600
|
+
process.stdout.write("\x1b[1A\r\x1b[K");
|
|
442
601
|
resolve(resolveVal);
|
|
443
602
|
};
|
|
444
603
|
const repaint = (isFirst = false) => {
|
|
@@ -446,7 +605,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
|
446
605
|
lastCursorRow = cursorRow;
|
|
447
606
|
lastTotalRows = totalRows;
|
|
448
607
|
const frameLines = frame.split("\n");
|
|
449
|
-
if (
|
|
608
|
+
if (isFirst) {
|
|
609
|
+
// A blank line above the box (mirrors the original's leading Text("")).
|
|
610
|
+
process.stdout.write("\n");
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
450
613
|
// Cursor currently sits at cursorRow (inside the box). Move to the top
|
|
451
614
|
// border, then clear+rewrite each line so old content is fully removed.
|
|
452
615
|
process.stdout.write(`\x1b[${cursorRow}A`);
|