@youdie006/prodex 0.25.0 → 0.25.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.
@@ -439,6 +439,26 @@ export function classifyTranscriptRead(state, sentPrompt) {
439
439
  ? "pending"
440
440
  : "unavailable";
441
441
  }
442
+ /**
443
+ * Should prodex drag the tab back to the thread it pinned?
444
+ *
445
+ * Only when the pin is a real conversation, and only when the page is the only
446
+ * way left to read the answer. Pinning a project or new-chat page (the url a
447
+ * send starts on, before ChatGPT rewrites it to /c/<id>) made prodex treat its
448
+ * OWN conversation as a stray tab and navigate away from the answer it was
449
+ * waiting for. And while the transcript can answer, moving someone else's tab
450
+ * back buys nothing.
451
+ */
452
+ export function shouldRecoverThreadNavigation(args) {
453
+ const { pinnedThreadUrl, currentUrl, lastTranscriptClassification } = args;
454
+ if (!pinnedThreadUrl || !currentUrl)
455
+ return false;
456
+ if (!conversationIdFromThreadUrl(pinnedThreadUrl))
457
+ return false;
458
+ if (lastTranscriptClassification === "pending" || lastTranscriptClassification === "answer")
459
+ return false;
460
+ return !chatGptUrlsReferToSameTarget(currentUrl, pinnedThreadUrl);
461
+ }
442
462
  export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
443
463
  return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating;
444
464
  }
@@ -2143,6 +2163,7 @@ export async function sendChatGptPrompt(options) {
2143
2163
  });
2144
2164
  }
2145
2165
  let recoveredNavigations = 0;
2166
+ let lastTranscriptClassification;
2146
2167
  const answerIsStable = createChatGptAnswerStabilityTracker();
2147
2168
  while (Date.now() - started < timeoutMs) {
2148
2169
  await sleep(1000);
@@ -2159,12 +2180,11 @@ export async function sendChatGptPrompt(options) {
2159
2180
  // instead of caret heuristics, and the model that actually answered.
2160
2181
  if (transcriptConversationId && !finalState.generating) {
2161
2182
  const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
2183
+ lastTranscriptClassification = transcript.classification;
2162
2184
  if (transcript.answer)
2163
2185
  return transcriptResult(transcript.answer);
2164
2186
  }
2165
- // Only the DOM reader depends on which thread the tab is showing; once the
2166
- // conversation id is known, a wandering tab is harmless.
2167
- if (!transcriptConversationId && pinnedThreadUrl && finalState?.url && !chatGptUrlsReferToSameTarget(finalState.url, pinnedThreadUrl)) {
2187
+ if (shouldRecoverThreadNavigation({ pinnedThreadUrl, currentUrl: finalState?.url, lastTranscriptClassification })) {
2168
2188
  if (recoveredNavigations >= 2) {
2169
2189
  throw new ChatGptBrowserBlockerError({
2170
2190
  code: "thread_navigated_away",
@@ -2205,6 +2225,7 @@ export async function sendChatGptPrompt(options) {
2205
2225
  // code that innerText flattens) and the model that actually answered.
2206
2226
  if (transcriptConversationId) {
2207
2227
  const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
2228
+ lastTranscriptClassification = transcript.classification;
2208
2229
  if (transcript.answer)
2209
2230
  return transcriptResult(transcript.answer);
2210
2231
  // The transcript can read this conversation and says it is not done:
@@ -3135,7 +3156,14 @@ const NORMALIZED_PROMPT_MATCH_CHARS = 120;
3135
3156
  * tool prefixes it ("@Deep research ...") and attachments append to it.
3136
3157
  */
3137
3158
  export function transcriptMatchesSentPrompt(userText, sentPrompt) {
3138
- const normalize = (value) => value.replace(/\s+/g, " ").trim();
3159
+ // The composer escapes markdown when it stores what was typed ("## File" is
3160
+ // kept as "\\## File", fences as escaped backticks), so undo that before
3161
+ // comparing - otherwise every prompt carrying markdown, which is every
3162
+ // --file send, looks like a different conversation.
3163
+ const normalize = (value) => value
3164
+ .replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
3165
+ .replace(/\s+/g, " ")
3166
+ .trim();
3139
3167
  const seen = normalize(userText);
3140
3168
  const sent = normalize(sentPrompt);
3141
3169
  if (seen.length === 0 || sent.length === 0)
@@ -3370,17 +3398,11 @@ export function answerExpression() {
3370
3398
  });
3371
3399
  const assistantMessages = messages.filter((message) => message.role === "assistant");
3372
3400
  const userMessages = messages.filter((message) => message.role === "user");
3373
- // Deep research renders no assistant-role node at all: the thread is
3374
- // conversation-turn sections, the prompt in the first and the report in a
3375
- // later one (measured live - roles were ["user"] only while a research ran).
3376
- // Fall back to the last turn that is NOT the user's, so such an answer is
3377
- // readable instead of looking like "no answer" forever.
3378
- const turnAnswers = assistantMessages.length > 0 ? [] : [...document.querySelectorAll('[data-testid^="conversation-turn"]')]
3379
- .filter((turn) => !turn.querySelector('[data-message-author-role="user"]'))
3380
- .map((turn) => ({ role: "assistant", text: (turn.innerText || "").trim(), modelSlug: undefined }))
3381
- .filter((turn) => turn.text.length > 0);
3382
- const effectiveAssistants = assistantMessages.length > 0 ? assistantMessages : turnAnswers;
3383
- const assistant = effectiveAssistants.at(-1);
3401
+ // A turn without an assistant message is NOT an answer. Treating one as an
3402
+ // answer (a 0.21.3 fallback for deep research, which is read from the
3403
+ // transcript now) turned a tool's progress panel into a 28-character
3404
+ // "answer" that a consult returned as its result.
3405
+ const assistant = assistantMessages.at(-1);
3384
3406
  const buttons = [...document.querySelectorAll('button,[role="button"]')]
3385
3407
  .filter((node) => !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length))
3386
3408
  .filter((node) => !node.closest(excludedTextSelector))
@@ -3393,13 +3415,16 @@ export function answerExpression() {
3393
3415
  return {
3394
3416
  title: document.title,
3395
3417
  url: location.href,
3396
- answer: assistant ? answer : text.slice(-4000),
3418
+ // No assistant message means no answer. This used to hand back the last
3419
+ // 4000 characters of the page, which reads as sidebar and navigation text
3420
+ // dressed up as a reply.
3421
+ answer,
3397
3422
  textSample: text.slice(0, 12000),
3398
3423
  blockerTextSample: visibleTextOutsideMessages(excludedTextSelector).slice(0, 12000),
3399
3424
  blockerScanTextSample: visibleTextOutsideMessages(blockerScanExcludedSelector).slice(0, 12000),
3400
3425
  visibleButtonLabels: buttons,
3401
3426
  generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || buttons.some((label) => generatingControlPattern.test(label)),
3402
- assistantMessageCount: effectiveAssistants.length,
3427
+ assistantMessageCount: assistantMessages.length,
3403
3428
  userMessageCount: userMessages.length,
3404
3429
  // ChatGPT tags each assistant message with the model that produced it -
3405
3430
  // the only ground truth for "did the Pro selection actually take".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",