@rind-ai/cli 0.4.1 → 0.6.0
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/bin/rind.js +5 -5
- package/lib/assistant-renderer.js +179 -265
- package/lib/choice-menu-state.js +46 -46
- package/lib/cli-input-actions.js +548 -0
- package/lib/cli-output-controller.js +460 -0
- package/lib/cli-runtime-controller.js +350 -0
- package/lib/cli-state-store.js +32 -0
- package/lib/cli-state.js +41 -0
- package/lib/command-controller.js +159 -126
- package/lib/compact-context-state.js +22 -22
- package/lib/components/assistant-message.js +169 -0
- package/lib/components/composer-area.js +25 -0
- package/lib/components/dynamic-block.js +20 -0
- package/lib/components/monitor-stack.js +35 -0
- package/lib/components/text-block.js +47 -0
- package/lib/components/tool-block.js +122 -0
- package/lib/composer-terminal.js +224 -203
- package/lib/event-controller.js +243 -242
- package/lib/frontend-cli-implementation.js +656 -1111
- package/lib/input-controller.js +75 -94
- package/lib/input-errors.js +3 -3
- package/lib/interrupt-state.js +9 -9
- package/lib/line-editor.js +541 -541
- package/lib/local-slash-commands.js +217 -0
- package/lib/markdown-lines.js +103 -0
- package/lib/model-menu-state.js +50 -50
- package/lib/one-shot-progress.js +145 -0
- package/lib/one-shot.js +228 -0
- package/lib/question-menu-state.js +61 -0
- package/lib/rendering.js +1309 -1060
- package/lib/runtime-client.js +241 -193
- package/lib/runtime-env.js +21 -21
- package/lib/runtime-protocol.js +122 -15
- package/lib/slash-command-mode.js +0 -11
- package/lib/slash-menu-state.js +59 -59
- package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
- package/lib/terminal-key.js +97 -97
- package/lib/text-width.js +335 -151
- package/lib/theme-menu-state.js +31 -0
- package/lib/theme.js +134 -0
- package/lib/tool-display.js +675 -0
- package/lib/tui/component.js +55 -0
- package/lib/tui/cursor.js +29 -0
- package/lib/tui/input-buffer.js +172 -0
- package/lib/tui/tui.js +591 -0
- package/lib/turn-controller.js +68 -78
- package/package.json +28 -28
- package/lib/assistant-stream-buffer.js +0 -25
- package/lib/terminal-ui.js +0 -581
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseToolArguments,
|
|
3
|
+
renderToolFinished,
|
|
4
|
+
renderToolRunning,
|
|
5
|
+
} from "../tool-display.js";
|
|
6
|
+
|
|
7
|
+
const TICKER_TOOLS = new Set(["bash", "bash_output", "delegate", "search_web", "fetch_web_page"]);
|
|
8
|
+
|
|
9
|
+
export class ToolBlock {
|
|
10
|
+
constructor({ event, onRequestRender, leading = false }) {
|
|
11
|
+
this.name = event?.tool_name || "tool";
|
|
12
|
+
this.args = parseToolArguments(event);
|
|
13
|
+
this.phase = "running";
|
|
14
|
+
this.startedAt = Date.now();
|
|
15
|
+
this.progressMessage = "";
|
|
16
|
+
this.fileChange = null;
|
|
17
|
+
this.resultEvent = null;
|
|
18
|
+
this.expanded = false;
|
|
19
|
+
this.leading = Boolean(leading);
|
|
20
|
+
this.timer = null;
|
|
21
|
+
this.onRequestRender = onRequestRender;
|
|
22
|
+
if (TICKER_TOOLS.has(this.name)) {
|
|
23
|
+
this.timer = setInterval(() => {
|
|
24
|
+
this.onRequestRender?.();
|
|
25
|
+
}, 1000);
|
|
26
|
+
this.timer.unref?.();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setProgress(message) {
|
|
31
|
+
if (this.phase !== "running") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const next = String(message ?? "");
|
|
35
|
+
if (next === this.progressMessage) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.progressMessage = next;
|
|
39
|
+
this.onRequestRender?.();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The runtime streams tool_input_started (id+name only) before
|
|
43
|
+
// tool_requested (full parsed arguments); merge late-arriving args into
|
|
44
|
+
// an already-created block so titles gain their command/path/etc.
|
|
45
|
+
enrichArgs(event) {
|
|
46
|
+
const incoming = parseToolArguments(event);
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
49
|
+
const current = this.args?.[key];
|
|
50
|
+
if ((current === undefined || current === null || current === "") && value !== undefined && value !== null && value !== "") {
|
|
51
|
+
if (!this.args || typeof this.args !== "object") {
|
|
52
|
+
this.args = {};
|
|
53
|
+
}
|
|
54
|
+
this.args[key] = value;
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (changed) {
|
|
59
|
+
this.onRequestRender?.();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
finish(event, fileChange) {
|
|
64
|
+
this.phase = "done";
|
|
65
|
+
this.resultEvent = event || this.resultEvent || { status: "completed", result: "" };
|
|
66
|
+
this.fileChange = fileChange || null;
|
|
67
|
+
this.clearTimer();
|
|
68
|
+
this.onRequestRender?.();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setExpanded(expanded) {
|
|
72
|
+
const next = Boolean(expanded);
|
|
73
|
+
if (this.expanded === next) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.expanded = next;
|
|
77
|
+
this.onRequestRender?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get isRunning() {
|
|
81
|
+
return this.phase === "running";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clearTimer() {
|
|
85
|
+
if (!this.timer) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
clearInterval(this.timer);
|
|
89
|
+
this.timer = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
invalidate() {
|
|
93
|
+
// No cached render state (blocks restyle every frame); keep the
|
|
94
|
+
// elapsed-time ticker alive across full repaints.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(width) {
|
|
98
|
+
let lines;
|
|
99
|
+
if (this.phase === "running") {
|
|
100
|
+
lines = renderToolRunning({
|
|
101
|
+
name: this.name,
|
|
102
|
+
args: this.args,
|
|
103
|
+
phase: "running",
|
|
104
|
+
elapsedMs: Date.now() - this.startedAt,
|
|
105
|
+
progressMessage: this.progressMessage,
|
|
106
|
+
}, width);
|
|
107
|
+
} else {
|
|
108
|
+
lines = renderToolFinished({
|
|
109
|
+
name: this.name,
|
|
110
|
+
args: this.args,
|
|
111
|
+
phase: "done",
|
|
112
|
+
expanded: this.expanded,
|
|
113
|
+
event: this.resultEvent,
|
|
114
|
+
fileChange: this.fileChange,
|
|
115
|
+
}, width);
|
|
116
|
+
}
|
|
117
|
+
if (this.leading && lines.length) {
|
|
118
|
+
return ["", ...lines];
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/lib/composer-terminal.js
CHANGED
|
@@ -1,203 +1,224 @@
|
|
|
1
|
-
import { graphemes, stripAnsi, textWidth, wrapTextCells } from "./text-width.js";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_COLUMNS = 80;
|
|
4
|
-
const INPUT_MARKER = "\n ▷ ";
|
|
5
|
-
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
6
|
-
const SGR_SEQUENCE = /^\x1b\[([0-9;]*)m$/;
|
|
7
|
-
|
|
8
|
-
export function prepareComposerFrame(frame = {}, columns = DEFAULT_COLUMNS) {
|
|
9
|
-
const width = terminalColumns(columns);
|
|
10
|
-
const block = splitPromptBlock(frame.prompt);
|
|
11
|
-
const input = String(frame.inputText || "");
|
|
12
|
-
const display = input || String(frame.placeholder || "");
|
|
13
|
-
const position = cursorPosition(frame, input);
|
|
14
|
-
const leadingLines = wrapRenderedLines(block.leading, width);
|
|
15
|
-
const inputLines = wrapInputLines(block.prefix, display, width);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
1
|
+
import { graphemes, stripAnsi, textWidth, wrapTextCells } from "./text-width.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_COLUMNS = 80;
|
|
4
|
+
const INPUT_MARKER = "\n ▷ ";
|
|
5
|
+
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
6
|
+
const SGR_SEQUENCE = /^\x1b\[([0-9;]*)m$/;
|
|
7
|
+
|
|
8
|
+
export function prepareComposerFrame(frame = {}, columns = DEFAULT_COLUMNS) {
|
|
9
|
+
const width = terminalColumns(columns);
|
|
10
|
+
const block = splitPromptBlock(frame.prompt);
|
|
11
|
+
const input = String(frame.inputText || "");
|
|
12
|
+
const display = input || String(frame.placeholder || "");
|
|
13
|
+
const position = cursorPosition(frame, input);
|
|
14
|
+
const leadingLines = wrapRenderedLines(block.leading, width);
|
|
15
|
+
const inputLines = wrapInputLines(block.prefix, display, width);
|
|
16
|
+
const menuLines = wrapRenderedLines(frame.menuText, width);
|
|
17
|
+
const menuCursor = frame.menuCursor
|
|
18
|
+
? resolveMenuCursor(frame.menuCursor, frame.menuText, width)
|
|
19
|
+
: null;
|
|
20
|
+
const cursor = menuCursor || visualCursor(block.prefix, input, position, width, inputLines);
|
|
21
|
+
const trailingLines = wrapRenderedLines(block.trailing, width);
|
|
22
|
+
const lines = [...leadingLines, ...inputLines, ...menuLines, ...trailingLines];
|
|
23
|
+
const cursorRow = menuCursor
|
|
24
|
+
? leadingLines.length + inputLines.length + cursor.row
|
|
25
|
+
: leadingLines.length + cursor.row;
|
|
26
|
+
const selectedMenuRow = menuLines.findIndex((line) => stripAnsi(line).startsWith(" › "));
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
lines,
|
|
30
|
+
cursorRow,
|
|
31
|
+
cursorColumn: cursor.column,
|
|
32
|
+
focusRow: selectedMenuRow === -1
|
|
33
|
+
? cursorRow
|
|
34
|
+
: leadingLines.length + inputLines.length + selectedMenuRow,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveMenuCursor(cursor, menuText, columns) {
|
|
39
|
+
const targetLine = Math.max(0, Math.floor(Number(cursor.line) || 0));
|
|
40
|
+
const rawLines = String(menuText || "").split("\n");
|
|
41
|
+
if (rawLines.at(-1) === "") {
|
|
42
|
+
rawLines.pop();
|
|
43
|
+
}
|
|
44
|
+
let row = 0;
|
|
45
|
+
for (let index = 0; index < targetLine; index += 1) {
|
|
46
|
+
row += wrapRenderedLines(rawLines[index] || "", columns).length;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
row,
|
|
50
|
+
column: Math.max(0, Math.floor(Number(cursor.column) || 0)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wrapInputLines(prefix, text, columns) {
|
|
55
|
+
const logicalLines = String(text || "").split("\n");
|
|
56
|
+
const prefixWidth = textWidth(prefix);
|
|
57
|
+
const continuationPrefix = " ".repeat(prefixWidth);
|
|
58
|
+
const contentWidth = Math.max(1, columns - prefixWidth);
|
|
59
|
+
const lines = [];
|
|
60
|
+
for (const [index, logicalLine] of logicalLines.entries()) {
|
|
61
|
+
const linePrefix = index === 0 ? prefix : continuationPrefix;
|
|
62
|
+
if (logicalLine.includes("\x1b")) {
|
|
63
|
+
lines.push(...wrapLine(
|
|
64
|
+
linePrefix,
|
|
65
|
+
continuationPrefix,
|
|
66
|
+
logicalLine,
|
|
67
|
+
contentWidth,
|
|
68
|
+
));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const chunks = wrapTextCells(logicalLine, contentWidth, contentWidth);
|
|
72
|
+
lines.push(...chunks.map((chunk, chunkIndex) => (
|
|
73
|
+
`${chunkIndex === 0 ? linePrefix : continuationPrefix}${chunk.text}`
|
|
74
|
+
)));
|
|
75
|
+
}
|
|
76
|
+
return lines.length ? lines : [String(prefix || "")];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function visualCursor(prefix, input, position, columns, inputLines) {
|
|
80
|
+
const logicalLines = String(input).split("\n");
|
|
81
|
+
const prefixWidth = textWidth(prefix);
|
|
82
|
+
const continuationPrefix = " ".repeat(prefixWidth);
|
|
83
|
+
const contentWidth = Math.max(1, columns - prefixWidth);
|
|
84
|
+
const visualLines = [];
|
|
85
|
+
for (const [lineIndex, logicalLine] of logicalLines.entries()) {
|
|
86
|
+
const chunks = wrapTextCells(logicalLine, contentWidth, contentWidth);
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
visualLines.push({
|
|
89
|
+
startColumn: chunk.startColumn,
|
|
90
|
+
length: chunk.length,
|
|
91
|
+
allowsEnd: chunk.allowsEnd,
|
|
92
|
+
line: lineIndex,
|
|
93
|
+
prefixWidth,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const lastChunk = chunks.at(-1);
|
|
97
|
+
if (lineIndex === logicalLines.length - 1 && !lastChunk.allowsEnd) {
|
|
98
|
+
visualLines.push({
|
|
99
|
+
startColumn: lastChunk.startColumn + lastChunk.length,
|
|
100
|
+
length: 0,
|
|
101
|
+
allowsEnd: true,
|
|
102
|
+
line: lineIndex,
|
|
103
|
+
prefixWidth,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let row = visualLines.length - 1;
|
|
109
|
+
for (const [index, visualLine] of visualLines.entries()) {
|
|
110
|
+
if (visualLine.line !== position.line) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const offset = position.column - visualLine.startColumn;
|
|
114
|
+
if (offset >= 0 && (offset < visualLine.length || (visualLine.allowsEnd && offset === visualLine.length))) {
|
|
115
|
+
row = index;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
while (inputLines.length <= row) {
|
|
120
|
+
inputLines.push(continuationPrefix);
|
|
121
|
+
}
|
|
122
|
+
const visualLine = visualLines[row];
|
|
123
|
+
const text = graphemes(logicalLines[position.line])
|
|
124
|
+
.slice(visualLine.startColumn, position.column)
|
|
125
|
+
.join("");
|
|
126
|
+
return {
|
|
127
|
+
row,
|
|
128
|
+
column: visualLine.prefixWidth + textWidth(text),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function wrapLine(prefix, continuationPrefix, text, contentWidth) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
let line = "";
|
|
135
|
+
let width = 0;
|
|
136
|
+
let activeStyle = "";
|
|
137
|
+
for (const segment of displaySegments(text)) {
|
|
138
|
+
const sgr = segment.match(SGR_SEQUENCE);
|
|
139
|
+
if (sgr) {
|
|
140
|
+
line += segment;
|
|
141
|
+
const codes = (sgr[1] || "0").split(";").map((code) => code || "0");
|
|
142
|
+
const resetIndex = codes.lastIndexOf("0");
|
|
143
|
+
activeStyle = resetIndex === codes.length - 1
|
|
144
|
+
? ""
|
|
145
|
+
: resetIndex >= 0
|
|
146
|
+
? segment
|
|
147
|
+
: `${activeStyle}${segment}`;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const segmentWidth = textWidth(segment);
|
|
151
|
+
if (width > 0 && width + segmentWidth > contentWidth) {
|
|
152
|
+
lines.push(`${lines.length ? continuationPrefix : prefix}${line}`);
|
|
153
|
+
line = activeStyle;
|
|
154
|
+
width = 0;
|
|
155
|
+
}
|
|
156
|
+
line += segment;
|
|
157
|
+
width += segmentWidth;
|
|
158
|
+
}
|
|
159
|
+
lines.push(`${lines.length ? continuationPrefix : prefix}${line}`);
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function displaySegments(value) {
|
|
164
|
+
const text = String(value || "");
|
|
165
|
+
const segments = [];
|
|
166
|
+
let position = 0;
|
|
167
|
+
for (const match of text.matchAll(ANSI_SEQUENCE)) {
|
|
168
|
+
segments.push(...graphemes(text.slice(position, match.index)), match[0]);
|
|
169
|
+
position = match.index + match[0].length;
|
|
170
|
+
}
|
|
171
|
+
segments.push(...graphemes(text.slice(position)));
|
|
172
|
+
return segments;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cursorPosition(frame, input) {
|
|
176
|
+
const lines = String(input).split("\n");
|
|
177
|
+
const line = Math.min(lines.length - 1, Math.max(0, Math.floor(Number(frame.cursor?.line) || 0)));
|
|
178
|
+
return {
|
|
179
|
+
line,
|
|
180
|
+
column: Math.min(
|
|
181
|
+
graphemes(lines[line]).length,
|
|
182
|
+
Math.max(0, Math.floor(Number(frame.cursor?.column) || 0)),
|
|
183
|
+
),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function terminalColumns(columns) {
|
|
188
|
+
const value = Number(columns);
|
|
189
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_COLUMNS;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function splitPromptBlock(prompt) {
|
|
193
|
+
const text = String(prompt || "");
|
|
194
|
+
const index = text.lastIndexOf(INPUT_MARKER);
|
|
195
|
+
if (index === -1) {
|
|
196
|
+
return { leading: "", prefix: text, trailing: "" };
|
|
197
|
+
}
|
|
198
|
+
const inputStart = index + 1;
|
|
199
|
+
const trailingStart = text.indexOf("\n", inputStart);
|
|
200
|
+
if (trailingStart === -1) {
|
|
201
|
+
return {
|
|
202
|
+
leading: text.slice(0, inputStart),
|
|
203
|
+
prefix: text.slice(inputStart),
|
|
204
|
+
trailing: "",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
leading: text.slice(0, inputStart),
|
|
209
|
+
prefix: text.slice(inputStart, trailingStart),
|
|
210
|
+
trailing: text.slice(trailingStart + 1),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function wrapRenderedLines(text, columns) {
|
|
215
|
+
const value = String(text || "");
|
|
216
|
+
if (!value) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const lines = value.split("\n");
|
|
220
|
+
if (lines.at(-1) === "") {
|
|
221
|
+
lines.pop();
|
|
222
|
+
}
|
|
223
|
+
return lines.flatMap((line) => wrapLine("", "", line, columns));
|
|
224
|
+
}
|