@youdie006/prodex 0.31.0 → 0.32.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.
@@ -1416,18 +1416,32 @@ async function assertSelectionCommitted(cdp, label) {
1416
1416
  throw new Error(`ChatGPT selection "${label}" did not commit; the model menu stayed open. Retry, or pick it manually in the visible browser.`);
1417
1417
  }
1418
1418
  }
1419
+ // Roughly three seconds of grace for a menu that is still painting.
1420
+ const POWER_SLIDER_APPEAR_ATTEMPTS = 5;
1419
1421
  /**
1420
1422
  * Move the power slider until its Effort readout is the requested step. The
1421
1423
  * menu must already be open. Returns the quota line so the caller can warn
1422
1424
  * when Pro runs are nearly spent.
1423
1425
  */
1424
1426
  async function selectPowerStep(cdp, requested) {
1425
- const focused = await cdp.evaluate(focusPowerSliderExpression());
1427
+ // The menu paints a moment after it opens, and on a page that has just been
1428
+ // built - a project created seconds ago - that moment is longer. Failing the
1429
+ // whole send on the first empty look cost a real consult its answer, so give
1430
+ // the slider a few beats to appear.
1431
+ let focused = await cdp.evaluate(focusPowerSliderExpression());
1432
+ for (let attempt = 0; attempt < POWER_SLIDER_APPEAR_ATTEMPTS && !focused?.ok; attempt += 1) {
1433
+ await sleep(600);
1434
+ focused = await cdp.evaluate(focusPowerSliderExpression());
1435
+ }
1426
1436
  if (!focused?.ok) {
1427
1437
  throw new Error(focused?.reason ??
1428
1438
  "ChatGPT's model picker did not expose its power slider, so the requested model/effort could not be selected.");
1429
1439
  }
1430
1440
  let state = await cdp.evaluate(powerSliderStateExpression());
1441
+ for (let attempt = 0; attempt < POWER_SLIDER_APPEAR_ATTEMPTS && !state?.ok; attempt += 1) {
1442
+ await sleep(600);
1443
+ state = await cdp.evaluate(powerSliderStateExpression());
1444
+ }
1431
1445
  if (!state?.ok)
1432
1446
  throw new Error(state?.reason ?? "Could not read ChatGPT's power slider");
1433
1447
  const steps = (state.max ?? 4) - (state.min ?? 0) + 1;
package/dist/tui-run.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import readline from "node:readline";
10
10
  import { renderBanner } from "./banner.js";
11
11
  import { destinationChoices, effortChoices, SEND_KINDS } from "./tui-flow.js";
12
- import { consultArgsFromChoices, conversationsInProject, conversationThreadUrl, moveCursor, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
12
+ import { consultArgsFromChoices, conversationsInProject, conversationThreadUrl, moveCursor, parseAttachmentLine, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
13
13
  const ESC = "";
14
14
  const CLEAR = `${ESC}[2J${ESC}[H`;
15
15
  // The alternate screen buffer: the terminal comes back exactly as it was, so a
@@ -154,9 +154,12 @@ export async function runInteractiveConsult(io, deps) {
154
154
  const pinnedPromise = deps.pinnedProject?.().catch(() => undefined);
155
155
  const pinnedModelPromise = deps.pinnedModel?.().catch(() => undefined);
156
156
  const conversationsPromise = deps.listConversations?.().catch(() => []);
157
+ // A normal chat asks one more question than the tool kinds do, so how many
158
+ // steps remain is not known until this first answer is given.
159
+ const totalSteps = 4;
157
160
  const kindChoice = await pick(io, {
158
161
  title: "What kind of send is this?",
159
- step: { index: 1, total: 3 },
162
+ step: { index: 1 },
160
163
  options: SEND_KINDS.map((kind) => ({ label: kind.label, ...(kind.hint ? { hint: kind.hint } : {}) }))
161
164
  }, banner);
162
165
  if (kindChoice === undefined)
@@ -165,10 +168,13 @@ export async function runInteractiveConsult(io, deps) {
165
168
  // Only an ordinary chat has a reasoning level to choose: the tool kinds
166
169
  // bring their own pipeline, and an effort would deselect Pro under them.
167
170
  let effort;
168
- if (SEND_KINDS[kindChoice].id === "chat") {
171
+ const asksReasoning = SEND_KINDS[kindChoice].id === "chat";
172
+ const steps = asksReasoning ? totalSteps : totalSteps - 1;
173
+ if (asksReasoning) {
169
174
  const efforts = effortChoices(await pinnedModelPromise);
170
175
  const chosen = await pick(io, {
171
176
  title: "How much reasoning?",
177
+ step: { index: 2, total: steps },
172
178
  options: efforts.map((entry) => ({ label: entry.label, ...(entry.hint ? { hint: entry.hint } : {}) }))
173
179
  }, banner);
174
180
  if (chosen === undefined)
@@ -184,7 +190,7 @@ export async function runInteractiveConsult(io, deps) {
184
190
  const destinations = destinationChoices(pinnedProject);
185
191
  const destinationChoice = await pick(io, {
186
192
  title: "Where should it go?",
187
- step: { index: 2, total: 3 },
193
+ step: { index: asksReasoning ? 3 : 2, total: steps },
188
194
  options: destinations.map((entry) => ({ label: entry.label, ...(entry.hint ? { hint: entry.hint } : {}) }))
189
195
  }, header);
190
196
  if (destinationChoice === undefined)
@@ -246,18 +252,25 @@ export async function runInteractiveConsult(io, deps) {
246
252
  // The prompt comes last: what you type depends on what you just decided.
247
253
  io.write(CLEAR + header + SHOW_CURSOR);
248
254
  const kindLabel = SEND_KINDS[kindChoice].label;
249
- const prompt = await askLine(io, `${kindLabel} Step 3 of 3\n\n prompt: `);
255
+ const prompt = await askLine(io, `${kindLabel} Step ${steps} of ${steps}\n\n prompt: `);
250
256
  io.write(HIDE_CURSOR);
251
257
  if (prompt.length === 0) {
252
258
  io.write("Nothing to ask.\n");
253
259
  return 1;
254
260
  }
261
+ // Uploading a file is the only way ChatGPT can open a pdf, pptx, xlsx or
262
+ // image, and the picker had no way to say so. Asked after the prompt, and
263
+ // skipped by pressing enter, so the common case costs one keystroke.
264
+ io.write(SHOW_CURSOR);
265
+ const attachments = parseAttachmentLine(await askLine(io, "\n attach files? repo-relative paths, space separated, enter to skip\n files: "));
266
+ io.write(HIDE_CURSOR);
255
267
  const choices = {
256
268
  prompt,
257
269
  projectMode,
258
270
  ...(projectName ? { projectName } : {}),
259
271
  ...(targetUrl ? { targetUrl } : {}),
260
272
  ...(effort ? { effort } : {}),
273
+ ...(attachments.length > 0 ? { attachments } : {}),
261
274
  tools,
262
275
  newChat: !targetUrl
263
276
  };
package/dist/tui.js CHANGED
@@ -57,6 +57,21 @@ export function consultArgsFromChoices(choices) {
57
57
  args.push("--", prompt);
58
58
  return args;
59
59
  }
60
+ /**
61
+ * Split the one line a person types into the paths the send wants one flag at
62
+ * a time. Quotes are honoured because file names have spaces in them.
63
+ */
64
+ export function parseAttachmentLine(line) {
65
+ const out = [];
66
+ const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
67
+ let match;
68
+ while ((match = pattern.exec(line)) !== null) {
69
+ const value = (match[1] ?? match[2] ?? match[3] ?? "").trim();
70
+ if (value.length > 0)
71
+ out.push(value);
72
+ }
73
+ return out;
74
+ }
60
75
  /**
61
76
  * Projects with the id their conversations are tagged with.
62
77
  *
@@ -151,7 +166,9 @@ export function renderSelectList(input) {
151
166
  const color = input.color ?? true;
152
167
  const width = input.width ?? 100;
153
168
  const selected = new Set(input.selected ?? []);
154
- const step = input.step ? paint(` Step ${input.step.index} of ${input.step.total}`, DIM, color) : "";
169
+ const step = input.step
170
+ ? paint(` Step ${input.step.index}${input.step.total ? ` of ${input.step.total}` : ""}`, DIM, color)
171
+ : "";
155
172
  const lines = [`${paint(input.title, BOLD, color)}${step}`, ""];
156
173
  // Hints line up in their own column; ragged hints read as noise next to the
157
174
  // labels they belong to.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",