@lazyingart/agintiflow 0.12.0 → 0.12.2
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/README.md +1 -1
- package/package.json +1 -1
- package/scripts/smoke-cli-chat.js +34 -1
- package/src/interactive-cli.js +487 -29
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ aginti
|
|
|
67
67
|
aginti chat
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
|
|
70
|
+
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. Assistant responses render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, and tables. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
|
|
71
71
|
|
|
72
72
|
For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSeek v4 pro by default and exposes `apply_patch` as a deterministic workspace tool. It supports exact replacements, Codex-style patch envelopes, and unified diffs, with preflight checks, path guardrails, hashes, and compact per-file diffs. See [docs/patch-tools.md](docs/patch-tools.md).
|
|
73
73
|
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { buildPromptLayout, stripMarkdown } from "../src/interactive-cli.js";
|
|
7
8
|
|
|
8
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-cli-chat-"));
|
|
@@ -52,6 +53,38 @@ function runCli(args, inputText) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
try {
|
|
56
|
+
const renderedMarkdown = stripMarkdown(
|
|
57
|
+
[
|
|
58
|
+
"**Docker status**",
|
|
59
|
+
"",
|
|
60
|
+
"| Check | Result |",
|
|
61
|
+
"| --- | --- |",
|
|
62
|
+
"| `/.dockerenv` | **Present** |",
|
|
63
|
+
"| Hostname | `abc123` |",
|
|
64
|
+
].join("\n")
|
|
65
|
+
);
|
|
66
|
+
if (renderedMarkdown.includes("**") || renderedMarkdown.includes("| --- |")) {
|
|
67
|
+
throw new Error("terminal markdown renderer left raw markdown syntax");
|
|
68
|
+
}
|
|
69
|
+
if (!renderedMarkdown.includes("Check") || !renderedMarkdown.includes("Present")) {
|
|
70
|
+
throw new Error("terminal markdown renderer dropped table content");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const promptLayout = buildPromptLayout(`${"x".repeat(180)}\nsecond line`, 95, 80, 24);
|
|
74
|
+
const visibleLengths = promptLayout.renderedRows.map((line) =>
|
|
75
|
+
line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").length
|
|
76
|
+
);
|
|
77
|
+
if (promptLayout.rows.length < 4 || Math.max(...visibleLengths) > 79) {
|
|
78
|
+
throw new Error("terminal prompt layout did not wrap long multiline input safely");
|
|
79
|
+
}
|
|
80
|
+
if (promptLayout.cursorRow < 0 || promptLayout.cursorColumn < 0) {
|
|
81
|
+
throw new Error("terminal prompt layout returned an invalid cursor location");
|
|
82
|
+
}
|
|
83
|
+
const hugePromptLayout = buildPromptLayout(Array.from({ length: 30 }, (_unused, index) => `line ${index + 1}`).join("\n"), 120, 80, 20);
|
|
84
|
+
if (hugePromptLayout.renderedRows.length > 12 || !hugePromptLayout.renderedRows.some((line) => line.includes("earlier input row"))) {
|
|
85
|
+
throw new Error("terminal prompt layout did not bound redraw size for large prompts");
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
const result = await runChat("Create notes/interactive.md with a short CLI chat smoke message\n/exit\n");
|
|
56
89
|
const written = await fs.readFile(path.join(tempRoot, "notes/interactive.md"), "utf8");
|
|
57
90
|
if (!written.includes("Created by AgInTiFlow mock mode.")) {
|
|
@@ -74,7 +107,7 @@ try {
|
|
|
74
107
|
{
|
|
75
108
|
ok: true,
|
|
76
109
|
projectRoot: tempRoot,
|
|
77
|
-
checks: ["interactive-chat", "mock-file-write", "run-status", "resume-latest"],
|
|
110
|
+
checks: ["markdown-render", "prompt-layout", "interactive-chat", "mock-file-write", "run-status", "resume-latest"],
|
|
78
111
|
},
|
|
79
112
|
null,
|
|
80
113
|
2
|
package/src/interactive-cli.js
CHANGED
|
@@ -18,6 +18,9 @@ const ansi = {
|
|
|
18
18
|
green: "\x1b[32m",
|
|
19
19
|
yellow: "\x1b[33m",
|
|
20
20
|
red: "\x1b[31m",
|
|
21
|
+
blue: "\x1b[34m",
|
|
22
|
+
magenta: "\x1b[35m",
|
|
23
|
+
faint: "\x1b[2m",
|
|
21
24
|
clearLine: "\x1b[2K",
|
|
22
25
|
cursorHide: "\x1b[?25l",
|
|
23
26
|
cursorShow: "\x1b[?25h",
|
|
@@ -50,6 +53,7 @@ const SLASH_COMMANDS = [
|
|
|
50
53
|
"/web",
|
|
51
54
|
"/exit",
|
|
52
55
|
];
|
|
56
|
+
const promptHistory = [];
|
|
53
57
|
|
|
54
58
|
function color(value, ...codes) {
|
|
55
59
|
if (!useColor || codes.length === 0) return String(value);
|
|
@@ -64,6 +68,39 @@ function label(name, bgCode) {
|
|
|
64
68
|
return color(` ${name} `, bgCode, ansi.bold);
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
function terminalWidth() {
|
|
72
|
+
return Math.max(Number(output.columns) || 80, 40);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function terminalHeight() {
|
|
76
|
+
return Math.max(Number(output.rows) || 24, 10);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function editorWidth(width = terminalWidth()) {
|
|
80
|
+
return Math.max(Number(width) - 1, 39);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function promptViewportRows(height = terminalHeight()) {
|
|
84
|
+
return Math.max(Math.min(Math.floor(Number(height) * 0.42), 10), 4);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function visualLength(value) {
|
|
88
|
+
return stripAnsi(value).length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function padVisible(value, width) {
|
|
92
|
+
const padding = Math.max(width - visualLength(value), 0);
|
|
93
|
+
return `${value}${" ".repeat(padding)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function panelLine(content = "", bgCode = ansi.systemBg, width = editorWidth()) {
|
|
97
|
+
const raw = String(content || "");
|
|
98
|
+
const safeContent = visualLength(raw) > width ? stripAnsi(raw).slice(0, width) : raw;
|
|
99
|
+
if (!useColor) return padVisible(safeContent, width);
|
|
100
|
+
const padded = padVisible(safeContent, width).replaceAll(ansi.reset, `${ansi.reset}${bgCode}`);
|
|
101
|
+
return `${bgCode}${padded}${ansi.reset}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
67
104
|
function userPrompt() {
|
|
68
105
|
return `\n${label("user>", ansi.userBg)} ${color("|", ansi.userBg)} `;
|
|
69
106
|
}
|
|
@@ -90,12 +127,17 @@ function commandSuggestions(line = "") {
|
|
|
90
127
|
return SLASH_COMMANDS.filter((command) => command.startsWith(trimmed)).slice(0, 8);
|
|
91
128
|
}
|
|
92
129
|
|
|
93
|
-
function
|
|
130
|
+
function clamp(value, min, max) {
|
|
131
|
+
return Math.min(Math.max(value, min), max);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function stripMarkdown(text) {
|
|
94
135
|
const lines = String(text || "").split(/\r?\n/);
|
|
95
136
|
let inFence = false;
|
|
96
137
|
const rendered = [];
|
|
97
138
|
|
|
98
|
-
for (
|
|
139
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
140
|
+
const rawLine = lines[index];
|
|
99
141
|
let line = rawLine;
|
|
100
142
|
if (/^\s*```/.test(line)) {
|
|
101
143
|
inFence = !inFence;
|
|
@@ -119,6 +161,12 @@ function stripMarkdown(text) {
|
|
|
119
161
|
if (/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line)) {
|
|
120
162
|
continue;
|
|
121
163
|
}
|
|
164
|
+
const table = parseMarkdownTable(lines, index);
|
|
165
|
+
if (table) {
|
|
166
|
+
rendered.push(...renderMarkdownTable(table));
|
|
167
|
+
index += table.rawLineCount - 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
122
170
|
line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
123
171
|
line = line.replace(/\*\*([^*]+)\*\*/g, (_, value) => color(value, ansi.bold));
|
|
124
172
|
line = line.replace(/__([^_]+)__/g, (_, value) => color(value, ansi.bold));
|
|
@@ -137,6 +185,75 @@ function stripMarkdown(text) {
|
|
|
137
185
|
return rendered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
138
186
|
}
|
|
139
187
|
|
|
188
|
+
function splitMarkdownTableRow(line = "") {
|
|
189
|
+
const trimmed = String(line || "").trim();
|
|
190
|
+
if (!trimmed.includes("|")) return null;
|
|
191
|
+
const normalized = trimmed.replace(/^\|/, "").replace(/\|$/, "");
|
|
192
|
+
const cells = normalized.split("|").map((cell) => cell.trim());
|
|
193
|
+
return cells.length >= 2 ? cells : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isMarkdownTableSeparator(line = "") {
|
|
197
|
+
const cells = splitMarkdownTableRow(line);
|
|
198
|
+
return Boolean(cells?.length) && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseMarkdownTable(lines, startIndex) {
|
|
202
|
+
const header = splitMarkdownTableRow(lines[startIndex]);
|
|
203
|
+
if (!header || !isMarkdownTableSeparator(lines[startIndex + 1] || "")) return null;
|
|
204
|
+
const rows = [];
|
|
205
|
+
let index = startIndex + 2;
|
|
206
|
+
while (index < lines.length) {
|
|
207
|
+
const row = splitMarkdownTableRow(lines[index]);
|
|
208
|
+
if (!row) break;
|
|
209
|
+
rows.push(row);
|
|
210
|
+
index += 1;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
header,
|
|
214
|
+
rows,
|
|
215
|
+
rawLineCount: Math.max(index - startIndex, 2),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderMarkdownTable(table) {
|
|
220
|
+
const allRows = [table.header, ...table.rows];
|
|
221
|
+
const columnCount = Math.max(...allRows.map((row) => row.length));
|
|
222
|
+
const widths = Array.from({ length: columnCount }, (_unused, column) =>
|
|
223
|
+
Math.min(
|
|
224
|
+
Math.max(
|
|
225
|
+
...allRows.map((row) => visualLength(stripMarkdownInline(row[column] || ""))),
|
|
226
|
+
3
|
|
227
|
+
),
|
|
228
|
+
36
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
const formatRow = (row, header = false) =>
|
|
232
|
+
widths
|
|
233
|
+
.map((width, column) => {
|
|
234
|
+
const value = stripMarkdownInline(row[column] || "");
|
|
235
|
+
return padVisible(value, width);
|
|
236
|
+
})
|
|
237
|
+
.join(color(" │ ", ansi.dim));
|
|
238
|
+
const separator = widths.map((width) => "─".repeat(width)).join(color("──┼──", ansi.dim));
|
|
239
|
+
return [
|
|
240
|
+
color(formatRow(table.header, true), ansi.bold, ansi.cyan),
|
|
241
|
+
color(separator, ansi.dim),
|
|
242
|
+
...table.rows.map((row) => formatRow(row)),
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stripMarkdownInline(value = "") {
|
|
247
|
+
return String(value)
|
|
248
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)")
|
|
249
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, text) => color(text, ansi.bold))
|
|
250
|
+
.replace(/__([^_]+)__/g, (_, text) => color(text, ansi.bold))
|
|
251
|
+
.replace(/`([^`]+)`/g, (_, text) => color(text, ansi.yellow))
|
|
252
|
+
.replace(/~~([^~]+)~~/g, "$1")
|
|
253
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
254
|
+
.replace(/_([^_]+)_/g, "$1");
|
|
255
|
+
}
|
|
256
|
+
|
|
140
257
|
function rolePrefix(name, bgCode) {
|
|
141
258
|
return `${label(name, bgCode)} ${color("|", bgCode)} `;
|
|
142
259
|
}
|
|
@@ -183,7 +300,10 @@ async function renderLaunchHeader(packageVersion = "") {
|
|
|
183
300
|
const title = "AgInTi Flow";
|
|
184
301
|
const subtitle = "web-first agent workspace";
|
|
185
302
|
const version = packageVersion ? `v${packageVersion}` : "";
|
|
186
|
-
const
|
|
303
|
+
const width = Math.min(Math.max(terminalWidth() - 2, 58), 82);
|
|
304
|
+
const top = `╭${"─".repeat(width)}╮`;
|
|
305
|
+
const mid = `├${"─".repeat(width)}┤`;
|
|
306
|
+
const bottom = `╰${"─".repeat(width)}╯`;
|
|
187
307
|
|
|
188
308
|
if (!useColor || process.env.AGINTIFLOW_NO_ANIMATION === "1") {
|
|
189
309
|
console.log(` AgInTiFlow ${packageVersion || ""}`.trim());
|
|
@@ -198,11 +318,15 @@ async function renderLaunchHeader(packageVersion = "") {
|
|
|
198
318
|
output.write(`\r${ansi.clearLine}`);
|
|
199
319
|
output.write(ansi.cursorShow);
|
|
200
320
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.log(
|
|
205
|
-
console.log(color(
|
|
321
|
+
const border = "\x1b[38;5;45m";
|
|
322
|
+
const titleLine = `${shimmerText(title, 2)} ${color(version, ansi.dim)}`;
|
|
323
|
+
const tagline = "browser + shell + files + docker + web search + scouts";
|
|
324
|
+
console.log(color(top, border));
|
|
325
|
+
console.log(`${color("│", border)} ${padVisible(titleLine, width - 2)} ${color("│", border)}`);
|
|
326
|
+
console.log(`${color("│", border)} ${color(padVisible(subtitle, width - 2), ansi.dim)} ${color("│", border)}`);
|
|
327
|
+
console.log(color(mid, border));
|
|
328
|
+
console.log(`${color("│", border)} ${color(padVisible(tagline, width - 2), ansi.cyan)} ${color("│", border)}`);
|
|
329
|
+
console.log(color(bottom, border));
|
|
206
330
|
}
|
|
207
331
|
|
|
208
332
|
function printHelp() {
|
|
@@ -238,24 +362,236 @@ function printHelp() {
|
|
|
238
362
|
);
|
|
239
363
|
}
|
|
240
364
|
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
365
|
+
function logicalLinesWithOffsets(buffer = "") {
|
|
366
|
+
const lines = String(buffer).split("\n");
|
|
367
|
+
let offset = 0;
|
|
368
|
+
return lines.map((line, index) => {
|
|
369
|
+
const start = offset;
|
|
370
|
+
const end = start + line.length;
|
|
371
|
+
offset = end + 1;
|
|
372
|
+
return {
|
|
373
|
+
text: line,
|
|
374
|
+
start,
|
|
375
|
+
end,
|
|
376
|
+
hasNewline: index < lines.length - 1,
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function promptVisibleWindow(rows, cursorRow, height = terminalHeight()) {
|
|
382
|
+
const maxRows = promptViewportRows(height);
|
|
383
|
+
if (rows.length <= maxRows) {
|
|
384
|
+
return { start: 0, end: rows.length, topHidden: 0, bottomHidden: 0 };
|
|
245
385
|
}
|
|
386
|
+
const half = Math.floor(maxRows / 2);
|
|
387
|
+
const start = clamp(cursorRow - half, 0, rows.length - maxRows);
|
|
388
|
+
const end = start + maxRows;
|
|
389
|
+
return {
|
|
390
|
+
start,
|
|
391
|
+
end,
|
|
392
|
+
topHidden: start,
|
|
393
|
+
bottomHidden: rows.length - end,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
246
396
|
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
397
|
+
export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight()) {
|
|
398
|
+
const safeBuffer = String(buffer || "");
|
|
399
|
+
const safeCursor = clamp(Number(cursor) || 0, 0, safeBuffer.length);
|
|
400
|
+
const lineWidth = editorWidth(width);
|
|
401
|
+
const firstPrefix = " user ";
|
|
402
|
+
const nextPrefix = " ... ";
|
|
403
|
+
const firstInnerWidth = Math.max(lineWidth - firstPrefix.length, 8);
|
|
404
|
+
const nextInnerWidth = Math.max(lineWidth - nextPrefix.length, 8);
|
|
405
|
+
const rows = [];
|
|
406
|
+
|
|
407
|
+
for (const [lineIndex, line] of logicalLinesWithOffsets(safeBuffer).entries()) {
|
|
408
|
+
let localOffset = 0;
|
|
409
|
+
const text = line.text;
|
|
410
|
+
if (!text) {
|
|
411
|
+
rows.push({
|
|
412
|
+
prefix: lineIndex === 0 ? firstPrefix : nextPrefix,
|
|
413
|
+
text: "",
|
|
414
|
+
start: line.start,
|
|
415
|
+
end: line.start,
|
|
416
|
+
innerWidth: lineIndex === 0 ? firstInnerWidth : nextInnerWidth,
|
|
417
|
+
lineStart: line.start,
|
|
418
|
+
lineEnd: line.end,
|
|
419
|
+
hasNewline: line.hasNewline,
|
|
420
|
+
});
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
while (localOffset < text.length) {
|
|
425
|
+
const prefix = lineIndex === 0 && localOffset === 0 ? firstPrefix : nextPrefix;
|
|
426
|
+
const innerWidth = prefix === firstPrefix ? firstInnerWidth : nextInnerWidth;
|
|
427
|
+
const chunk = text.slice(localOffset, localOffset + innerWidth);
|
|
428
|
+
rows.push({
|
|
429
|
+
prefix,
|
|
430
|
+
text: chunk,
|
|
431
|
+
start: line.start + localOffset,
|
|
432
|
+
end: line.start + localOffset + chunk.length,
|
|
433
|
+
innerWidth,
|
|
434
|
+
lineStart: line.start,
|
|
435
|
+
lineEnd: line.end,
|
|
436
|
+
hasNewline: line.hasNewline,
|
|
437
|
+
});
|
|
438
|
+
localOffset += chunk.length;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (rows.length === 0) {
|
|
443
|
+
rows.push({
|
|
444
|
+
prefix: firstPrefix,
|
|
445
|
+
text: "",
|
|
446
|
+
start: 0,
|
|
447
|
+
end: 0,
|
|
448
|
+
innerWidth: firstInnerWidth,
|
|
449
|
+
lineStart: 0,
|
|
450
|
+
lineEnd: 0,
|
|
451
|
+
hasNewline: false,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const last = rows[rows.length - 1];
|
|
456
|
+
if (safeCursor === safeBuffer.length && last.end === safeCursor && last.text.length >= last.innerWidth) {
|
|
457
|
+
rows.push({
|
|
458
|
+
prefix: nextPrefix,
|
|
459
|
+
text: "",
|
|
460
|
+
start: safeCursor,
|
|
461
|
+
end: safeCursor,
|
|
462
|
+
innerWidth: nextInnerWidth,
|
|
463
|
+
lineStart: safeCursor,
|
|
464
|
+
lineEnd: safeCursor,
|
|
465
|
+
hasNewline: false,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let cursorRow = rows.length - 1;
|
|
470
|
+
let cursorColumn = rows[cursorRow].prefix.length;
|
|
471
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
472
|
+
const row = rows[index];
|
|
473
|
+
const next = rows[index + 1];
|
|
474
|
+
if (safeCursor < row.end) {
|
|
475
|
+
cursorRow = index;
|
|
476
|
+
cursorColumn = row.prefix.length + safeCursor - row.start;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
if (safeCursor === row.end) {
|
|
480
|
+
if (next && next.start === safeCursor && row.text.length >= row.innerWidth) continue;
|
|
481
|
+
cursorRow = index;
|
|
482
|
+
cursorColumn = row.prefix.length + safeCursor - row.start;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
253
485
|
}
|
|
486
|
+
|
|
487
|
+
const suggestions = commandSuggestions(safeBuffer.split("\n")[0] || "");
|
|
488
|
+
const emptyHint = "type a request, /help, Enter to send, Ctrl+J for newline";
|
|
489
|
+
const visible = promptVisibleWindow(rows, cursorRow, height);
|
|
490
|
+
const renderedRows = [];
|
|
491
|
+
let renderedCursorRow = cursorRow - visible.start;
|
|
492
|
+
|
|
493
|
+
if (visible.topHidden > 0) {
|
|
494
|
+
renderedRows.push(panelLine(` ... ${visible.topHidden} earlier input row${visible.topHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
|
|
495
|
+
renderedCursorRow += 1;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (const row of rows.slice(visible.start, visible.end)) {
|
|
499
|
+
const content = safeBuffer ? `${row.prefix}${row.text}` : `${row.prefix}${emptyHint}`;
|
|
500
|
+
renderedRows.push(panelLine(content, ansi.userBg, lineWidth));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (visible.bottomHidden > 0) {
|
|
504
|
+
renderedRows.push(panelLine(` ... ${visible.bottomHidden} later input row${visible.bottomHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
|
|
505
|
+
}
|
|
506
|
+
|
|
254
507
|
if (suggestions.length > 0) {
|
|
255
|
-
|
|
508
|
+
renderedRows.push(panelLine(` hint ${suggestions.join(" ")}`, ansi.systemBg, lineWidth));
|
|
256
509
|
}
|
|
257
|
-
|
|
258
|
-
return
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
rows,
|
|
513
|
+
renderedRows,
|
|
514
|
+
cursorRow: renderedCursorRow,
|
|
515
|
+
absoluteCursorRow: cursorRow,
|
|
516
|
+
cursorColumn: clamp(cursorColumn, 0, editorWidth(width) - 1),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function cursorLocation(layout, cursor) {
|
|
521
|
+
for (let index = 0; index < layout.rows.length; index += 1) {
|
|
522
|
+
const row = layout.rows[index];
|
|
523
|
+
const next = layout.rows[index + 1];
|
|
524
|
+
if (cursor < row.end) return { rowIndex: index, column: cursor - row.start };
|
|
525
|
+
if (cursor === row.end) {
|
|
526
|
+
if (next && next.start === cursor && row.text.length >= row.innerWidth) continue;
|
|
527
|
+
return { rowIndex: index, column: cursor - row.start };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const rowIndex = Math.max(layout.rows.length - 1, 0);
|
|
531
|
+
const row = layout.rows[rowIndex];
|
|
532
|
+
return { rowIndex, column: Math.max(row.end - row.start, 0) };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function clearRenderedPrompt(previous) {
|
|
536
|
+
if (!previous.lineCount) return;
|
|
537
|
+
const below = previous.lineCount - 1 - previous.cursorRow;
|
|
538
|
+
if (below > 0) output.write(`\x1b[${below}B`);
|
|
539
|
+
output.write(`\r${ansi.clearLine}`);
|
|
540
|
+
for (let index = 1; index < previous.lineCount; index += 1) {
|
|
541
|
+
output.write(`\x1b[1A\r${ansi.clearLine}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0 }) {
|
|
546
|
+
output.write(ansi.cursorHide);
|
|
547
|
+
clearRenderedPrompt(previous);
|
|
548
|
+
const layout = buildPromptLayout(buffer, cursor);
|
|
549
|
+
output.write(layout.renderedRows.join("\n"));
|
|
550
|
+
const below = layout.renderedRows.length - 1 - layout.cursorRow;
|
|
551
|
+
if (below > 0) output.write(`\x1b[${below}A`);
|
|
552
|
+
output.write(`\r\x1b[${layout.cursorColumn + 1}G`);
|
|
553
|
+
output.write(ansi.cursorShow);
|
|
554
|
+
return {
|
|
555
|
+
lineCount: layout.renderedRows.length,
|
|
556
|
+
cursorRow: layout.cursorRow,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function moveToPromptBottom(rendered) {
|
|
561
|
+
const below = Math.max((rendered?.lineCount || 1) - 1 - (rendered?.cursorRow || 0), 0);
|
|
562
|
+
if (below > 0) output.write(`\x1b[${below}B`);
|
|
563
|
+
output.write("\r");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function lineBounds(buffer, cursor) {
|
|
567
|
+
const safeCursor = clamp(cursor, 0, buffer.length);
|
|
568
|
+
const start = buffer.lastIndexOf("\n", safeCursor - 1) + 1;
|
|
569
|
+
const nextNewline = buffer.indexOf("\n", safeCursor);
|
|
570
|
+
const end = nextNewline === -1 ? buffer.length : nextNewline;
|
|
571
|
+
return { start, end };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function insertAt(buffer, cursor, text) {
|
|
575
|
+
return {
|
|
576
|
+
buffer: `${buffer.slice(0, cursor)}${text}${buffer.slice(cursor)}`,
|
|
577
|
+
cursor: cursor + text.length,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function removeBefore(buffer, cursor) {
|
|
582
|
+
if (cursor <= 0) return { buffer, cursor };
|
|
583
|
+
return {
|
|
584
|
+
buffer: `${buffer.slice(0, cursor - 1)}${buffer.slice(cursor)}`,
|
|
585
|
+
cursor: cursor - 1,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function removeAt(buffer, cursor) {
|
|
590
|
+
if (cursor >= buffer.length) return { buffer, cursor };
|
|
591
|
+
return {
|
|
592
|
+
buffer: `${buffer.slice(0, cursor)}${buffer.slice(cursor + 1)}`,
|
|
593
|
+
cursor,
|
|
594
|
+
};
|
|
259
595
|
}
|
|
260
596
|
|
|
261
597
|
function createAbortError(message = "Aborted with Ctrl+C") {
|
|
@@ -270,34 +606,97 @@ function readTtyPrompt() {
|
|
|
270
606
|
emitKeypressEvents(input);
|
|
271
607
|
const wasRaw = Boolean(input.isRaw);
|
|
272
608
|
let buffer = "";
|
|
273
|
-
let
|
|
609
|
+
let cursor = 0;
|
|
610
|
+
let rendered = { lineCount: 0, cursorRow: 0 };
|
|
611
|
+
let preferredColumn = null;
|
|
612
|
+
let historyIndex = promptHistory.length;
|
|
613
|
+
let draft = "";
|
|
614
|
+
let redrawHandle = null;
|
|
274
615
|
|
|
275
616
|
const cleanup = () => {
|
|
617
|
+
if (redrawHandle) {
|
|
618
|
+
clearImmediate(redrawHandle);
|
|
619
|
+
redrawHandle = null;
|
|
620
|
+
}
|
|
276
621
|
input.off("keypress", handler);
|
|
277
622
|
if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
|
|
278
623
|
input.pause();
|
|
279
624
|
output.write(ansi.cursorShow);
|
|
280
625
|
};
|
|
281
626
|
|
|
627
|
+
const renderNow = () => {
|
|
628
|
+
if (redrawHandle) {
|
|
629
|
+
clearImmediate(redrawHandle);
|
|
630
|
+
redrawHandle = null;
|
|
631
|
+
}
|
|
632
|
+
rendered = renderPromptBuffer(buffer, cursor, rendered);
|
|
633
|
+
};
|
|
634
|
+
|
|
282
635
|
const redraw = () => {
|
|
283
|
-
|
|
636
|
+
if (redrawHandle) return;
|
|
637
|
+
redrawHandle = setImmediate(() => {
|
|
638
|
+
redrawHandle = null;
|
|
639
|
+
rendered = renderPromptBuffer(buffer, cursor, rendered);
|
|
640
|
+
});
|
|
284
641
|
};
|
|
285
642
|
|
|
286
643
|
const submit = () => {
|
|
644
|
+
renderNow();
|
|
645
|
+
moveToPromptBottom(rendered);
|
|
287
646
|
cleanup();
|
|
288
647
|
output.write("\n");
|
|
648
|
+
const saved = buffer.trim();
|
|
649
|
+
if (saved && promptHistory[promptHistory.length - 1] !== buffer) promptHistory.push(buffer);
|
|
289
650
|
resolve(buffer);
|
|
290
651
|
};
|
|
291
652
|
|
|
653
|
+
const setBuffer = (nextBuffer, nextCursor = nextBuffer.length) => {
|
|
654
|
+
buffer = nextBuffer;
|
|
655
|
+
cursor = clamp(nextCursor, 0, buffer.length);
|
|
656
|
+
preferredColumn = null;
|
|
657
|
+
redraw();
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const moveVertical = (direction) => {
|
|
661
|
+
const layout = buildPromptLayout(buffer, cursor);
|
|
662
|
+
const location = cursorLocation(layout, cursor);
|
|
663
|
+
const targetRowIndex = location.rowIndex + direction;
|
|
664
|
+
if (targetRowIndex < 0) {
|
|
665
|
+
if (promptHistory.length === 0) return;
|
|
666
|
+
if (historyIndex === promptHistory.length) draft = buffer;
|
|
667
|
+
historyIndex = Math.max(historyIndex - 1, 0);
|
|
668
|
+
setBuffer(promptHistory[historyIndex], promptHistory[historyIndex].length);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (targetRowIndex >= layout.rows.length) {
|
|
672
|
+
if (historyIndex < promptHistory.length - 1) {
|
|
673
|
+
historyIndex += 1;
|
|
674
|
+
setBuffer(promptHistory[historyIndex], promptHistory[historyIndex].length);
|
|
675
|
+
} else if (historyIndex < promptHistory.length) {
|
|
676
|
+
historyIndex = promptHistory.length;
|
|
677
|
+
setBuffer(draft, draft.length);
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const currentColumn = preferredColumn ?? location.column;
|
|
682
|
+
const targetRow = layout.rows[targetRowIndex];
|
|
683
|
+
cursor = targetRow.start + Math.min(currentColumn, targetRow.end - targetRow.start);
|
|
684
|
+
preferredColumn = currentColumn;
|
|
685
|
+
redraw();
|
|
686
|
+
};
|
|
687
|
+
|
|
292
688
|
const handler = (str = "", key = {}) => {
|
|
293
689
|
if (key.ctrl && key.name === "c") {
|
|
690
|
+
renderNow();
|
|
691
|
+
moveToPromptBottom(rendered);
|
|
294
692
|
cleanup();
|
|
295
693
|
output.write("\n");
|
|
296
694
|
reject(createAbortError());
|
|
297
695
|
return;
|
|
298
696
|
}
|
|
299
|
-
if ((key.ctrl && key.name === "j") ||
|
|
300
|
-
buffer
|
|
697
|
+
if ((key.ctrl && key.name === "j") || key.sequence === "\n") {
|
|
698
|
+
({ buffer, cursor } = insertAt(buffer, cursor, "\n"));
|
|
699
|
+
preferredColumn = null;
|
|
301
700
|
redraw();
|
|
302
701
|
return;
|
|
303
702
|
}
|
|
@@ -306,7 +705,61 @@ function readTtyPrompt() {
|
|
|
306
705
|
return;
|
|
307
706
|
}
|
|
308
707
|
if (key.name === "backspace") {
|
|
309
|
-
buffer = buffer
|
|
708
|
+
({ buffer, cursor } = removeBefore(buffer, cursor));
|
|
709
|
+
preferredColumn = null;
|
|
710
|
+
redraw();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (key.name === "delete") {
|
|
714
|
+
({ buffer, cursor } = removeAt(buffer, cursor));
|
|
715
|
+
preferredColumn = null;
|
|
716
|
+
redraw();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (key.name === "left") {
|
|
720
|
+
cursor = Math.max(cursor - 1, 0);
|
|
721
|
+
preferredColumn = null;
|
|
722
|
+
redraw();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (key.name === "right") {
|
|
726
|
+
cursor = Math.min(cursor + 1, buffer.length);
|
|
727
|
+
preferredColumn = null;
|
|
728
|
+
redraw();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (key.name === "up") {
|
|
732
|
+
moveVertical(-1);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (key.name === "down") {
|
|
736
|
+
moveVertical(1);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if ((key.ctrl && key.name === "a") || key.name === "home") {
|
|
740
|
+
cursor = lineBounds(buffer, cursor).start;
|
|
741
|
+
preferredColumn = null;
|
|
742
|
+
redraw();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if ((key.ctrl && key.name === "e") || key.name === "end") {
|
|
746
|
+
cursor = lineBounds(buffer, cursor).end;
|
|
747
|
+
preferredColumn = null;
|
|
748
|
+
redraw();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (key.ctrl && key.name === "u") {
|
|
752
|
+
const bounds = lineBounds(buffer, cursor);
|
|
753
|
+
buffer = `${buffer.slice(0, bounds.start)}${buffer.slice(cursor)}`;
|
|
754
|
+
cursor = bounds.start;
|
|
755
|
+
preferredColumn = null;
|
|
756
|
+
redraw();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (key.ctrl && key.name === "k") {
|
|
760
|
+
const bounds = lineBounds(buffer, cursor);
|
|
761
|
+
buffer = `${buffer.slice(0, cursor)}${buffer.slice(bounds.end)}`;
|
|
762
|
+
preferredColumn = null;
|
|
310
763
|
redraw();
|
|
311
764
|
return;
|
|
312
765
|
}
|
|
@@ -314,27 +767,32 @@ function readTtyPrompt() {
|
|
|
314
767
|
const suggestions = commandSuggestions(buffer.split("\n")[0] || "");
|
|
315
768
|
if (suggestions.length === 1) {
|
|
316
769
|
buffer = suggestions[0];
|
|
770
|
+
cursor = buffer.length;
|
|
317
771
|
}
|
|
772
|
+
preferredColumn = null;
|
|
318
773
|
redraw();
|
|
319
774
|
return;
|
|
320
775
|
}
|
|
321
776
|
if (key.name === "escape") {
|
|
322
777
|
buffer = "";
|
|
778
|
+
cursor = 0;
|
|
779
|
+
preferredColumn = null;
|
|
323
780
|
redraw();
|
|
324
781
|
return;
|
|
325
782
|
}
|
|
326
783
|
if (key.ctrl || key.meta) return;
|
|
327
784
|
if (str && !key.sequence?.startsWith("\x1b")) {
|
|
328
|
-
|
|
785
|
+
const text = str.replace(/\r/g, "");
|
|
786
|
+
({ buffer, cursor } = insertAt(buffer, cursor, text));
|
|
787
|
+
preferredColumn = null;
|
|
329
788
|
redraw();
|
|
330
789
|
}
|
|
331
790
|
};
|
|
332
791
|
|
|
333
792
|
input.resume();
|
|
334
793
|
input.setRawMode(true);
|
|
335
|
-
output.write(ansi.cursorHide);
|
|
336
794
|
input.on("keypress", handler);
|
|
337
|
-
|
|
795
|
+
renderNow();
|
|
338
796
|
});
|
|
339
797
|
}
|
|
340
798
|
|