@youdie006/prodex 0.27.1 → 0.27.3
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/tui-run.js +65 -24
- package/dist/tui.js +19 -0
- package/package.json +1 -1
package/dist/tui-run.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* what keeps the interactive path from drifting away from the documented flags.
|
|
8
8
|
*/
|
|
9
9
|
import readline from "node:readline";
|
|
10
|
-
import { consultArgsFromChoices, moveCursor, renderContextPanel, renderProgressBar, renderSelectList, toggleSelection } from "./tui.js";
|
|
10
|
+
import { consultArgsFromChoices, moveCursor, progressLabel, renderContextPanel, renderProgressBar, renderSelectList, toggleSelection } from "./tui.js";
|
|
11
11
|
const ESC = "";
|
|
12
12
|
const CLEAR = `${ESC}[2J${ESC}[H`;
|
|
13
13
|
// The alternate screen buffer: the terminal comes back exactly as it was, so a
|
|
@@ -17,14 +17,54 @@ const ALT_SCREEN_OFF = `${ESC}[?1049l`;
|
|
|
17
17
|
const CLEAR_LINE = `${ESC}[2K\r`;
|
|
18
18
|
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
19
19
|
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Keys are queued by one long-lived listener rather than a listener attached
|
|
22
|
+
* per read.
|
|
23
|
+
*
|
|
24
|
+
* Attaching on demand drops every key pressed while nothing is waiting - during
|
|
25
|
+
* a redraw, or across the seconds it takes to read the project list out of the
|
|
26
|
+
* browser. Measured by using it: the first key after the prompt vanished, so
|
|
27
|
+
* "4" (no project) was swallowed and the next key landed on the wrong row.
|
|
28
|
+
*/
|
|
29
|
+
class KeyQueue {
|
|
30
|
+
input;
|
|
31
|
+
pending = [];
|
|
32
|
+
waiting;
|
|
33
|
+
constructor(input) {
|
|
34
|
+
this.input = input;
|
|
35
|
+
this.input.on("keypress", this.onKey);
|
|
36
|
+
}
|
|
37
|
+
onKey = (_str, key) => {
|
|
38
|
+
const value = key ?? {};
|
|
39
|
+
if (this.waiting) {
|
|
40
|
+
const resolve = this.waiting;
|
|
41
|
+
this.waiting = undefined;
|
|
42
|
+
resolve(value);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.pending.push(value);
|
|
46
|
+
};
|
|
47
|
+
next() {
|
|
48
|
+
const buffered = this.pending.shift();
|
|
49
|
+
if (buffered)
|
|
50
|
+
return Promise.resolve(buffered);
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
this.waiting = resolve;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Drop keys typed before a screen existed to receive them. */
|
|
56
|
+
drain() {
|
|
57
|
+
this.pending.length = 0;
|
|
58
|
+
}
|
|
59
|
+
dispose() {
|
|
60
|
+
this.input.off("keypress", this.onKey);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let keys;
|
|
64
|
+
function readKey(_io) {
|
|
65
|
+
if (!keys)
|
|
66
|
+
throw new Error("The key reader is not running.");
|
|
67
|
+
return keys.next();
|
|
28
68
|
}
|
|
29
69
|
function isCancel(key) {
|
|
30
70
|
return (key.ctrl === true && key.name === "c") || key.name === "escape" || key.name === "q";
|
|
@@ -118,6 +158,7 @@ async function askLine(io, question) {
|
|
|
118
158
|
io.input.setRawMode?.(true);
|
|
119
159
|
readline.emitKeypressEvents(io.input);
|
|
120
160
|
io.input.resume();
|
|
161
|
+
keys?.drain();
|
|
121
162
|
return answer.trim();
|
|
122
163
|
}
|
|
123
164
|
const TOOL_CHOICES = [
|
|
@@ -134,24 +175,22 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
134
175
|
readline.emitKeypressEvents(io.input);
|
|
135
176
|
io.input.setRawMode?.(true);
|
|
136
177
|
io.input.resume();
|
|
178
|
+
keys = new KeyQueue(io.input);
|
|
137
179
|
io.write(ALT_SCREEN_ON + HIDE_CURSOR);
|
|
138
180
|
let header = "";
|
|
139
181
|
try {
|
|
140
182
|
io.write(CLEAR);
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const prompt = await askLine(io, `${header}Ask ChatGPT Pro\n\n prompt: `);
|
|
183
|
+
// Gather the context WHILE the prompt is being typed. Reading the browser
|
|
184
|
+
// state takes a second or two, and doing it first left the screen blank for
|
|
185
|
+
// that long - anything typed into that gap was swallowed, prompt included.
|
|
186
|
+
const contextPromise = deps.describeContext?.().catch(() => []);
|
|
187
|
+
const prompt = await askLine(io, "Ask ChatGPT Pro\n\n prompt: ");
|
|
188
|
+
// The panel still leads every question that follows, which is where it
|
|
189
|
+
// earns its place: choosing a project and tools without knowing the browser
|
|
190
|
+
// is down wastes every answer given before the failure.
|
|
191
|
+
const rows = (await contextPromise) ?? [];
|
|
192
|
+
if (rows.length > 0)
|
|
193
|
+
header = `${renderContextPanel(rows, { color: colorEnabled(), width: terminalWidth() })}\n\n`;
|
|
155
194
|
if (prompt.length === 0) {
|
|
156
195
|
io.write("Nothing to ask.\n");
|
|
157
196
|
return 1;
|
|
@@ -233,7 +272,7 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
233
272
|
}, 250);
|
|
234
273
|
try {
|
|
235
274
|
const code = await deps.runConsult(args, (line) => {
|
|
236
|
-
label = line
|
|
275
|
+
label = progressLabel(line).slice(0, 60);
|
|
237
276
|
});
|
|
238
277
|
clearInterval(timer);
|
|
239
278
|
io.write(CLEAR_LINE);
|
|
@@ -246,6 +285,8 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
246
285
|
}
|
|
247
286
|
}
|
|
248
287
|
finally {
|
|
288
|
+
keys?.dispose();
|
|
289
|
+
keys = undefined;
|
|
249
290
|
io.write(ALT_SCREEN_OFF + SHOW_CURSOR);
|
|
250
291
|
io.input.setRawMode?.(false);
|
|
251
292
|
io.input.pause();
|
package/dist/tui.js
CHANGED
|
@@ -115,6 +115,25 @@ function formatElapsed(ms) {
|
|
|
115
115
|
const rest = seconds % 60;
|
|
116
116
|
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Turn a `progress:` line from the send into the label beside the bar.
|
|
120
|
+
*
|
|
121
|
+
* The bar already carries the elapsed clock, so a label that opens with
|
|
122
|
+
* "waiting 2m 57s" prints the same number twice and pushes the part worth
|
|
123
|
+
* reading off to the right. Keep the detail, drop the duplicate.
|
|
124
|
+
*/
|
|
125
|
+
export function progressLabel(line) {
|
|
126
|
+
const text = line.replace(/^progress:\s*/, "").trim();
|
|
127
|
+
const waiting = /^waiting\s+[0-9]+[a-z]*(?:\s+[0-9]+[a-z]*)*\s*(.*)$/i.exec(text);
|
|
128
|
+
if (!waiting)
|
|
129
|
+
return text;
|
|
130
|
+
const detail = waiting[1].trim();
|
|
131
|
+
if (detail.length === 0)
|
|
132
|
+
return "waiting";
|
|
133
|
+
// The send wraps its detail in parentheses; the bar reads better without them.
|
|
134
|
+
const unwrapped = /^\((.*)\)$/.exec(detail);
|
|
135
|
+
return (unwrapped ? unwrapped[1] : detail).trim();
|
|
136
|
+
}
|
|
118
137
|
const SPINNER = ["|", "/", "-", "\\"];
|
|
119
138
|
/**
|
|
120
139
|
* A Pro consult can run for many minutes with nothing on screen. The bar fills
|