@rind-ai/cli 0.6.1 → 0.7.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 +2 -2
- package/lib/assistant-renderer.js +42 -15
- package/lib/cli-input-actions.js +133 -19
- package/lib/cli-output-controller.js +82 -94
- package/lib/cli-runtime-controller.js +155 -8
- package/lib/cli-state.js +2 -2
- package/lib/command-controller.js +34 -0
- package/lib/components/assistant-message.js +54 -21
- package/lib/composer-terminal.js +1 -1
- package/lib/event-controller.js +9 -11
- package/lib/frontend-cli-implementation.js +108 -18
- package/lib/ipc.js +186 -0
- package/lib/line-editor.js +29 -4
- package/lib/local-slash-commands.js +2 -0
- package/lib/markdown-lines.js +142 -0
- package/lib/one-shot-progress.js +145 -145
- package/lib/one-shot.js +3 -3
- package/lib/prompt-history-store.js +54 -0
- package/lib/question-menu-state.js +10 -0
- package/lib/rendering.js +466 -102
- package/lib/runtime-client.js +12 -13
- package/lib/runtime-protocol.js +5 -0
- package/lib/send.js +53 -0
- package/lib/task-monitor-controller.js +45 -45
- package/lib/terminal-key.js +47 -3
- package/lib/text-width.js +2 -19
- package/lib/tool-display.js +18 -23
- package/lib/tui/cursor.js +17 -8
- package/lib/tui/input-buffer.js +69 -28
- package/lib/tui/tui.js +128 -30
- package/lib/turn-controller.js +13 -4
- package/package.json +5 -5
- package/lib/frontend-cli.js +0 -5
package/lib/runtime-client.js
CHANGED
|
@@ -50,12 +50,11 @@ export function createRuntimeClient({
|
|
|
50
50
|
cwd = process.cwd(),
|
|
51
51
|
rindHome = process.env.RIND_HOME,
|
|
52
52
|
runtimePath = process.env.RIND_RUNTIME_PATH || "",
|
|
53
|
-
onEvent = () => {},
|
|
54
53
|
onMessage = null,
|
|
55
54
|
onStderr = () => {},
|
|
56
55
|
onExit = () => {},
|
|
57
56
|
}) {
|
|
58
|
-
const handleEvent = onMessage
|
|
57
|
+
const handleEvent = onMessage;
|
|
59
58
|
const launch = resolveRuntimeLaunch({ python, repoRoot, runtimePath, cliArgs });
|
|
60
59
|
|
|
61
60
|
let nextId = 1;
|
|
@@ -120,13 +119,13 @@ export function createRuntimeClient({
|
|
|
120
119
|
onExit(code, signal, { closing, error });
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
function request(method, params = {}) {
|
|
124
|
-
const id = nextId++;
|
|
125
|
-
return new Promise((resolve, reject) => {
|
|
126
|
-
if (!child || !child.stdin.writable || child.destroyed) {
|
|
127
|
-
reject(new Error("Runtime is not running. Start it before sending a request."));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
122
|
+
function request(method, params = {}) {
|
|
123
|
+
const id = nextId++;
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
if (!child || !child.stdin.writable || child.destroyed) {
|
|
126
|
+
reject(new Error("Runtime is not running. Start it before sending a request."));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
130
129
|
const entry = { resolve, reject };
|
|
131
130
|
if (!LONG_RUNNING_METHODS.has(method)) {
|
|
132
131
|
entry.timer = setTimeout(() => {
|
|
@@ -145,8 +144,8 @@ export function createRuntimeClient({
|
|
|
145
144
|
reject(error);
|
|
146
145
|
});
|
|
147
146
|
});
|
|
148
|
-
}
|
|
149
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
150
149
|
function receive(line) {
|
|
151
150
|
let message;
|
|
152
151
|
try {
|
|
@@ -239,8 +238,8 @@ export function createRuntimeClient({
|
|
|
239
238
|
get child() {
|
|
240
239
|
return child;
|
|
241
240
|
},
|
|
242
|
-
start,
|
|
243
|
-
request,
|
|
241
|
+
start,
|
|
242
|
+
request,
|
|
244
243
|
shutdown,
|
|
245
244
|
forceShutdown,
|
|
246
245
|
closeInput,
|
package/lib/runtime-protocol.js
CHANGED
|
@@ -8,6 +8,7 @@ export const runtimeMethods = Object.freeze({
|
|
|
8
8
|
sessionNew: "session/new",
|
|
9
9
|
sessionList: "session/list",
|
|
10
10
|
sessionSwitch: "session/switch",
|
|
11
|
+
sessionFork: "session/fork",
|
|
11
12
|
sessionReplay: "session/replay",
|
|
12
13
|
sessionPrompt: "session/prompt",
|
|
13
14
|
sessionCancel: "session/cancel",
|
|
@@ -28,12 +29,15 @@ export const runtimeMethods = Object.freeze({
|
|
|
28
29
|
goalSet: "rind/goal/set",
|
|
29
30
|
goalStatus: "rind/goal/status",
|
|
30
31
|
goalClear: "rind/goal/clear",
|
|
32
|
+
contextInspect: "rind/context/inspect",
|
|
33
|
+
usageSummary: "rind/usage/summary",
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
export const sessionScopedMethods = new Set([
|
|
34
37
|
runtimeMethods.sessionPrompt,
|
|
35
38
|
runtimeMethods.sessionReplay,
|
|
36
39
|
runtimeMethods.sessionSwitch,
|
|
40
|
+
runtimeMethods.sessionFork,
|
|
37
41
|
runtimeMethods.sessionCancel,
|
|
38
42
|
runtimeMethods.modelSet,
|
|
39
43
|
runtimeMethods.modelEffortSet,
|
|
@@ -51,6 +55,7 @@ export const sessionScopedMethods = new Set([
|
|
|
51
55
|
runtimeMethods.goalSet,
|
|
52
56
|
runtimeMethods.goalStatus,
|
|
53
57
|
runtimeMethods.goalClear,
|
|
58
|
+
runtimeMethods.contextInspect,
|
|
54
59
|
]);
|
|
55
60
|
|
|
56
61
|
export const turnScopedMethods = new Set([
|
package/lib/send.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sendIpc } from "./ipc.js";
|
|
2
|
+
import { paint } from "./theme.js";
|
|
3
|
+
|
|
4
|
+
export const sendHelp = [
|
|
5
|
+
"Usage: rind send --session <id> \"<prompt>\"",
|
|
6
|
+
"",
|
|
7
|
+
"Sends a prompt to the running rind session with that id. Find the id in",
|
|
8
|
+
"the target session's startup banner or /status. Delivery is acknowledged",
|
|
9
|
+
"immediately; the reply appears in that session.",
|
|
10
|
+
].join("\n");
|
|
11
|
+
|
|
12
|
+
export function parseSendArgs(args) {
|
|
13
|
+
const result = { prompt: null, session: null };
|
|
14
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
15
|
+
const flag = args[index];
|
|
16
|
+
if (flag === "--session") {
|
|
17
|
+
const value = args[index + 1];
|
|
18
|
+
if (value === undefined || value.startsWith("--")) {
|
|
19
|
+
throw new Error("--session requires a value.");
|
|
20
|
+
}
|
|
21
|
+
index += 1;
|
|
22
|
+
if (result.session !== null) throw new Error("--session may only be specified once.");
|
|
23
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Invalid session id.");
|
|
24
|
+
result.session = value;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (flag.startsWith("--")) {
|
|
28
|
+
throw new Error(`Unknown send option: ${flag}`);
|
|
29
|
+
}
|
|
30
|
+
if (result.prompt !== null) {
|
|
31
|
+
throw new Error("send accepts exactly one prompt.");
|
|
32
|
+
}
|
|
33
|
+
result.prompt = flag;
|
|
34
|
+
}
|
|
35
|
+
if (!result.session) {
|
|
36
|
+
throw new Error("send requires --session <id>.");
|
|
37
|
+
}
|
|
38
|
+
if (!String(result.prompt || "").trim()) {
|
|
39
|
+
throw new Error("send requires a non-empty prompt.");
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runSend({ args, stdout = process.stdout, stderr = process.stderr }) {
|
|
45
|
+
const options = parseSendArgs(args);
|
|
46
|
+
const result = await sendIpc({ session: options.session, input: options.prompt });
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
stderr.write(`${result.message}\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
stdout.write(`${paint.success("✓")} sent to rind · session ${options.session}\n`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
@@ -16,11 +16,11 @@ export function createTaskMonitorController({
|
|
|
16
16
|
let refreshTimer = null;
|
|
17
17
|
let listInFlight = null;
|
|
18
18
|
let monitorTimer = null;
|
|
19
|
-
let monitorPollInFlight = false;
|
|
20
|
-
let monitor = null;
|
|
21
|
-
let monitorInputWasActive = false;
|
|
22
|
-
let generation = 0;
|
|
23
|
-
let pollToken = 0;
|
|
19
|
+
let monitorPollInFlight = false;
|
|
20
|
+
let monitor = null;
|
|
21
|
+
let monitorInputWasActive = false;
|
|
22
|
+
let generation = 0;
|
|
23
|
+
let pollToken = 0;
|
|
24
24
|
|
|
25
25
|
function refresh() {
|
|
26
26
|
if (!terminalUi || state.runtimeClosing) {
|
|
@@ -29,12 +29,12 @@ export function createTaskMonitorController({
|
|
|
29
29
|
if (listInFlight) {
|
|
30
30
|
return listInFlight;
|
|
31
31
|
}
|
|
32
|
-
const requestGeneration = generation;
|
|
33
|
-
const promise = request(runtimeMethods.backgroundList)
|
|
34
|
-
.then((result) => {
|
|
35
|
-
if (requestGeneration !== generation) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
32
|
+
const requestGeneration = generation;
|
|
33
|
+
const promise = request(runtimeMethods.backgroundList)
|
|
34
|
+
.then((result) => {
|
|
35
|
+
if (requestGeneration !== generation) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
38
|
const listed = Array.isArray(result?.tasks) ? result.tasks : [];
|
|
39
39
|
const ids = new Set();
|
|
40
40
|
for (const task of listed) {
|
|
@@ -50,19 +50,19 @@ export function createTaskMonitorController({
|
|
|
50
50
|
tasks.delete(bgId);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
updateCount();
|
|
54
|
-
if (monitor) {
|
|
55
|
-
monitor.selectedIndex = clampIndex(monitor.selectedIndex);
|
|
56
|
-
redraw();
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
.finally(() => {
|
|
60
|
-
if (listInFlight === promise) {
|
|
61
|
-
listInFlight = null;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
listInFlight = promise;
|
|
65
|
-
return promise;
|
|
53
|
+
updateCount();
|
|
54
|
+
if (monitor) {
|
|
55
|
+
monitor.selectedIndex = clampIndex(monitor.selectedIndex);
|
|
56
|
+
redraw();
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
if (listInFlight === promise) {
|
|
61
|
+
listInFlight = null;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
listInFlight = promise;
|
|
65
|
+
return promise;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function updateCount() {
|
|
@@ -104,12 +104,12 @@ export function createTaskMonitorController({
|
|
|
104
104
|
refreshTimer = null;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function clear() {
|
|
108
|
-
generation += 1;
|
|
109
|
-
listInFlight = null;
|
|
110
|
-
pollToken += 1;
|
|
111
|
-
monitorPollInFlight = false;
|
|
112
|
-
tasks.clear();
|
|
107
|
+
function clear() {
|
|
108
|
+
generation += 1;
|
|
109
|
+
listInFlight = null;
|
|
110
|
+
pollToken += 1;
|
|
111
|
+
monitorPollInFlight = false;
|
|
112
|
+
tasks.clear();
|
|
113
113
|
pendingCommands.clear();
|
|
114
114
|
delegates.clear();
|
|
115
115
|
stopRefresh();
|
|
@@ -266,15 +266,15 @@ export function createTaskMonitorController({
|
|
|
266
266
|
if (!selected?.bg_id) {
|
|
267
267
|
return;
|
|
268
268
|
}
|
|
269
|
-
const requestGeneration = generation;
|
|
270
|
-
const requestToken = ++pollToken;
|
|
271
|
-
monitorPollInFlight = true;
|
|
269
|
+
const requestGeneration = generation;
|
|
270
|
+
const requestToken = ++pollToken;
|
|
271
|
+
monitorPollInFlight = true;
|
|
272
272
|
try {
|
|
273
273
|
const result = await request(runtimeMethods.backgroundOutput, {
|
|
274
274
|
bg_id: selected.bg_id,
|
|
275
275
|
max_output_chars: 20000,
|
|
276
276
|
});
|
|
277
|
-
if (requestGeneration === generation && result?.task && typeof result.task === "object") {
|
|
277
|
+
if (requestGeneration === generation && result?.task && typeof result.task === "object") {
|
|
278
278
|
tasks.set(selected.bg_id, {
|
|
279
279
|
...selected,
|
|
280
280
|
...result.task,
|
|
@@ -285,10 +285,10 @@ export function createTaskMonitorController({
|
|
|
285
285
|
}
|
|
286
286
|
} catch {
|
|
287
287
|
// Periodic list refresh reconciles expired tasks without interrupting input.
|
|
288
|
-
} finally {
|
|
289
|
-
if (requestToken === pollToken) {
|
|
290
|
-
monitorPollInFlight = false;
|
|
291
|
-
}
|
|
288
|
+
} finally {
|
|
289
|
+
if (requestToken === pollToken) {
|
|
290
|
+
monitorPollInFlight = false;
|
|
291
|
+
}
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
@@ -369,12 +369,12 @@ export function createTaskMonitorController({
|
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
function stop() {
|
|
373
|
-
generation += 1;
|
|
374
|
-
listInFlight = null;
|
|
375
|
-
pollToken += 1;
|
|
376
|
-
monitorPollInFlight = false;
|
|
377
|
-
stopRefresh();
|
|
372
|
+
function stop() {
|
|
373
|
+
generation += 1;
|
|
374
|
+
listInFlight = null;
|
|
375
|
+
pollToken += 1;
|
|
376
|
+
monitorPollInFlight = false;
|
|
377
|
+
stopRefresh();
|
|
378
378
|
stopMonitorPolling();
|
|
379
379
|
monitor = null;
|
|
380
380
|
pendingCommands.clear();
|
|
@@ -390,7 +390,7 @@ export function createTaskMonitorController({
|
|
|
390
390
|
return tasks.size ? "background" : "delegates";
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
function clampIndex(index, page = monitor?.page || "background") {
|
|
393
|
+
function clampIndex(index, page = monitor?.page || "background") {
|
|
394
394
|
const count = pageItems(page).length;
|
|
395
395
|
if (!count) {
|
|
396
396
|
return 0;
|
package/lib/terminal-key.js
CHANGED
|
@@ -44,12 +44,40 @@ export function parseTerminalKey(raw = "") {
|
|
|
44
44
|
return { kind: "text", name: "", text: value };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const kitty = value.match(/^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/);
|
|
48
|
+
if (kitty) {
|
|
49
|
+
const codepoint = Number(kitty[1]);
|
|
50
|
+
const shiftedCodepoint = kitty[2] ? Number(kitty[2]) : codepoint;
|
|
51
|
+
const modifier = Number(kitty[4] || 1);
|
|
52
|
+
if (kitty[5] === "3") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const special = kittySpecialKey(codepoint, modifier);
|
|
56
|
+
if (special) {
|
|
57
|
+
return special;
|
|
58
|
+
}
|
|
59
|
+
if (codepoint < 32 || codepoint === 127) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const character = String.fromCodePoint(shiftedCodepoint);
|
|
63
|
+
if (modifier === 1 || modifier === 2) {
|
|
64
|
+
return { kind: "text", name: "", text: character };
|
|
65
|
+
}
|
|
66
|
+
return key(character.toLowerCase(), modifier);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const modifiedArrow = value.match(/^\x1b\[1;([0-9]+)(?::([1-3]))?([ABCDHF])$/);
|
|
48
70
|
if (modifiedArrow) {
|
|
49
|
-
|
|
71
|
+
if (modifiedArrow[2] === "3") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return key(ARROW_KEYS[modifiedArrow[3]], Number(modifiedArrow[1]));
|
|
50
75
|
}
|
|
51
|
-
const tilde = value.match(/^\x1b\[([0-9]+)(?:;([
|
|
76
|
+
const tilde = value.match(/^\x1b\[([0-9]+)(?:;([0-9]+))?(?::([1-3]))?~$/);
|
|
52
77
|
if (tilde) {
|
|
78
|
+
if (tilde[3] === "3") {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
53
81
|
return Number(tilde[1]) === 3 ? key("delete", Number(tilde[2] || 1)) : null;
|
|
54
82
|
}
|
|
55
83
|
const csiKey = value.match(/^\x1b\[([ABCDHFZ])$/);
|
|
@@ -95,3 +123,19 @@ function key(name, modifier = 1) {
|
|
|
95
123
|
text: "",
|
|
96
124
|
};
|
|
97
125
|
}
|
|
126
|
+
|
|
127
|
+
function kittySpecialKey(codepoint, modifier) {
|
|
128
|
+
if (codepoint === 13) {
|
|
129
|
+
return key("enter", modifier);
|
|
130
|
+
}
|
|
131
|
+
if (codepoint === 9) {
|
|
132
|
+
return key("tab", modifier);
|
|
133
|
+
}
|
|
134
|
+
if (codepoint === 127) {
|
|
135
|
+
return key("backspace", modifier);
|
|
136
|
+
}
|
|
137
|
+
if (codepoint === 27) {
|
|
138
|
+
return key("escape", modifier);
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
package/lib/text-width.js
CHANGED
|
@@ -4,6 +4,8 @@ const segmenter = typeof Intl?.Segmenter === "function"
|
|
|
4
4
|
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
5
5
|
: null;
|
|
6
6
|
|
|
7
|
+
export const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
8
|
+
|
|
7
9
|
export function stripAnsi(value) {
|
|
8
10
|
return String(value || "").replace(ANSI_RE, "");
|
|
9
11
|
}
|
|
@@ -82,25 +84,6 @@ export function truncateToWidth(value, maxWidth, ellipsis = "...") {
|
|
|
82
84
|
return `${takeStartCells(text, maxWidth - suffixWidth)}${ellipsis}`;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
export function expandTabs(value, tabWidth = 4) {
|
|
86
|
-
const text = String(value || "");
|
|
87
|
-
if (!text.includes("\t")) {
|
|
88
|
-
return text;
|
|
89
|
-
}
|
|
90
|
-
let column = 0;
|
|
91
|
-
let output = "";
|
|
92
|
-
for (const segment of graphemes(text)) {
|
|
93
|
-
if (segment === "\t") {
|
|
94
|
-
const spaces = tabWidth - (column % tabWidth);
|
|
95
|
-
output += " ".repeat(spaces);
|
|
96
|
-
column += spaces;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
output += segment;
|
|
100
|
-
column += segmentWidth(segment);
|
|
101
|
-
}
|
|
102
|
-
return output;
|
|
103
|
-
}
|
|
104
87
|
|
|
105
88
|
export function wrapTextWithAnsi(value, firstWidth, continuationWidth = firstWidth) {
|
|
106
89
|
const source = String(value ?? "");
|
package/lib/tool-display.js
CHANGED
|
@@ -133,7 +133,7 @@ function resultDataFromRaw(result) {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function renderToolRunning(context, width) {
|
|
136
|
+
export function renderToolRunning(context, width) {
|
|
137
137
|
const renderer = TOOL_RENDERERS[context.name] || GENERIC_RENDERER;
|
|
138
138
|
const elapsedSeconds = Math.floor((context.elapsedMs || 0) / 1000);
|
|
139
139
|
const elapsed = ELAPSED_TITLE_TOOLS.has(context.name) ? dim(` · ${elapsedSeconds}s`) : "";
|
|
@@ -143,8 +143,8 @@ export function renderToolRunning(context, width) {
|
|
|
143
143
|
if (context.progressMessage) {
|
|
144
144
|
lines.push(dim(` ↳ ${clipText(context.progressMessage, width, 8)}`));
|
|
145
145
|
}
|
|
146
|
-
return indentToolLines(lines, width);
|
|
147
|
-
}
|
|
146
|
+
return indentToolLines(lines, width);
|
|
147
|
+
}
|
|
148
148
|
|
|
149
149
|
export function renderToolFinished(context, width) {
|
|
150
150
|
const renderer = TOOL_RENDERERS[context.name] || GENERIC_RENDERER;
|
|
@@ -152,10 +152,10 @@ export function renderToolFinished(context, width) {
|
|
|
152
152
|
const lines = [clipCells(renderer.finished(context, width, state), Math.max(1, width))];
|
|
153
153
|
if (state.kind === "error") {
|
|
154
154
|
const detail = errorDetail(context.event, width);
|
|
155
|
-
if (detail) {
|
|
156
|
-
lines.push(detail);
|
|
157
|
-
}
|
|
158
|
-
return indentToolLines(lines, width);
|
|
155
|
+
if (detail) {
|
|
156
|
+
lines.push(detail);
|
|
157
|
+
}
|
|
158
|
+
return indentToolLines(lines, width);
|
|
159
159
|
}
|
|
160
160
|
const bodyLimit = context.expanded ? EXPANDED_HARD_CAPS[context.name] ?? 200 : COLLAPSED_BODY_CAPS[context.name] ?? 0;
|
|
161
161
|
const produced = renderer.body ? renderer.body(context, width, bodyLimit) : [];
|
|
@@ -164,20 +164,20 @@ export function renderToolFinished(context, width) {
|
|
|
164
164
|
if (normalized.total > 0) {
|
|
165
165
|
lines.push(bodyFooter(normalized.total, context.expanded));
|
|
166
166
|
}
|
|
167
|
-
return indentToolLines(lines, width);
|
|
167
|
+
return indentToolLines(lines, width);
|
|
168
168
|
}
|
|
169
169
|
lines.push(...normalized.lines);
|
|
170
170
|
const hidden = Math.max(0, normalized.total - normalized.lines.length);
|
|
171
171
|
if (hidden > 0) {
|
|
172
172
|
lines.push(bodyFooter(hidden, context.expanded));
|
|
173
173
|
}
|
|
174
|
-
return indentToolLines(lines, width);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function indentToolLines(lines, width) {
|
|
178
|
-
const limit = Math.max(1, Number(width) || 1);
|
|
179
|
-
return lines.map((line) => clipCells(` ${line}`, limit));
|
|
180
|
-
}
|
|
174
|
+
return indentToolLines(lines, width);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function indentToolLines(lines, width) {
|
|
178
|
+
const limit = Math.max(1, Number(width) || 1);
|
|
179
|
+
return lines.map((line) => clipCells(` ${line}`, limit));
|
|
180
|
+
}
|
|
181
181
|
|
|
182
182
|
function normalizeBody(produced) {
|
|
183
183
|
if (Array.isArray(produced)) {
|
|
@@ -233,7 +233,7 @@ function durationPart(name, event) {
|
|
|
233
233
|
return dim(` · ${formatDuration(event?.duration_ms)}`);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
function formatDuration(durationMs) {
|
|
236
|
+
export function formatDuration(durationMs) {
|
|
237
237
|
const value = Number(durationMs || 0);
|
|
238
238
|
if (!Number.isFinite(value) || value <= 0) {
|
|
239
239
|
return "0ms";
|
|
@@ -388,12 +388,12 @@ const BASH_RENDERER = {
|
|
|
388
388
|
|
|
389
389
|
const BASH_OUTPUT_RENDERER = {
|
|
390
390
|
runningMain(context, width) {
|
|
391
|
-
return `${bold("bg")} ${
|
|
391
|
+
return `${bold("bg")} ${commandArg(context.args.bg_id, width, 10)}`;
|
|
392
392
|
},
|
|
393
393
|
finished(context, width, state) {
|
|
394
394
|
const { data } = resultData(context);
|
|
395
395
|
const id = context.args.bg_id || data.bg_id;
|
|
396
|
-
let main = `${bold("bg")} ${
|
|
396
|
+
let main = `${bold("bg")} ${commandArg(id, width, 14)}`;
|
|
397
397
|
if (state.kind === "cancelled") {
|
|
398
398
|
main += ` ${dim("(cancelled)")}`;
|
|
399
399
|
}
|
|
@@ -658,11 +658,6 @@ function commandArg(value, width, reserve) {
|
|
|
658
658
|
return text ? clipText(text, width, reserve) : dim("…");
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
function bgIdArg(value, width, reserve) {
|
|
662
|
-
const text = singleLineText(value);
|
|
663
|
-
return text ? clipText(text, width, reserve) : dim("…");
|
|
664
|
-
}
|
|
665
|
-
|
|
666
661
|
export const TOOL_RENDERERS = {
|
|
667
662
|
bash: BASH_RENDERER,
|
|
668
663
|
bash_output: BASH_OUTPUT_RENDERER,
|
package/lib/tui/cursor.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CURSOR_MARKER } from "./tui.js";
|
|
2
|
+
|
|
3
|
+
// Non-global: String.match must expose match.index for cursor placement.
|
|
4
|
+
const ANSI_SEQUENCE = /\[[0-?]*[ -/]*[@-~]/;
|
|
2
5
|
import { graphemes, textWidth } from "../text-width.js";
|
|
3
6
|
|
|
4
|
-
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
5
7
|
|
|
6
8
|
export function insertCursorMarker(line, column) {
|
|
7
9
|
const text = String(line || "");
|
|
@@ -16,14 +18,21 @@ export function insertCursorMarker(line, column) {
|
|
|
16
18
|
continue;
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
21
|
+
const ansiIndex = text.indexOf("\x1b", position);
|
|
22
|
+
const end = ansiIndex === -1 ? text.length : ansiIndex;
|
|
23
|
+
const content = text.slice(position, end);
|
|
24
|
+
if (!content) {
|
|
25
|
+
position += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const segment of graphemes(content)) {
|
|
29
|
+
const segmentWidth = textWidth(segment);
|
|
30
|
+
if (width + segmentWidth > target) {
|
|
31
|
+
return `${text.slice(0, position)}${CURSOR_MARKER}${text.slice(position)}`;
|
|
32
|
+
}
|
|
33
|
+
width += segmentWidth;
|
|
34
|
+
position += segment.length;
|
|
24
35
|
}
|
|
25
|
-
width += segmentWidth;
|
|
26
|
-
position += segment.length;
|
|
27
36
|
}
|
|
28
37
|
return `${text.slice(0, position)}${CURSOR_MARKER}${text.slice(position)}`;
|
|
29
38
|
}
|
package/lib/tui/input-buffer.js
CHANGED
|
@@ -3,6 +3,42 @@ const PASTE_START = "\x1b[200~";
|
|
|
3
3
|
const PASTE_END = "\x1b[201~";
|
|
4
4
|
const DEFAULT_INPUT_TIMEOUT_MS = 10;
|
|
5
5
|
|
|
6
|
+
function sequenceStatus(value) {
|
|
7
|
+
if (!value.startsWith(ESC)) {
|
|
8
|
+
return "not-escape";
|
|
9
|
+
}
|
|
10
|
+
if (value.length === 1) {
|
|
11
|
+
return "incomplete";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const type = value[1];
|
|
15
|
+
if (type === "[") {
|
|
16
|
+
if (value.startsWith(`${ESC}[M`)) {
|
|
17
|
+
return value.length >= 6 ? "complete" : "incomplete";
|
|
18
|
+
}
|
|
19
|
+
return csiStatus(value);
|
|
20
|
+
}
|
|
21
|
+
if (type === "O") {
|
|
22
|
+
return value.length >= 3 ? "complete" : "incomplete";
|
|
23
|
+
}
|
|
24
|
+
if (type === "]" || type === "P" || type === "_") {
|
|
25
|
+
return value.includes("\x07") || value.includes(`${ESC}\\`) ? "complete" : "incomplete";
|
|
26
|
+
}
|
|
27
|
+
const codepoint = value.codePointAt(1);
|
|
28
|
+
if (codepoint >= 0xd800 && codepoint <= 0xdbff && value.length < 3) {
|
|
29
|
+
return "incomplete";
|
|
30
|
+
}
|
|
31
|
+
return "complete";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function csiStatus(value) {
|
|
35
|
+
if (value.length < 3) {
|
|
36
|
+
return "incomplete";
|
|
37
|
+
}
|
|
38
|
+
const final = value.charCodeAt(value.length - 1);
|
|
39
|
+
return final >= 0x40 && final <= 0x7e ? "complete" : "incomplete";
|
|
40
|
+
}
|
|
41
|
+
|
|
6
42
|
export function createInputBuffer(options = {}) {
|
|
7
43
|
const onSequence = typeof options.onSequence === "function" ? options.onSequence : () => {};
|
|
8
44
|
const onPaste = typeof options.onPaste === "function" ? options.onPaste : () => {};
|
|
@@ -14,6 +50,7 @@ export function createInputBuffer(options = {}) {
|
|
|
14
50
|
let timer = null;
|
|
15
51
|
let pasteMode = false;
|
|
16
52
|
let pasteBuffer = "";
|
|
53
|
+
let pendingKittyCodepoint = null;
|
|
17
54
|
|
|
18
55
|
function feed(data) {
|
|
19
56
|
const value = Buffer.isBuffer(data) ? data.toString("utf8") : String(data || "");
|
|
@@ -39,6 +76,7 @@ export function createInputBuffer(options = {}) {
|
|
|
39
76
|
buffer = "";
|
|
40
77
|
pasteMode = false;
|
|
41
78
|
pasteBuffer = "";
|
|
79
|
+
pendingKittyCodepoint = null;
|
|
42
80
|
}
|
|
43
81
|
|
|
44
82
|
function clearTimer() {
|
|
@@ -50,9 +88,20 @@ export function createInputBuffer(options = {}) {
|
|
|
50
88
|
}
|
|
51
89
|
|
|
52
90
|
function emit(sequence) {
|
|
53
|
-
if (sequence) {
|
|
54
|
-
|
|
91
|
+
if (!sequence) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (pendingKittyCodepoint !== null) {
|
|
95
|
+
const codepoint = sequence.codePointAt(0);
|
|
96
|
+
if (!sequence.startsWith(ESC) && sequence.length === String.fromCodePoint(codepoint).length && codepoint === pendingKittyCodepoint) {
|
|
97
|
+
pendingKittyCodepoint = null;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
pendingKittyCodepoint = null;
|
|
55
101
|
}
|
|
102
|
+
onSequence(sequence);
|
|
103
|
+
const kittyPrintable = sequence.match(/^\x1b\[(\d+)u$/);
|
|
104
|
+
pendingKittyCodepoint = kittyPrintable ? Number(kittyPrintable[1]) : null;
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
function processBuffer() {
|
|
@@ -77,6 +126,7 @@ export function createInputBuffer(options = {}) {
|
|
|
77
126
|
pasteMode = true;
|
|
78
127
|
pasteBuffer = pasteContent;
|
|
79
128
|
buffer = "";
|
|
129
|
+
pendingKittyCodepoint = null;
|
|
80
130
|
const endIndex = pasteBuffer.indexOf(PASTE_END);
|
|
81
131
|
if (endIndex !== -1) {
|
|
82
132
|
finishPaste(endIndex);
|
|
@@ -92,6 +142,7 @@ export function createInputBuffer(options = {}) {
|
|
|
92
142
|
const remaining = pasteBuffer.slice(endIndex + PASTE_END.length);
|
|
93
143
|
pasteMode = false;
|
|
94
144
|
pasteBuffer = "";
|
|
145
|
+
pendingKittyCodepoint = null;
|
|
95
146
|
onPaste(content);
|
|
96
147
|
if (remaining) {
|
|
97
148
|
feed(remaining);
|
|
@@ -130,10 +181,18 @@ export function splitSequences(value) {
|
|
|
130
181
|
continue;
|
|
131
182
|
}
|
|
132
183
|
|
|
133
|
-
|
|
184
|
+
let end = findEscapeEnd(value, position);
|
|
134
185
|
if (end === -1) {
|
|
135
186
|
return { sequences, remainder: value.slice(position) };
|
|
136
187
|
}
|
|
188
|
+
if (value.slice(position, end) === `${ESC}${ESC}`) {
|
|
189
|
+
const next = value[end];
|
|
190
|
+
if (["[", "]", "O", "P", "_"].includes(next)) {
|
|
191
|
+
sequences.push(ESC);
|
|
192
|
+
position += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
137
196
|
sequences.push(value.slice(position, end));
|
|
138
197
|
position = end;
|
|
139
198
|
}
|
|
@@ -141,32 +200,14 @@ export function splitSequences(value) {
|
|
|
141
200
|
}
|
|
142
201
|
|
|
143
202
|
function findEscapeEnd(value, start) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (type === "[") {
|
|
149
|
-
for (let index = start + 2; index < value.length; index += 1) {
|
|
150
|
-
const code = value.charCodeAt(index);
|
|
151
|
-
if (code >= 0x40 && code <= 0x7e) {
|
|
152
|
-
return index + 1;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return -1;
|
|
156
|
-
}
|
|
157
|
-
if (type === "O") {
|
|
158
|
-
return start + 3 <= value.length ? start + 3 : -1;
|
|
159
|
-
}
|
|
160
|
-
if (type === "]" || type === "P" || type === "_") {
|
|
161
|
-
const bell = value.indexOf("\x07", start + 2);
|
|
162
|
-
const stringTerminator = value.indexOf(`${ESC}\\`, start + 2);
|
|
163
|
-
if (bell === -1 && stringTerminator === -1) {
|
|
164
|
-
return -1;
|
|
203
|
+
for (let end = start + 1; end <= value.length; end += 1) {
|
|
204
|
+
const status = sequenceStatus(value.slice(start, end));
|
|
205
|
+
if (status === "complete") {
|
|
206
|
+
return end;
|
|
165
207
|
}
|
|
166
|
-
if (
|
|
167
|
-
return
|
|
208
|
+
if (status === "not-escape") {
|
|
209
|
+
return start + 1;
|
|
168
210
|
}
|
|
169
|
-
return stringTerminator + 2;
|
|
170
211
|
}
|
|
171
|
-
return
|
|
212
|
+
return -1;
|
|
172
213
|
}
|