@youdie006/prodex 0.21.0 → 0.21.1

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.
@@ -1872,6 +1872,7 @@ export async function sendChatGptPrompt(options) {
1872
1872
  emitProgress("selecting", `attached ${uploaded.attached.join(", ")}`);
1873
1873
  }
1874
1874
  const toolLabels = (options.tools ?? []).map(resolveComposerToolLabel);
1875
+ const wantsDeepResearch = toolLabels.includes(DEEP_RESEARCH_TOOL_LABEL);
1875
1876
  if (toolLabels.length > 0)
1876
1877
  emitProgress("selecting", `tools=${toolLabels.join(", ")}`);
1877
1878
  await insertComposerTextViaCdp(cdp, options.prompt, page, toolLabels);
@@ -1915,6 +1916,26 @@ export async function sendChatGptPrompt(options) {
1915
1916
  }
1916
1917
  }
1917
1918
  dbgSend(`submit posted=${promptPosted} submitButtonFound=${submitButtonFound}`);
1919
+ // Deep research does not begin when the prompt posts: it shows a start
1920
+ // control with a countdown ring. Press it instead of trusting the timer -
1921
+ // a run left waiting sat with zero assistant messages for 30+ minutes.
1922
+ if (wantsDeepResearch) {
1923
+ const startDeadline = Date.now() + 60_000;
1924
+ let pressed = false;
1925
+ while (Date.now() < startDeadline) {
1926
+ const start = await cdp.evaluate(deepResearchStartButtonRectExpression());
1927
+ if (start.ok && start.x !== undefined && start.y !== undefined) {
1928
+ await dispatchMouseClickAt(cdp, start.x, start.y);
1929
+ pressed = true;
1930
+ emitProgress("selecting", "deep research started");
1931
+ break;
1932
+ }
1933
+ await sleep(1_000);
1934
+ }
1935
+ if (!pressed) {
1936
+ sendWarnings.push("deep_research_start_not_found: no start control appeared for the deep research run. If ChatGPT asked a clarifying question instead, answer it with a follow-up consult in the same thread.");
1937
+ }
1938
+ }
1918
1939
  }
1919
1940
  finally {
1920
1941
  cdp.close();
@@ -2564,10 +2585,31 @@ export async function attachFilesToComposer(cdp, absolutePaths, options = {}) {
2564
2585
  // check has to ignore the token or it reads as leftover contamination.
2565
2586
  // ---------------------------------------------------------------------------
2566
2587
  const COMPOSER_TOOL_ALIASES = [
2567
- { label: "Deep research", aliases: ["deep research", "deep-research", "deepresearch", "deep", "research"] },
2568
- { label: "Web search", aliases: ["web search", "web-search", "websearch", "search", "web"] },
2569
- { label: "Create image", aliases: ["create image", "create-image", "image", "img"] }
2588
+ // menuText carries every string the menu row is known to render. ChatGPT
2589
+ // dropped the visible "Deep research" title at one point and left only its
2590
+ // description, which made a title-only lookup fail (measured live), so each
2591
+ // tool is found by title OR description.
2592
+ {
2593
+ label: "Deep research",
2594
+ aliases: ["deep research", "deep-research", "deepresearch", "deep", "research"],
2595
+ menuText: ["Deep research", "Get a detailed report"]
2596
+ },
2597
+ {
2598
+ label: "Web search",
2599
+ aliases: ["web search", "web-search", "websearch", "search", "web"],
2600
+ menuText: ["Web search", "Find real-time news and info"]
2601
+ },
2602
+ {
2603
+ label: "Create image",
2604
+ aliases: ["create image", "create-image", "image", "img"],
2605
+ menuText: ["Create image", "Visualize anything"]
2606
+ }
2570
2607
  ];
2608
+ /** Every string the tools menu may render for this tool. */
2609
+ export function composerToolMenuTexts(label) {
2610
+ const known = COMPOSER_TOOL_ALIASES.find((tool) => tool.label === label);
2611
+ return known ? [...known.menuText] : [label];
2612
+ }
2571
2613
  /**
2572
2614
  * Map what a caller typed to the label ChatGPT renders. An unknown value is
2573
2615
  * passed through unchanged, so a tool ChatGPT adds tomorrow is reachable by
@@ -2594,11 +2636,11 @@ export function composerToolsButtonRectExpression() {
2594
2636
  }
2595
2637
  /** Click point for a tools-menu entry, matched by its visible label. */
2596
2638
  export function composerToolEntryRectExpression(label) {
2597
- const labelJson = JSON.stringify(label);
2639
+ const candidatesJson = JSON.stringify(composerToolMenuTexts(label).map((text) => text.toLowerCase()));
2598
2640
  return `(() => {${CLICK_POINT_SNIPPET}
2599
- const wanted = ${labelJson}.trim().toLowerCase();
2641
+ const candidates = ${candidatesJson};
2600
2642
  const leaves = [...document.querySelectorAll("div,span,button,a")].filter((el) => el.children.length === 0);
2601
- const leaf = leaves.find((el) => (el.textContent || "").trim().toLowerCase() === wanted);
2643
+ const leaf = leaves.find((el) => candidates.includes((el.textContent || "").trim().toLowerCase()));
2602
2644
  if (!leaf) {
2603
2645
  const available = [...new Set(leaves.map((el) => (el.textContent || "").trim()).filter((t) => t.length > 1 && t.length < 30))].slice(0, 20);
2604
2646
  return { ok: false, reason: "tool not found in the composer tools menu", available };
@@ -2611,8 +2653,12 @@ export function composerToolEntryRectExpression(label) {
2611
2653
  export function activeComposerToolsExpression(labels) {
2612
2654
  const labelsJson = JSON.stringify(labels);
2613
2655
  return `(() => {
2656
+ // The token normally lands inside the editor, but some builds render it as
2657
+ // a pill in the surrounding composer form - check both before deciding a
2658
+ // selection did not take.
2614
2659
  const el = document.querySelector('#prompt-textarea,[contenteditable="true"]');
2615
- const text = el ? (el.innerText || "") : "";
2660
+ const form = el ? (el.closest("form") || el.parentElement) : null;
2661
+ const text = (el ? el.innerText || "" : "") + String.fromCharCode(10) + (form ? form.innerText || "" : "");
2616
2662
  return { ok: true, active: ${labelsJson}.filter((label) => text.includes(label)) };
2617
2663
  })()`;
2618
2664
  }
@@ -2649,8 +2695,9 @@ export function composerTextStateExpression(expectedText, toolLabels = []) {
2649
2695
  export async function enableComposerTools(cdp, labels) {
2650
2696
  const enabled = [];
2651
2697
  for (const label of labels) {
2652
- const already = await cdp.evaluate(activeComposerToolsExpression([label]));
2653
- if ((already?.active ?? []).includes(label)) {
2698
+ const activeProbe = composerToolMenuTexts(label);
2699
+ const already = await cdp.evaluate(activeComposerToolsExpression(activeProbe));
2700
+ if ((already?.active ?? []).length > 0) {
2654
2701
  enabled.push(label);
2655
2702
  continue;
2656
2703
  }
@@ -2678,12 +2725,33 @@ export async function enableComposerTools(cdp, labels) {
2678
2725
  const activeDeadline = Date.now() + 8_000;
2679
2726
  let active = false;
2680
2727
  for (;;) {
2681
- const state = await cdp.evaluate(activeComposerToolsExpression([label]));
2682
- active = (state?.active ?? []).includes(label);
2728
+ const state = await cdp.evaluate(activeComposerToolsExpression(activeProbe));
2729
+ active = (state?.active ?? []).length > 0;
2683
2730
  if (active || Date.now() >= activeDeadline)
2684
2731
  break;
2685
2732
  await sleep(250);
2686
2733
  }
2734
+ if (!active) {
2735
+ // One retry: the menu can close on a click that lands as the popover is
2736
+ // still settling, which looks identical to a refused selection.
2737
+ const retryButton = await cdp.evaluate(composerToolsButtonRectExpression());
2738
+ if (retryButton.ok && retryButton.x !== undefined && retryButton.y !== undefined) {
2739
+ await dispatchMouseClickAt(cdp, retryButton.x, retryButton.y);
2740
+ await sleep(1_000);
2741
+ const retryEntry = await cdp.evaluate(composerToolEntryRectExpression(label));
2742
+ if (retryEntry.ok && retryEntry.x !== undefined && retryEntry.y !== undefined) {
2743
+ await dispatchMouseClickAt(cdp, retryEntry.x, retryEntry.y);
2744
+ const retryDeadline = Date.now() + 8_000;
2745
+ for (;;) {
2746
+ const state = await cdp.evaluate(activeComposerToolsExpression(activeProbe));
2747
+ active = (state?.active ?? []).length > 0;
2748
+ if (active || Date.now() >= retryDeadline)
2749
+ break;
2750
+ await sleep(250);
2751
+ }
2752
+ }
2753
+ }
2754
+ }
2687
2755
  if (!active)
2688
2756
  throw new Error(`Selected "${label}" but the composer never showed it as active.`);
2689
2757
  enabled.push(label);
@@ -2778,6 +2846,27 @@ export function chunkComposerText(text, size = COMPOSER_INSERT_CHUNK_CHARS) {
2778
2846
  chunks.push(current);
2779
2847
  return chunks;
2780
2848
  }
2849
+ /**
2850
+ * The start control a deep research run shows after the prompt posts. It has a
2851
+ * countdown ring and starts on its own eventually, but prodex must not depend
2852
+ * on that: a run left waiting produced zero assistant messages for 30+ minutes
2853
+ * (measured live), because nothing pressed it.
2854
+ */
2855
+ export function deepResearchStartButtonRectExpression() {
2856
+ return `(() => {${CLICK_POINT_SNIPPET}
2857
+ const buttons = [...document.querySelectorAll('button,[role="button"]')];
2858
+ const target = buttons.find((b) => {
2859
+ const text = ((b.innerText || "") + " " + (b.getAttribute("aria-label") || "")).trim();
2860
+ // "Start dictation" and "Start Voice" live in the same composer and
2861
+ // would otherwise swallow this click - pressing the microphone instead
2862
+ // of starting the research (measured live).
2863
+ if (/dictation|voice|mic|음성|받아쓰기/i.test(text)) return false;
2864
+ return /^(start|시작)\\s*$|start research|시작하기|리서치 시작|조사 시작/i.test(text);
2865
+ });
2866
+ if (!target) return { ok: false, reason: "no deep research start button" };
2867
+ return clickPoint(target);
2868
+ })()`;
2869
+ }
2781
2870
  export function submitExpression() {
2782
2871
  return `(() => {
2783
2872
  ${composerExpressionHelpers()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",