@youdie006/prodex 0.24.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.
- package/dist/bundle.js +6 -1
- package/dist/chatgpt-browser.js +143 -44
- package/dist/cli-pro.js +4 -3
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -12,10 +12,14 @@ export async function buildDryRunBundle(root, input) {
|
|
|
12
12
|
input.prompt.trim()
|
|
13
13
|
];
|
|
14
14
|
const files = [];
|
|
15
|
+
// The prompt leads so the instruction is never buried under file dumps.
|
|
16
|
+
const sendSections = [input.prompt.trim()];
|
|
15
17
|
for (const file of input.files) {
|
|
16
18
|
const content = await readRepoFile(root, file, { maxLines: 500 });
|
|
17
19
|
files.push({ path: file, role: "context", bytes: Buffer.byteLength(content.content, "utf8") });
|
|
18
|
-
|
|
20
|
+
const fileSection = ["", `## File: ${file}`, "", "```text", content.content, "```"];
|
|
21
|
+
sections.push(...fileSection);
|
|
22
|
+
sendSections.push(...fileSection);
|
|
19
23
|
}
|
|
20
24
|
return {
|
|
21
25
|
schema_version: SCHEMA_VERSION,
|
|
@@ -24,6 +28,7 @@ export async function buildDryRunBundle(root, input) {
|
|
|
24
28
|
prompt: input.prompt,
|
|
25
29
|
files,
|
|
26
30
|
text: sections.join("\n"),
|
|
31
|
+
sendText: sendSections.join("\n"),
|
|
27
32
|
created_at: nowIso()
|
|
28
33
|
};
|
|
29
34
|
}
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -402,8 +402,63 @@ export function isUsableChatGptAnswer(answer) {
|
|
|
402
402
|
if (headerOnly)
|
|
403
403
|
return false;
|
|
404
404
|
}
|
|
405
|
+
// A composer tool renders its own progress panel into the assistant turn
|
|
406
|
+
// before any text exists. Measured on a --tool web-search send: "Searching
|
|
407
|
+
// the web" over an "Answer now" button, which the page reports as a settled
|
|
408
|
+
// two-line answer. Only treat it as a placeholder when it is the WHOLE
|
|
409
|
+
// content, so an answer that discusses searching still counts.
|
|
410
|
+
if (lineCount <= 2) {
|
|
411
|
+
const toolPanelOnly = normalized
|
|
412
|
+
.split(/\r?\n/)
|
|
413
|
+
.map((line) => line.trim())
|
|
414
|
+
.filter(Boolean)
|
|
415
|
+
.every((line) => /^(searching the web|웹\s*검색\s*중|answer now|지금\s*답변|검색\s*중)$/i.test(line));
|
|
416
|
+
if (toolPanelOnly)
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
405
419
|
return true;
|
|
406
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Who decides the answer is finished: the transcript, when it can be read.
|
|
423
|
+
*
|
|
424
|
+
* The page only guesses - a tool's progress panel looks exactly like a settled
|
|
425
|
+
* two-line answer, and a consult once returned "Searching the web / Answer now"
|
|
426
|
+
* as its result. When the transcript is reachable AND belongs to this consult,
|
|
427
|
+
* its state is authoritative: "pending" keeps the wait alive even if the page
|
|
428
|
+
* looks done. "unavailable" (unreachable, or a different conversation) is the
|
|
429
|
+
* only case that falls back to reading the page.
|
|
430
|
+
*/
|
|
431
|
+
export function classifyTranscriptRead(state, sentPrompt) {
|
|
432
|
+
if (!state)
|
|
433
|
+
return "unavailable";
|
|
434
|
+
if (!transcriptMatchesSentPrompt(state.userText, sentPrompt))
|
|
435
|
+
return "unavailable";
|
|
436
|
+
if (state.ok)
|
|
437
|
+
return "answer";
|
|
438
|
+
return state.reason === "answer_not_finished" || state.reason === "no_assistant_message" || state.reason === "answer_empty"
|
|
439
|
+
? "pending"
|
|
440
|
+
: "unavailable";
|
|
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
|
+
}
|
|
407
462
|
export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
|
|
408
463
|
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating;
|
|
409
464
|
}
|
|
@@ -1750,25 +1805,23 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1750
1805
|
warnings: []
|
|
1751
1806
|
};
|
|
1752
1807
|
}
|
|
1753
|
-
|
|
1754
|
-
* Read the answer from the conversation transcript, or undefined when it is not
|
|
1755
|
-
* there yet. The transcript trails the rendered stream by a beat, so callers
|
|
1756
|
-
* either poll it or fall back to the DOM text.
|
|
1757
|
-
*/
|
|
1758
|
-
async function readTranscriptAnswer(page, conversationId) {
|
|
1808
|
+
async function readTranscriptAnswer(page, conversationId, sentPrompt) {
|
|
1759
1809
|
let transcript;
|
|
1760
1810
|
try {
|
|
1761
1811
|
transcript = await evaluateOnPage(page, transcriptAnswerExpression(conversationId), { timeoutMs: 30_000 });
|
|
1762
1812
|
}
|
|
1763
1813
|
catch {
|
|
1764
|
-
// Transcript
|
|
1814
|
+
// Transcript unreachable (endpoint changed, transient failure): the DOM
|
|
1765
1815
|
// reader still runs, so this never blocks a send.
|
|
1766
|
-
return
|
|
1816
|
+
return { classification: "unavailable" };
|
|
1767
1817
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1818
|
+
const classification = classifyTranscriptRead(transcript, sentPrompt);
|
|
1819
|
+
if (classification !== "answer")
|
|
1820
|
+
return { classification };
|
|
1770
1821
|
const answer = resolveTranscriptCitations(transcript.text, transcript.references).trim();
|
|
1771
|
-
return answer.length > 0
|
|
1822
|
+
return answer.length > 0
|
|
1823
|
+
? { classification: "answer", answer: { answer, modelSlug: transcript.modelSlug } }
|
|
1824
|
+
: { classification: "pending" };
|
|
1772
1825
|
}
|
|
1773
1826
|
export async function sendChatGptPrompt(options) {
|
|
1774
1827
|
const port = resolveCdpPort(options.port);
|
|
@@ -2084,7 +2137,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
2084
2137
|
await sleep(5_000);
|
|
2085
2138
|
continue;
|
|
2086
2139
|
}
|
|
2087
|
-
if (lastState.ok && lastState.report.trim().length > 0) {
|
|
2140
|
+
if (lastState.ok && lastState.report.trim().length > 0 && transcriptMatchesSentPrompt(lastState.userText, options.prompt)) {
|
|
2088
2141
|
const report = resolveTranscriptCitations(lastState.report, lastState.references).trim();
|
|
2089
2142
|
emitProgress("answered", `deep research report (${report.length} chars)`);
|
|
2090
2143
|
return {
|
|
@@ -2110,24 +2163,28 @@ export async function sendChatGptPrompt(options) {
|
|
|
2110
2163
|
});
|
|
2111
2164
|
}
|
|
2112
2165
|
let recoveredNavigations = 0;
|
|
2166
|
+
let lastTranscriptClassification;
|
|
2113
2167
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
2114
2168
|
while (Date.now() - started < timeoutMs) {
|
|
2115
2169
|
await sleep(1000);
|
|
2116
2170
|
try {
|
|
2117
2171
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2172
|
+
// First conversation id wins. Re-deriving it every poll would let a tab
|
|
2173
|
+
// that wandered to another thread redirect the read to a stranger's
|
|
2174
|
+
// conversation - and the prompt check below is the second line of defence,
|
|
2175
|
+
// not the first.
|
|
2118
2176
|
if (!transcriptConversationId && finalState?.url)
|
|
2119
2177
|
transcriptConversationId = conversationIdFromThreadUrl(finalState.url);
|
|
2120
2178
|
// The transcript is the same data the page renders, minus the rendering:
|
|
2121
2179
|
// markdown instead of flattened innerText, an explicit finish state
|
|
2122
2180
|
// instead of caret heuristics, and the model that actually answered.
|
|
2123
2181
|
if (transcriptConversationId && !finalState.generating) {
|
|
2124
|
-
const transcript = await readTranscriptAnswer(page, transcriptConversationId);
|
|
2125
|
-
|
|
2126
|
-
|
|
2182
|
+
const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
|
|
2183
|
+
lastTranscriptClassification = transcript.classification;
|
|
2184
|
+
if (transcript.answer)
|
|
2185
|
+
return transcriptResult(transcript.answer);
|
|
2127
2186
|
}
|
|
2128
|
-
|
|
2129
|
-
// conversation id is known, a wandering tab is harmless.
|
|
2130
|
-
if (!transcriptConversationId && pinnedThreadUrl && finalState?.url && !chatGptUrlsReferToSameTarget(finalState.url, pinnedThreadUrl)) {
|
|
2187
|
+
if (shouldRecoverThreadNavigation({ pinnedThreadUrl, currentUrl: finalState?.url, lastTranscriptClassification })) {
|
|
2131
2188
|
if (recoveredNavigations >= 2) {
|
|
2132
2189
|
throw new ChatGptBrowserBlockerError({
|
|
2133
2190
|
code: "thread_navigated_away",
|
|
@@ -2167,12 +2224,16 @@ export async function sendChatGptPrompt(options) {
|
|
|
2167
2224
|
// Give the transcript that beat: it carries markdown (tables and fenced
|
|
2168
2225
|
// code that innerText flattens) and the model that actually answered.
|
|
2169
2226
|
if (transcriptConversationId) {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2227
|
+
const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
|
|
2228
|
+
lastTranscriptClassification = transcript.classification;
|
|
2229
|
+
if (transcript.answer)
|
|
2230
|
+
return transcriptResult(transcript.answer);
|
|
2231
|
+
// The transcript can read this conversation and says it is not done:
|
|
2232
|
+
// believe it over a page that merely looks settled. A tool's progress
|
|
2233
|
+
// panel renders exactly like a two-line answer, and that is how a
|
|
2234
|
+
// consult once returned "Searching the web / Answer now" as its result.
|
|
2235
|
+
if (transcript.classification === "pending")
|
|
2236
|
+
continue;
|
|
2176
2237
|
}
|
|
2177
2238
|
break;
|
|
2178
2239
|
}
|
|
@@ -3030,7 +3091,7 @@ export function deepResearchUnreadableBlocker(threadUrl) {
|
|
|
3030
3091
|
*/
|
|
3031
3092
|
export function transcriptAnswerExpression(conversationId) {
|
|
3032
3093
|
return `(async () => {
|
|
3033
|
-
const fail = (reason, extra) => Object.assign({ ok: false, reason, status: "", endTurn: false, isComplete: false, text: "", modelSlug: "", references: [] }, extra || {});
|
|
3094
|
+
const fail = (reason, extra) => Object.assign({ ok: false, reason, status: "", endTurn: false, isComplete: false, text: "", modelSlug: "", references: [], userText: "" }, extra || {});
|
|
3034
3095
|
let token = "";
|
|
3035
3096
|
try {
|
|
3036
3097
|
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
@@ -3063,7 +3124,9 @@ export function transcriptAnswerExpression(conversationId) {
|
|
|
3063
3124
|
const message = chain.find(
|
|
3064
3125
|
(entry) => entry && entry.author && entry.author.role === "assistant" && entry.content && entry.content.content_type === "text"
|
|
3065
3126
|
);
|
|
3066
|
-
|
|
3127
|
+
const userMessage = chain.find((entry) => entry && entry.author && entry.author.role === "user" && entry.content);
|
|
3128
|
+
const userText = userMessage ? (userMessage.content.parts || []).filter((part) => typeof part === "string").join("") : "";
|
|
3129
|
+
if (!message) return fail("no_assistant_message", { userText });
|
|
3067
3130
|
const parts = (message.content.parts || []).filter((part) => typeof part === "string");
|
|
3068
3131
|
const text = parts.join("");
|
|
3069
3132
|
const metadata = message.metadata || {};
|
|
@@ -3073,13 +3136,41 @@ export function transcriptAnswerExpression(conversationId) {
|
|
|
3073
3136
|
isComplete: metadata.is_complete === true,
|
|
3074
3137
|
text,
|
|
3075
3138
|
modelSlug: metadata.model_slug || "",
|
|
3076
|
-
references: Array.isArray(metadata.content_references) ? metadata.content_references : []
|
|
3139
|
+
references: Array.isArray(metadata.content_references) ? metadata.content_references : [],
|
|
3140
|
+
userText
|
|
3077
3141
|
};
|
|
3078
3142
|
if (state.status !== "finished_successfully" || !state.endTurn) return fail("answer_not_finished", state);
|
|
3079
3143
|
if (!text) return fail("answer_empty", state);
|
|
3080
3144
|
return Object.assign({ ok: true, reason: "" }, state);
|
|
3081
3145
|
})()`;
|
|
3082
3146
|
}
|
|
3147
|
+
const NORMALIZED_PROMPT_MATCH_CHARS = 120;
|
|
3148
|
+
/**
|
|
3149
|
+
* Does this transcript belong to the consult that is waiting on it?
|
|
3150
|
+
*
|
|
3151
|
+
* The browser is shared with the user and other agents. Reading "whatever
|
|
3152
|
+
* conversation the tab shows" once cost a consult its answer - prodex saved a
|
|
3153
|
+
* different conversation as the result. Identity is checked against the prompt
|
|
3154
|
+
* that was actually sent, normalized because ChatGPT re-wraps whitespace, and
|
|
3155
|
+
* as a containment test because the transcript wraps the prompt: a composer
|
|
3156
|
+
* tool prefixes it ("@Deep research ...") and attachments append to it.
|
|
3157
|
+
*/
|
|
3158
|
+
export function transcriptMatchesSentPrompt(userText, sentPrompt) {
|
|
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();
|
|
3167
|
+
const seen = normalize(userText);
|
|
3168
|
+
const sent = normalize(sentPrompt);
|
|
3169
|
+
if (seen.length === 0 || sent.length === 0)
|
|
3170
|
+
return false;
|
|
3171
|
+
const expected = sent.slice(0, NORMALIZED_PROMPT_MATCH_CHARS);
|
|
3172
|
+
return seen.includes(expected);
|
|
3173
|
+
}
|
|
3083
3174
|
// ChatGPT marks citations with private-use delimiters (U+E200 opens, U+E202
|
|
3084
3175
|
// separates, U+E201 closes) and keeps the real sources in content_references.
|
|
3085
3176
|
const CITATION_DELIMITER_PATTERN = /[\uE200-\uE206]/;
|
|
@@ -3122,7 +3213,7 @@ export function resolveTranscriptCitations(text, references = []) {
|
|
|
3122
3213
|
}
|
|
3123
3214
|
export function deepResearchReportExpression(conversationId) {
|
|
3124
3215
|
return `(async () => {
|
|
3125
|
-
const fail = (reason, status) => ({ ok: false, reason, status: status || "", report: "", chars: 0, references: [] });
|
|
3216
|
+
const fail = (reason, status, userText) => ({ ok: false, reason, status: status || "", report: "", chars: 0, references: [], userText: userText || "" });
|
|
3126
3217
|
let token = "";
|
|
3127
3218
|
try {
|
|
3128
3219
|
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
@@ -3143,7 +3234,8 @@ export function deepResearchReportExpression(conversationId) {
|
|
|
3143
3234
|
} catch (error) {
|
|
3144
3235
|
return fail("conversation_error");
|
|
3145
3236
|
}
|
|
3146
|
-
const
|
|
3237
|
+
const mapping = (conversation && conversation.mapping) || {};
|
|
3238
|
+
const nodes = Object.keys(mapping).map((key) => mapping[key]);
|
|
3147
3239
|
const widgetNode = nodes.find(
|
|
3148
3240
|
(node) => node && node.message && node.message.metadata && node.message.metadata.chatgpt_sdk && node.message.metadata.chatgpt_sdk.widget_state
|
|
3149
3241
|
);
|
|
@@ -3155,12 +3247,22 @@ export function deepResearchReportExpression(conversationId) {
|
|
|
3155
3247
|
return fail("widget_state_unparsable");
|
|
3156
3248
|
}
|
|
3157
3249
|
const status = (state && state.status) || "";
|
|
3250
|
+
const chain = [];
|
|
3251
|
+
let walkId = conversation && conversation.current_node;
|
|
3252
|
+
let walkGuard = 0;
|
|
3253
|
+
while (walkId && mapping[walkId] && walkGuard < 2000) {
|
|
3254
|
+
walkGuard += 1;
|
|
3255
|
+
if (mapping[walkId].message) chain.push(mapping[walkId].message);
|
|
3256
|
+
walkId = mapping[walkId].parent;
|
|
3257
|
+
}
|
|
3258
|
+
const userNode = chain.find((entry) => entry && entry.author && entry.author.role === "user" && entry.content);
|
|
3259
|
+
const userText = userNode ? (userNode.content.parts || []).filter((part) => typeof part === "string").join("") : "";
|
|
3158
3260
|
const message = (state && state.report_message) || null;
|
|
3159
3261
|
const parts = message && message.content && message.content.parts;
|
|
3160
3262
|
const report = Array.isArray(parts) ? parts.filter((part) => typeof part === "string").join("") : "";
|
|
3161
3263
|
const references = message && message.metadata && Array.isArray(message.metadata.content_references) ? message.metadata.content_references : [];
|
|
3162
|
-
if (!report) return fail("report_not_ready", status);
|
|
3163
|
-
return { ok: true, reason: "", status, report, chars: report.length, references };
|
|
3264
|
+
if (!report) return fail("report_not_ready", status, userText);
|
|
3265
|
+
return { ok: true, reason: "", status, report, chars: report.length, references, userText };
|
|
3164
3266
|
})()`;
|
|
3165
3267
|
}
|
|
3166
3268
|
/**
|
|
@@ -3296,17 +3398,11 @@ export function answerExpression() {
|
|
|
3296
3398
|
});
|
|
3297
3399
|
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
3298
3400
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
3299
|
-
//
|
|
3300
|
-
//
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
|
|
3304
|
-
const turnAnswers = assistantMessages.length > 0 ? [] : [...document.querySelectorAll('[data-testid^="conversation-turn"]')]
|
|
3305
|
-
.filter((turn) => !turn.querySelector('[data-message-author-role="user"]'))
|
|
3306
|
-
.map((turn) => ({ role: "assistant", text: (turn.innerText || "").trim(), modelSlug: undefined }))
|
|
3307
|
-
.filter((turn) => turn.text.length > 0);
|
|
3308
|
-
const effectiveAssistants = assistantMessages.length > 0 ? assistantMessages : turnAnswers;
|
|
3309
|
-
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);
|
|
3310
3406
|
const buttons = [...document.querySelectorAll('button,[role="button"]')]
|
|
3311
3407
|
.filter((node) => !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length))
|
|
3312
3408
|
.filter((node) => !node.closest(excludedTextSelector))
|
|
@@ -3319,13 +3415,16 @@ export function answerExpression() {
|
|
|
3319
3415
|
return {
|
|
3320
3416
|
title: document.title,
|
|
3321
3417
|
url: location.href,
|
|
3322
|
-
|
|
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,
|
|
3323
3422
|
textSample: text.slice(0, 12000),
|
|
3324
3423
|
blockerTextSample: visibleTextOutsideMessages(excludedTextSelector).slice(0, 12000),
|
|
3325
3424
|
blockerScanTextSample: visibleTextOutsideMessages(blockerScanExcludedSelector).slice(0, 12000),
|
|
3326
3425
|
visibleButtonLabels: buttons,
|
|
3327
3426
|
generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || buttons.some((label) => generatingControlPattern.test(label)),
|
|
3328
|
-
assistantMessageCount:
|
|
3427
|
+
assistantMessageCount: assistantMessages.length,
|
|
3329
3428
|
userMessageCount: userMessages.length,
|
|
3330
3429
|
// ChatGPT tags each assistant message with the model that produced it -
|
|
3331
3430
|
// the only ground truth for "did the Pro selection actually take".
|
package/dist/cli-pro.js
CHANGED
|
@@ -52,7 +52,7 @@ export async function runChatgptCommand(rest, io) {
|
|
|
52
52
|
const task = await targetStore.createTask({
|
|
53
53
|
source: "codex",
|
|
54
54
|
title: "GPT Pro smoke",
|
|
55
|
-
prompt: bundle.
|
|
55
|
+
prompt: bundle.sendText,
|
|
56
56
|
repo_id: "default",
|
|
57
57
|
provenance: {
|
|
58
58
|
adapter: "chatgpt-control",
|
|
@@ -852,7 +852,8 @@ export async function runAskProCommand(rest, io) {
|
|
|
852
852
|
const task = await targetStore.createTask({
|
|
853
853
|
source: "codex",
|
|
854
854
|
title: "GPT Pro consult",
|
|
855
|
-
|
|
855
|
+
// Record what ChatGPT actually received, not the preview rendering.
|
|
856
|
+
prompt: bundle.sendText,
|
|
856
857
|
repo_id: "default",
|
|
857
858
|
files: files.map((file) => ({ path: file, role: "context" })),
|
|
858
859
|
provenance: {
|
|
@@ -901,7 +902,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
901
902
|
// fast. A dead holder is still reaped immediately.
|
|
902
903
|
const sendOnce = () => withBrowserSendLock(busyWaitMs ?? browserTimeoutMs ?? 0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
903
904
|
port: browserPort,
|
|
904
|
-
prompt: bundle.
|
|
905
|
+
prompt: bundle.sendText,
|
|
905
906
|
targetUrl: normalizedTargetUrl,
|
|
906
907
|
timeoutMs: browserTimeoutMs,
|
|
907
908
|
...(attachments.length > 0 ? { attachments } : {}),
|