@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 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
- function readKey(io) {
21
- return new Promise((resolve) => {
22
- const onKey = (_str, key) => {
23
- io.input.off("keypress", onKey);
24
- resolve(key ?? {});
25
- };
26
- io.input.on("keypress", onKey);
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
- // Show what the send will use before asking anything: choosing a project or
142
- // a tool without knowing the model, or whether the browser is even up, is
143
- // choosing blind - and a browser failure after four questions wastes all four.
144
- if (deps.describeContext) {
145
- try {
146
- const rows = await deps.describeContext();
147
- if (rows.length > 0)
148
- header = `${renderContextPanel(rows, { color: colorEnabled(), width: terminalWidth() })}\n\n`;
149
- }
150
- catch {
151
- // Context is a courtesy; never let it block the consult.
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.replace(/^progress:\s*/, "").slice(0, 60);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.27.1",
3
+ "version": "0.27.3",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",