@youdie006/prodex 0.27.1 → 0.27.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.
Files changed (2) hide show
  1. package/dist/tui-run.js +63 -22
  2. package/package.json +1 -1
package/dist/tui-run.js CHANGED
@@ -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;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",