@youdie006/prodex 0.40.48 → 0.40.49
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/chatgpt-browser.js +128 -18
- package/dist/cli-pro.js +5 -0
- package/dist/continue-thread.js +5 -3
- package/docs/releasing.md +38 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1150,7 +1150,8 @@ export function shouldRecoverThreadNavigation(args) {
|
|
|
1150
1150
|
return !chatGptUrlsReferToSameTarget(currentUrl, pinnedThreadUrl);
|
|
1151
1151
|
}
|
|
1152
1152
|
export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
|
|
1153
|
-
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating
|
|
1153
|
+
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating &&
|
|
1154
|
+
state.answerRenderFailed !== true;
|
|
1154
1155
|
}
|
|
1155
1156
|
// The acceptance phase waits for the prompt to POST (a new user/assistant
|
|
1156
1157
|
// message), not for the answer. So a timeout here almost never means "slow
|
|
@@ -1193,7 +1194,8 @@ export function acceptanceTimeoutError(ctx) {
|
|
|
1193
1194
|
});
|
|
1194
1195
|
}
|
|
1195
1196
|
export function hasPartialChatGptAnswer(previousAssistantMessageCount, state) {
|
|
1196
|
-
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer)
|
|
1197
|
+
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) &&
|
|
1198
|
+
state.answerRenderFailed !== true;
|
|
1197
1199
|
}
|
|
1198
1200
|
/**
|
|
1199
1201
|
* True when the tab is on a fresh, empty ChatGPT chat: the root URL (not a
|
|
@@ -1590,6 +1592,21 @@ export function chatGptBlockerErrorFromAnswerState(state) {
|
|
|
1590
1592
|
export function chatGptBlockerFromAnswerState(state) {
|
|
1591
1593
|
return detectChatGptPageBlocker(state);
|
|
1592
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* The answer exists, but the page shows "This response couldn’t load" in
|
|
1597
|
+
* its place. Measured 2026-10-30 in the browser container, where Chromium could
|
|
1598
|
+
* not start threads: the conversation held the finished answer, and loading
|
|
1599
|
+
* the thread again drew it.
|
|
1600
|
+
*/
|
|
1601
|
+
export function answerRenderFailedBlocker(thread) {
|
|
1602
|
+
return {
|
|
1603
|
+
code: "answer_render_failed",
|
|
1604
|
+
message: "ChatGPT answered, but the page could not display the answer (it shows \"This response couldn’t load\"). No answer was returned.",
|
|
1605
|
+
retryable: false,
|
|
1606
|
+
next_step: "Recovery loads the conversation again and reads the answer.",
|
|
1607
|
+
...(thread ? { thread } : {})
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1593
1610
|
function submittedRequestBlocker(blocker, requestId, thread) {
|
|
1594
1611
|
return {
|
|
1595
1612
|
...blocker,
|
|
@@ -2884,6 +2901,64 @@ export function sidebarProjectRowPointExpression(name) {
|
|
|
2884
2901
|
return { ok: true, x: Math.round(r.x + Math.min(r.width / 3, 60)), y: Math.round(r.y + r.height / 2) };
|
|
2885
2902
|
})()`;
|
|
2886
2903
|
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Show the projects the sidebar keeps behind "Show more".
|
|
2906
|
+
*
|
|
2907
|
+
* Measured 2026-10-30 on WSL: the sidebar listed 9 project rows (4 pinned, 5
|
|
2908
|
+
* under Projects) and a "Show more" button under the Projects section;
|
|
2909
|
+
* pressing it added 16 more rows that were not in the page at all before. A
|
|
2910
|
+
* --project send for any of them failed as "project not found in sidebar (9
|
|
2911
|
+
* projects visible)". The button has no label of its own, so it is found as
|
|
2912
|
+
* the ghost button of a section that lists projects, outside every row, and
|
|
2913
|
+
* not the section's toggle; its English text is the fallback. The sidebar
|
|
2914
|
+
* offers nothing to fold the list again, so it stays shown.
|
|
2915
|
+
*/
|
|
2916
|
+
export function expandSidebarProjectsExpression() {
|
|
2917
|
+
return `(async () => {
|
|
2918
|
+
const projectRows = (root) => root.querySelectorAll("[data-app-action-sidebar-project-row]").length;
|
|
2919
|
+
const before = projectRows(document);
|
|
2920
|
+
const showMore = (button) =>
|
|
2921
|
+
!button.closest("[data-app-action-sidebar-project-row],[data-sidebar-project-container-id^='project:']") &&
|
|
2922
|
+
!button.hasAttribute("data-app-action-sidebar-section-toggle") &&
|
|
2923
|
+
!button.getAttribute("aria-label") &&
|
|
2924
|
+
(button.getAttribute("data-variant") === "ghost" || /^show more$/i.test((button.innerText || "").trim())) &&
|
|
2925
|
+
button.getClientRects().length > 0;
|
|
2926
|
+
const sections = [...document.querySelectorAll("section[data-app-action-sidebar-section]")].filter((section) => projectRows(section) > 0);
|
|
2927
|
+
const more = sections.flatMap((section) => [...section.querySelectorAll("button")]).find(showMore);
|
|
2928
|
+
if (!more) return { clicked: false, before, after: before };
|
|
2929
|
+
more.click();
|
|
2930
|
+
for (let waited = 0; waited < 3000 && projectRows(document) <= before; waited += 100) {
|
|
2931
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2932
|
+
}
|
|
2933
|
+
return { clicked: true, before, after: projectRows(document) };
|
|
2934
|
+
})()`;
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* The sidebar row of an existing project, shown first if it is behind "Show
|
|
2938
|
+
* more". Polls, because right after a --new-chat navigation the sidebar's
|
|
2939
|
+
* Projects section has not hydrated yet (measured live: "0 projects visible"
|
|
2940
|
+
* on the first read, rows appear moments later) - the same class of race as
|
|
2941
|
+
* the model-selector button after new-chat (0.15.7).
|
|
2942
|
+
*/
|
|
2943
|
+
export async function findSidebarProjectRow(cdp, project, timeoutMs = 6_000) {
|
|
2944
|
+
let hit = { ok: false };
|
|
2945
|
+
let expanded = false;
|
|
2946
|
+
const deadline = Date.now() + timeoutMs;
|
|
2947
|
+
for (;;) {
|
|
2948
|
+
hit = await cdp.evaluate(projectItemRectExpression(project));
|
|
2949
|
+
if (hit.ok && hit.x !== undefined && hit.y !== undefined)
|
|
2950
|
+
return hit;
|
|
2951
|
+
if (!expanded && /project not found in sidebar/.test(hit.reason ?? "")) {
|
|
2952
|
+
const shown = await cdp.evaluate(expandSidebarProjectsExpression());
|
|
2953
|
+
expanded = shown?.clicked === true;
|
|
2954
|
+
if (expanded)
|
|
2955
|
+
continue;
|
|
2956
|
+
}
|
|
2957
|
+
if (Date.now() >= deadline)
|
|
2958
|
+
return hit;
|
|
2959
|
+
await sleep(300);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2887
2962
|
/** Hover a project row so its hover-only controls can be clicked. A no-op on the older sidebar. */
|
|
2888
2963
|
async function revealSidebarProjectControls(cdp, name) {
|
|
2889
2964
|
const point = await cdp.evaluate(sidebarProjectRowPointExpression(name));
|
|
@@ -3775,21 +3850,7 @@ async function waitForComposerProjectBinding(cdp, project, timeoutMs) {
|
|
|
3775
3850
|
// question, not this one's.
|
|
3776
3851
|
async function navigateToExistingProject(cdp, project) {
|
|
3777
3852
|
const hrefBefore = await cdp.evaluate("location.href");
|
|
3778
|
-
|
|
3779
|
-
// --new-chat navigation the sidebar's Projects section has not hydrated yet
|
|
3780
|
-
// (measured live: "0 projects visible" on the first read, rows appear
|
|
3781
|
-
// moments later) - the same class of race as the model-selector button
|
|
3782
|
-
// after new-chat (0.15.7).
|
|
3783
|
-
let hit = { ok: false };
|
|
3784
|
-
const projectDeadline = Date.now() + 6_000;
|
|
3785
|
-
for (;;) {
|
|
3786
|
-
hit = await cdp.evaluate(projectItemRectExpression(project));
|
|
3787
|
-
if (hit.ok && hit.x !== undefined && hit.y !== undefined)
|
|
3788
|
-
break;
|
|
3789
|
-
if (Date.now() >= projectDeadline)
|
|
3790
|
-
break;
|
|
3791
|
-
await sleep(300);
|
|
3792
|
-
}
|
|
3853
|
+
const hit = await findSidebarProjectRow(cdp, project);
|
|
3793
3854
|
if (!hit.ok || hit.x === undefined || hit.y === undefined) {
|
|
3794
3855
|
const detail = hit.reason && hit.reason !== "project not found in sidebar" ? ` (${hit.reason})` : "";
|
|
3795
3856
|
throw new Error(`ChatGPT project not found in sidebar: ${project}${detail} List the visible names with \`prodex pro browser projects\`.`);
|
|
@@ -3981,6 +4042,8 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
3981
4042
|
let lastAnswer = "";
|
|
3982
4043
|
let lastObservedUrl = "";
|
|
3983
4044
|
let completed = false;
|
|
4045
|
+
let renderFailed = false;
|
|
4046
|
+
let reloadedForRenderFailure = false;
|
|
3984
4047
|
try {
|
|
3985
4048
|
await cdp.send("Runtime.enable");
|
|
3986
4049
|
// In-tab navigation (location.assign, not Page.navigate which has crashed the
|
|
@@ -4033,6 +4096,21 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
4033
4096
|
thread: url
|
|
4034
4097
|
});
|
|
4035
4098
|
}
|
|
4099
|
+
// The tab may already be on the thread with the page's error in place
|
|
4100
|
+
// of the answer, so recovery loads the thread once more to draw it.
|
|
4101
|
+
if (state.answerRenderFailed && !state.generating && state.assistantMessageCount > 0) {
|
|
4102
|
+
renderFailed = true;
|
|
4103
|
+
stableRuns = 0;
|
|
4104
|
+
lastAnswer = "";
|
|
4105
|
+
if (!reloadedForRenderFailure) {
|
|
4106
|
+
reloadedForRenderFailure = true;
|
|
4107
|
+
await evaluateOnPage(page.page, markDocumentForReloadExpression());
|
|
4108
|
+
await navigateIdleChatGptPage(page.page, url);
|
|
4109
|
+
await waitForReloadedDocument(page.page, deadline);
|
|
4110
|
+
}
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
4113
|
+
renderFailed = false;
|
|
4036
4114
|
// Require a REAL assistant message, not answerExpression's page-chrome
|
|
4037
4115
|
// fallback (empty assistant returns sidebar/nav text): the thread's
|
|
4038
4116
|
// conversation loads asynchronously after navigation, so keep polling.
|
|
@@ -4057,6 +4135,13 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
4057
4135
|
}
|
|
4058
4136
|
if (!completed) {
|
|
4059
4137
|
const targetMatched = Boolean(state && chatGptUrlsReferToSameTarget(state.url, url));
|
|
4138
|
+
if (targetMatched && renderFailed) {
|
|
4139
|
+
throw new ChatGptBrowserBlockerError({
|
|
4140
|
+
...answerRenderFailedBlocker(url),
|
|
4141
|
+
retryable: true,
|
|
4142
|
+
next_step: "The page still could not display the answer after loading the conversation again. Check that the browser is not short of memory or processes, then rerun recovery."
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4060
4145
|
const hasUsableAnswer = Boolean(state && state.assistantMessageCount > 0 && isUsableChatGptAnswer(state.answer));
|
|
4061
4146
|
const code = !targetMatched
|
|
4062
4147
|
? "thread_target_mismatch"
|
|
@@ -4609,6 +4694,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
4609
4694
|
expandingPolls = 0;
|
|
4610
4695
|
let nonTextPolls = 0;
|
|
4611
4696
|
let nonTextSettled = false;
|
|
4697
|
+
let renderFailedPolls = 0;
|
|
4612
4698
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
4613
4699
|
while (Date.now() - started < timeoutMs) {
|
|
4614
4700
|
await sleep(1000);
|
|
@@ -4680,6 +4766,16 @@ export async function sendChatGptPrompt(options) {
|
|
|
4680
4766
|
continue;
|
|
4681
4767
|
}
|
|
4682
4768
|
nonTextPolls = 0;
|
|
4769
|
+
// Two reads in a row, so a failure the renderer is about to replace does
|
|
4770
|
+
// not end the send.
|
|
4771
|
+
if (finalState.answerRenderFailed && !finalState.generating && finalState.assistantMessageCount > beforeSubmit.assistantMessageCount) {
|
|
4772
|
+
renderFailedPolls += 1;
|
|
4773
|
+
if (renderFailedPolls >= 2) {
|
|
4774
|
+
throw attachSendWarnings(new ChatGptBrowserBlockerError(submittedRequestBlocker(answerRenderFailedBlocker(pinnedThreadUrl), requestId, pinnedThreadUrl)), sendWarnings);
|
|
4775
|
+
}
|
|
4776
|
+
continue;
|
|
4777
|
+
}
|
|
4778
|
+
renderFailedPolls = 0;
|
|
4683
4779
|
if (!hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, finalState))
|
|
4684
4780
|
continue;
|
|
4685
4781
|
// A "fresh" answer must also be stable: ChatGPT can momentarily look done
|
|
@@ -5012,7 +5108,10 @@ export function resolveProjectToDelete(projects, request) {
|
|
|
5012
5108
|
return { ok: true, id: matches[0].id, name: matches[0].name };
|
|
5013
5109
|
}
|
|
5014
5110
|
export async function listChatGptProjectsWithIds(input = {}) {
|
|
5015
|
-
return listVisibleChatGptNavigation(input,
|
|
5111
|
+
return listVisibleChatGptNavigation(input, `(async () => {
|
|
5112
|
+
await ${expandSidebarProjectsExpression()};
|
|
5113
|
+
return ${projectsWithIdsExpression()};
|
|
5114
|
+
})()`);
|
|
5016
5115
|
}
|
|
5017
5116
|
/** Project deletion has no bounded visible-DOM implementation. */
|
|
5018
5117
|
export async function deleteChatGptProject(_input) {
|
|
@@ -5064,6 +5163,11 @@ export async function listChatGptSidebarProjects(input = {}) {
|
|
|
5064
5163
|
break;
|
|
5065
5164
|
await sleep(300);
|
|
5066
5165
|
}
|
|
5166
|
+
if (projects.length > 0) {
|
|
5167
|
+
const shown = await evaluateOnPage(page, expandSidebarProjectsExpression());
|
|
5168
|
+
if (shown?.clicked)
|
|
5169
|
+
projects = await evaluateOnPage(page, sidebarProjectNamesExpression());
|
|
5170
|
+
}
|
|
5067
5171
|
return { url: status.url, projects };
|
|
5068
5172
|
}
|
|
5069
5173
|
/**
|
|
@@ -6780,6 +6884,12 @@ export function answerExpression() {
|
|
|
6780
6884
|
return Boolean(lastUser.node.compareDocumentPosition(img) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
6781
6885
|
});
|
|
6782
6886
|
})(),
|
|
6887
|
+
// The newer answer renderer, when it cannot draw an answer, puts
|
|
6888
|
+
// div[role=status][data-dil-render-failed] reading "This response
|
|
6889
|
+
// couldn’t load" in its place (measured 2026-10-30, while ChatGPT
|
|
6890
|
+
// held the finished answer), and that sentence was saved as the answer.
|
|
6891
|
+
answerRenderFailed: Boolean(assistant && [assistant.textNode, assistant.node].some((node) =>
|
|
6892
|
+
node && typeof node.querySelector === "function" && node.querySelector("[data-dil-render-failed]"))),
|
|
6783
6893
|
// ChatGPT tags each assistant message with the model that produced it -
|
|
6784
6894
|
// the only ground truth for "did the Pro selection actually take".
|
|
6785
6895
|
modelSlug: assistant ? assistant.modelSlug : undefined,
|
package/dist/cli-pro.js
CHANGED
|
@@ -1236,6 +1236,11 @@ export async function runAskProCommand(rest, io, beforeSend) {
|
|
|
1236
1236
|
// --tool turns on a composer tool (deep-research, web-search,
|
|
1237
1237
|
// create-image, or any label the menu shows) for this send.
|
|
1238
1238
|
const tools = readRepeatedFlag(parsedAskPro.optionArgs, "--tool");
|
|
1239
|
+
// A tool is opened by its label, so a value that is only spaces names none:
|
|
1240
|
+
// the prompt went out with no tool and nothing said so.
|
|
1241
|
+
if (tools.some((tool) => !tool.trim())) {
|
|
1242
|
+
throw new Error("--tool is blank; name a composer tool (web-search, create-image), or leave the option out.");
|
|
1243
|
+
}
|
|
1239
1244
|
if (tools.length > 0 && !hasSendMode) {
|
|
1240
1245
|
throw new Error("--tool only applies when sending (`prodex pro browser ask`); the dry-run preview cannot open ChatGPT's tools menu.");
|
|
1241
1246
|
}
|
package/dist/continue-thread.js
CHANGED
|
@@ -94,9 +94,11 @@ export function threadMatchesProject(threadUrl, project, knownProjectIds) {
|
|
|
94
94
|
// of them slug to "". The id is the only thing that identifies those, and
|
|
95
95
|
// the caller reads it off the live sidebar.
|
|
96
96
|
const slug = chatGptProjectSlug(project);
|
|
97
|
-
// The id
|
|
98
|
-
// "notes"
|
|
99
|
-
|
|
97
|
+
// The segment is "g-p-", the id, a dash and the name, so the name has to be
|
|
98
|
+
// all that follows the id. A suffix test let "notes" answer for "my-notes";
|
|
99
|
+
// a prefix test would let it answer for "notes-archive".
|
|
100
|
+
const named = /^g-p-[0-9a-f]+-(.+)$/i.exec(projectSegment)?.[1]?.toLowerCase();
|
|
101
|
+
if (slug && named === slug)
|
|
100
102
|
return true;
|
|
101
103
|
// No name in the URL: fall back to the id this project has been seen using.
|
|
102
104
|
// Nothing is guessed - the id came from another record of the same project -
|
package/docs/releasing.md
CHANGED
|
@@ -16,6 +16,24 @@ Release target: `v0.40.13` on `main`. The GitHub Release records the resolved co
|
|
|
16
16
|
|
|
17
17
|
The release adds a guarded, opt-in transition and more accurate blocker reporting. It does not claim that headless Pro consultation is verified on either deployment target or that authentication can be retained indefinitely.
|
|
18
18
|
|
|
19
|
+
## Browser Incident 2026-10-30: WSL browser container out of threads
|
|
20
|
+
|
|
21
|
+
**What happened**
|
|
22
|
+
- A probe consult through the WSL container (`prodex-browser-browser-1`, branch build reporting 0.40.18) came back `done`, request verified, with the answer "This response couldn’t load". The conversation held the real answer ("OK"). The page had drawn its own error in its place (`div[role=status][data-dil-render-failed]`).
|
|
23
|
+
- Cause: the container allowed 256 processes and threads. Chromium and the service used about 171 with one ChatGPT tab, and 17 stdio MCP servers (16 Claude Code sessions on the host, about 7 threads each) took the rest. Chromium logged `pthread_create: Resource temporarily unavailable` during the send.
|
|
24
|
+
- A reload of that thread while diagnosing it failed the same way, and then `Zygote could not fork`. After that Chromium could start no renderer at all. The tab stayed `browser_tab_crashed`, and prodex's one same-address reload before a send crashed again within 300 ms, so every consult through the container failed.
|
|
25
|
+
- M3's container was checked the same day: 151 of 256 in use, with 1 MCP server and no thread errors in 72 hours.
|
|
26
|
+
|
|
27
|
+
**Recovery (maintainer approved each step)**
|
|
28
|
+
- `docker update --pids-limit 1024 prodex-browser-browser-1` raised the limit in place. The thread errors stopped, but the browser still could not start renderers.
|
|
29
|
+
- `docker restart prodex-browser-browser-1`, after the container's send lock was confirmed free. The limit stays 1024 across restarts. Health was `healthy` within seconds, `container-client.mjs status` reported `ready: true`, and a fresh-chat consult with markdown in its prompt answered "OK" with its request verified. No thread errors were logged after the restart.
|
|
30
|
+
- The restart dropped every client's MCP connection to the container. Each WSL Claude Code session needs `/mcp` to reconnect.
|
|
31
|
+
|
|
32
|
+
**Fixes**
|
|
33
|
+
- `main` PR #79 (merge `eab9340`, not yet released): a send no longer saves the page's render error as the answer, and recovery loads the thread again to draw it. It was ported to `container/redesign-patches` as `f93b63f`.
|
|
34
|
+
- `container/redesign-patches` `55f2f3e`: `compose.json` sets `pids_limit` to 1024. The running WSL container already has it through `docker update`. M3's container gets it on its next rebuild.
|
|
35
|
+
- Neither container image has been rebuilt with these commits yet.
|
|
36
|
+
|
|
19
37
|
## Browser Incident 2026-10-20: M3 headless browser behind Cloudflare
|
|
20
38
|
|
|
21
39
|
**What happened**
|
|
@@ -35,6 +53,26 @@ The release adds a guarded, opt-in transition and more accurate blocker reportin
|
|
|
35
53
|
**Open**
|
|
36
54
|
- A long-running headless browser ends up behind Cloudflare's check (also noted on WSL, where the Xvfb display is used instead). On macOS there is no virtual display, and a minimized window counts as hidden, so the remaining no-headless option there is a window kept behind others. Changing the headless preference is a design decision that was not made here.
|
|
37
55
|
|
|
56
|
+
## 0.40.48 Release Record
|
|
57
|
+
|
|
58
|
+
Release commit `406ae25` on `main` (cut after PRs #74-#78, last merge `7a88f85`); tag `v0.40.48`. Released on the user's go-ahead, promptly because of #76: on 0.40.47, with the machine under load, the page often ignored the press on the model selector and the send was blocked as "ChatGPT model menu did not open after clicking the selector" (9 of 23 sends across five runs, every one starting from a conversation; also twice the same day without added load). #74 (horizontal rules), #75 (block content inside list items), #77 (inline code holding backticks) and #78 (table column alignment), found comparing live answers with the page, went out with it.
|
|
59
|
+
|
|
60
|
+
**Publishing**
|
|
61
|
+
- `npm run release:verify` passed on the release commit after a build, with nothing else running on the machine.
|
|
62
|
+
- `publish.yml` run 40819980786 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the five entries from `CHANGELOG.md`.
|
|
63
|
+
|
|
64
|
+
**Installed**
|
|
65
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.48. Running services were not restarted: no host stdio MCP servers on WSL, 1 on M3, none on the Mac mini.
|
|
66
|
+
- The maintainer watchdog checkout was pulled to `406ae25` and rebuilt.
|
|
67
|
+
|
|
68
|
+
**Container**
|
|
69
|
+
- `container/redesign-patches` `7f1a7b9` ports #74, `e8b970d` #75, `4f1670e` #76, `3b947c7` #77 and `e30e649` #78. That branch recognises the picker only through its trigger, so #76's test case for a stray picker beside a closed trigger is left out there. tsc, build, the full suite (2144 tests) and `release:verify` pass on `e30e649`.
|
|
70
|
+
- The images were not rebuilt: the WSL container still runs the `facbd9d` image (`c03be48c060d`) and the M3 container `cfaf881d2fd6`.
|
|
71
|
+
|
|
72
|
+
**Verified live on installed 0.40.48 (WSL, scratch repo, Instant)**
|
|
73
|
+
- One answer holding a numbered step with a code block, the inline code ``a`b``, a rule and a table aligned left and right came back as written: the code block indented under its step, ``a`b`` intact, `---`, and `| :--- | ---: |`.
|
|
74
|
+
- Six sends with two test suites running on the machine (load average up to 9.4) all went through; a read-only page watcher showed one of them pressing the model selector twice before its menu opened.
|
|
75
|
+
|
|
38
76
|
## 0.40.47 Release Record
|
|
39
77
|
|
|
40
78
|
Release commit `609b1c9` on `main` (cut after PRs #71-#73, last merge `40d7eee`); tag `v0.40.47`. Released promptly because of #72: on 0.40.46 any prompt carrying a url with "&" (a query string, bare or in a markdown link) was answered and then refused as `request_mismatch`, so the answer was not returned. #71 (italic and strikethrough in the newer renderer) and #73 (the `utm_source=chatgpt.com` parameter ChatGPT appends to rendered links) went out with it.
|