@youdie006/prodex 0.40.45 → 0.40.47
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 +94 -29
- package/dist/tui.js +79 -8
- package/docs/releasing.md +42 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -235,6 +235,40 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
235
235
|
return space ? out + " " : out;
|
|
236
236
|
};
|
|
237
237
|
const trimEnd = (value) => { let end = value.length; while (end > 0 && isSpaceChar(value[end - 1])) end -= 1; return value.slice(0, end); };
|
|
238
|
+
// The newer answer renderer sets white-space: pre-line on a paragraph,
|
|
239
|
+
// so a newline inside it is a line break on screen (measured
|
|
240
|
+
// 2026-10-28). Collapsing it like source whitespace returned "alpha beta"
|
|
241
|
+
// for an answer showing alpha and beta on two lines. Spaces and tabs
|
|
242
|
+
// still collapse; only the newlines stay.
|
|
243
|
+
const collapseKeepingLines = (value) => {
|
|
244
|
+
let out = "";
|
|
245
|
+
let space = false;
|
|
246
|
+
for (const ch of value) {
|
|
247
|
+
if (ch === NL) { while (out.endsWith(" ")) out = out.slice(0, -1); out += NL; space = false; continue; }
|
|
248
|
+
if (isSpaceChar(ch)) { space = true; continue; }
|
|
249
|
+
if (space && out && !out.endsWith(NL)) out += " ";
|
|
250
|
+
space = false;
|
|
251
|
+
out += ch;
|
|
252
|
+
}
|
|
253
|
+
return space && !out.endsWith(NL) ? out + " " : out;
|
|
254
|
+
};
|
|
255
|
+
const keepsLineBreaks = (node, inherited) => {
|
|
256
|
+
if (!node || node.nodeType !== 1) return inherited;
|
|
257
|
+
let whiteSpace = null;
|
|
258
|
+
try {
|
|
259
|
+
const view = node.ownerDocument && node.ownerDocument.defaultView;
|
|
260
|
+
if (view && typeof view.getComputedStyle === "function") whiteSpace = view.getComputedStyle(node).whiteSpace;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
whiteSpace = null;
|
|
263
|
+
}
|
|
264
|
+
if (!whiteSpace) {
|
|
265
|
+
const style = (attr(node, "style") || "").toLowerCase();
|
|
266
|
+
const at = style.indexOf("white-space");
|
|
267
|
+
if (at >= 0) whiteSpace = style.slice(at).split(":")[1].split(";")[0].trim();
|
|
268
|
+
}
|
|
269
|
+
if (!whiteSpace) return inherited;
|
|
270
|
+
return whiteSpace === "pre" || whiteSpace === "pre-wrap" || whiteSpace === "pre-line" || whiteSpace === "break-spaces";
|
|
271
|
+
};
|
|
238
272
|
const isCheckbox = (node) => attr(node, "role") === "checkbox" || (tagOf(node) === "input" && attr(node, "type") === "checkbox");
|
|
239
273
|
const skipped = (node) => {
|
|
240
274
|
const tag = tagOf(node);
|
|
@@ -281,16 +315,27 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
281
315
|
// The renderer web-search answers use since 2026-10-10 marks a source as a
|
|
282
316
|
// badge inside a popover trigger; its urls only exist once it is opened.
|
|
283
317
|
const isSourceBadge = (node) => attr(node, "data-d-component") === "popover-trigger" && findAll(node, (n) => attr(n, "data-pill") !== null).length > 0;
|
|
318
|
+
// ChatGPT's link component appends utm_source=chatgpt.com to every link
|
|
319
|
+
// it renders (measured 2026-10-28), a parameter the answer never wrote.
|
|
320
|
+
const untracked = (href) => {
|
|
321
|
+
const hashAt = href.indexOf("#");
|
|
322
|
+
const base = hashAt === -1 ? href : href.slice(0, hashAt);
|
|
323
|
+
const queryAt = base.indexOf("?");
|
|
324
|
+
if (queryAt === -1) return href;
|
|
325
|
+
const params = base.slice(queryAt + 1).split("&").filter((param) => param !== "utm_source=chatgpt.com");
|
|
326
|
+
return base.slice(0, queryAt) + (params.length > 0 ? "?" + params.join("&") : "") + (hashAt === -1 ? "" : href.slice(hashAt));
|
|
327
|
+
};
|
|
284
328
|
const pill = (node) => {
|
|
285
329
|
const label = collapse(node.textContent || "").trim();
|
|
286
330
|
if (!label) return "";
|
|
287
331
|
const link = findAll(node, (n) => tagOf(n) === "a" && (attr(n, "href") || "").startsWith("http"))[0];
|
|
288
|
-
return link ? "[" + label + "](" + attr(link, "href") + ")" : "[" + label + "]";
|
|
332
|
+
return link ? "[" + label + "](" + untracked(attr(link, "href")) + ")" : "[" + label + "]";
|
|
289
333
|
};
|
|
290
|
-
const inline = (node) => {
|
|
334
|
+
const inline = (node, keep) => {
|
|
335
|
+
const keepLines = keepsLineBreaks(node, keep === true);
|
|
291
336
|
let out = "";
|
|
292
337
|
for (const child of children(node)) {
|
|
293
|
-
const piece = inlineNode(child);
|
|
338
|
+
const piece = inlineNode(child, keepLines);
|
|
294
339
|
if (!piece) continue;
|
|
295
340
|
// A source pill sits right after its sentence; keep one space before it.
|
|
296
341
|
if (child.nodeType === 1 && (attr(child, "data-chatgpt-copy-reference") !== null || isSourceBadge(child)) && out && !isSpaceChar(out[out.length - 1])) out += " ";
|
|
@@ -298,8 +343,8 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
298
343
|
}
|
|
299
344
|
return out;
|
|
300
345
|
};
|
|
301
|
-
const inlineNode = (node) => {
|
|
302
|
-
if (node.nodeType === 3) return collapse(node.textContent || "");
|
|
346
|
+
const inlineNode = (node, keep) => {
|
|
347
|
+
if (node.nodeType === 3) return keep === true ? collapseKeepingLines(node.textContent || "") : collapse(node.textContent || "");
|
|
303
348
|
if (node.nodeType !== 1 || skipped(node)) return "";
|
|
304
349
|
const tag = tagOf(node);
|
|
305
350
|
// A task list item's box (measured: button[role=checkbox][aria-checked]).
|
|
@@ -308,14 +353,26 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
308
353
|
if (attr(node, "data-chatgpt-copy-reference") !== null || isSourceBadge(node)) return pill(node);
|
|
309
354
|
if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return BT + (node.textContent || "") + BT;
|
|
310
355
|
if (tag === "br") return NL;
|
|
311
|
-
const inner = inline(node);
|
|
312
|
-
// The newer renderer marks bold as span[data-d-default-strong]
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
356
|
+
const inner = inline(node, keep);
|
|
357
|
+
// The newer renderer marks bold as span[data-d-default-strong], italic
|
|
358
|
+
// as span[data-d-font-style=italic] and strikethrough as
|
|
359
|
+
// span[data-d-text-decoration=line-through] (measured 2026-10-28; only
|
|
360
|
+
// bold was read, so the other two came back as plain words). One
|
|
361
|
+
// element can carry several, so each applies in turn.
|
|
362
|
+
const strong = tag === "strong" || tag === "b" || attr(node, "data-d-default-strong") !== null;
|
|
363
|
+
const italic = tag === "em" || tag === "i" || attr(node, "data-d-font-style") === "italic";
|
|
364
|
+
const struck = tag === "del" || tag === "s" || (attr(node, "data-d-text-decoration") || "").includes("line-through");
|
|
365
|
+
if (strong || italic || struck) {
|
|
366
|
+
let marked = inner.trim();
|
|
367
|
+
if (!marked) return "";
|
|
368
|
+
if (strong) marked = "**" + marked + "**";
|
|
369
|
+
if (italic) marked = "*" + marked + "*";
|
|
370
|
+
if (struck) marked = "~~" + marked + "~~";
|
|
371
|
+
return marked;
|
|
372
|
+
}
|
|
316
373
|
if (tag === "a") {
|
|
317
374
|
const href = attr(node, "href") || "";
|
|
318
|
-
return href.startsWith("http") && inner.trim() ? "[" + inner.trim() + "](" + href + ")" : inner;
|
|
375
|
+
return href.startsWith("http") && inner.trim() ? "[" + inner.trim() + "](" + untracked(href) + ")" : inner;
|
|
319
376
|
}
|
|
320
377
|
return inner;
|
|
321
378
|
};
|
|
@@ -354,7 +411,7 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
354
411
|
const line = (cells) => "| " + cells.join(" | ") + " |";
|
|
355
412
|
return [line(rows[0]), line(rows[0].map(() => "---")), ...rows.slice(1).map(line)].join(NL);
|
|
356
413
|
};
|
|
357
|
-
const list = (node, depth) => {
|
|
414
|
+
const list = (node, depth, keep) => {
|
|
358
415
|
const ordered = tagOf(node) === "ol";
|
|
359
416
|
let number = parseInt(attr(node, "start") || "1", 10);
|
|
360
417
|
if (!Number.isFinite(number)) number = 1;
|
|
@@ -367,19 +424,20 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
367
424
|
const words = [];
|
|
368
425
|
const nested = [];
|
|
369
426
|
let buffer = "";
|
|
427
|
+
const keepItemLines = keepsLineBreaks(item, keep === true);
|
|
370
428
|
for (const child of children(item)) {
|
|
371
429
|
const tag = child.nodeType === 1 ? tagOf(child) : "";
|
|
372
|
-
if (tag === "ul" || tag === "ol") { nested.push(list(child, depth + 1)); continue; }
|
|
373
|
-
if (isBlock(child)) { if (buffer.trim()) words.push(buffer.trim()); buffer = ""; const b = block(child, depth + 1); if (b) words.push(b); continue; }
|
|
374
|
-
buffer += inlineNode(child);
|
|
430
|
+
if (tag === "ul" || tag === "ol") { nested.push(list(child, depth + 1, keepItemLines)); continue; }
|
|
431
|
+
if (isBlock(child)) { if (buffer.trim()) words.push(buffer.trim()); buffer = ""; const b = block(child, depth + 1, keepItemLines); if (b) words.push(b); continue; }
|
|
432
|
+
buffer += inlineNode(child, keepItemLines);
|
|
375
433
|
}
|
|
376
434
|
if (buffer.trim()) words.push(buffer.trim());
|
|
377
435
|
items.push(indent + marker + " " + words.join(" ") + (nested.length ? NL + nested.join(NL) : ""));
|
|
378
436
|
}
|
|
379
437
|
return items.join(NL);
|
|
380
438
|
};
|
|
381
|
-
const block = (node, depth) => {
|
|
382
|
-
if (node.nodeType === 3) return collapse(node.textContent || "").trim();
|
|
439
|
+
const block = (node, depth, keep) => {
|
|
440
|
+
if (node.nodeType === 3) return (keep === true ? collapseKeepingLines(node.textContent || "") : collapse(node.textContent || "")).trim();
|
|
383
441
|
if (node.nodeType !== 1 || skipped(node)) return "";
|
|
384
442
|
const tag = tagOf(node);
|
|
385
443
|
if (isMath(node)) return isDisplayMath(node) ? "$$" + NL + mathSource(node) + NL + "$$" : "$" + mathSource(node) + "$";
|
|
@@ -387,19 +445,20 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
387
445
|
if (isMermaidPreview(node)) return mermaidBlock(node);
|
|
388
446
|
if (tag === "table") return table(node);
|
|
389
447
|
if (/^h[1-6]$/.test(tag)) return "#".repeat(Number(tag[1])) + " " + inline(node).trim();
|
|
390
|
-
if (tag === "ul" || tag === "ol") return list(node, depth);
|
|
448
|
+
if (tag === "ul" || tag === "ol") return list(node, depth, keepsLineBreaks(node, keep === true));
|
|
391
449
|
if (tag === "hr") return "---";
|
|
392
|
-
if (tag === "blockquote") return blocks(node, depth).split(NL).map((line) => "> " + line).join(NL);
|
|
393
|
-
if (tag === "p") return inline(node).trim();
|
|
394
|
-
return blocks(node, depth);
|
|
450
|
+
if (tag === "blockquote") return blocks(node, depth, keep).split(NL).map((line) => "> " + line).join(NL);
|
|
451
|
+
if (tag === "p") return inline(node, keep).trim();
|
|
452
|
+
return blocks(node, depth, keep);
|
|
395
453
|
};
|
|
396
|
-
const blocks = (node, depth) => {
|
|
454
|
+
const blocks = (node, depth, keep) => {
|
|
455
|
+
const keepLines = keepsLineBreaks(node, keep === true);
|
|
397
456
|
const parts = [];
|
|
398
457
|
let buffer = "";
|
|
399
458
|
const flush = () => { if (buffer.trim()) parts.push(buffer.trim()); buffer = ""; };
|
|
400
459
|
for (const child of children(node)) {
|
|
401
|
-
if (isBlock(child)) { flush(); const b = block(child, depth); if (b) parts.push(b); continue; }
|
|
402
|
-
buffer += inlineNode(child);
|
|
460
|
+
if (isBlock(child)) { flush(); const b = block(child, depth, keepLines); if (b) parts.push(b); continue; }
|
|
461
|
+
buffer += inlineNode(child, keepLines);
|
|
403
462
|
}
|
|
404
463
|
flush();
|
|
405
464
|
return parts.join(NL + NL);
|
|
@@ -6244,9 +6303,8 @@ export function pickLandedConversation(candidates, sentPrompt) {
|
|
|
6244
6303
|
* nothing is the safe answer here.
|
|
6245
6304
|
*/
|
|
6246
6305
|
export function transcriptContainsWholeSentPrompt(userText, sentPrompt) {
|
|
6247
|
-
const normalize = (value) => value
|
|
6306
|
+
const normalize = (value) => undoMarkdownEscapes(value)
|
|
6248
6307
|
.replace(AUTOLINKED_URL_PATTERN, "$1")
|
|
6249
|
-
.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
|
|
6250
6308
|
.replace(/\s+/g, " ")
|
|
6251
6309
|
.trim();
|
|
6252
6310
|
const seen = normalize(userText);
|
|
@@ -6261,6 +6319,14 @@ const NORMALIZED_PROMPT_MATCH_CHARS = 120;
|
|
|
6261
6319
|
// halves differently ("Function\_(x)" vs "Function_\(x\)", measured
|
|
6262
6320
|
// 2026-10-21), so the pattern is applied after markdown escapes are undone.
|
|
6263
6321
|
const AUTOLINKED_URL_PATTERN = /\[(https?:\/\/[^\]\s]+)\]\(\1\)/g;
|
|
6322
|
+
// The composer escapes more than the markdown it would otherwise misread: an
|
|
6323
|
+
// autolinked url's href kept "\\&" where its text had "&" (measured
|
|
6324
|
+
// 2026-10-28). Any ASCII punctuation can carry a markdown escape, so every such
|
|
6325
|
+
// escape is undone, on both sides alike.
|
|
6326
|
+
const MARKDOWN_ESCAPE_PATTERN = /\\([!-\/:-@[-`{-~])/g;
|
|
6327
|
+
function undoMarkdownEscapes(value) {
|
|
6328
|
+
return value.replace(MARKDOWN_ESCAPE_PATTERN, "$1");
|
|
6329
|
+
}
|
|
6264
6330
|
/**
|
|
6265
6331
|
* ChatGPT displays a sent turn's HTML entities decoded (measured 2026-10-19:
|
|
6266
6332
|
* "& <b>" was shown as "& <b>"), so both sides are compared decoded.
|
|
@@ -6277,7 +6343,7 @@ function decodeHtmlEntities(value) {
|
|
|
6277
6343
|
});
|
|
6278
6344
|
}
|
|
6279
6345
|
function normalizeChatGptPromptText(value) {
|
|
6280
|
-
const unescaped = decodeHtmlEntities(value)
|
|
6346
|
+
const unescaped = undoMarkdownEscapes(decodeHtmlEntities(value)).replace(AUTOLINKED_URL_PATTERN, "$1");
|
|
6281
6347
|
const renderedFences = unescaped.replace(/(^|\r?\n)```text[ \t]*\r?\n([\s\S]*?)\r?\n```(?=\r?\n|$)/g, "$1text\n$2");
|
|
6282
6348
|
return renderedFences.replace(/\s+/g, " ").trim();
|
|
6283
6349
|
}
|
|
@@ -6308,8 +6374,7 @@ export function chatGptRequestMatchesUserTurn(userText, sentPrompt, requestId) {
|
|
|
6308
6374
|
return withoutFenceLines(userText).includes(withoutFenceLines(sentPrompt));
|
|
6309
6375
|
}
|
|
6310
6376
|
function withoutFenceLines(value) {
|
|
6311
|
-
return decodeHtmlEntities(value)
|
|
6312
|
-
.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
|
|
6377
|
+
return undoMarkdownEscapes(decodeHtmlEntities(value))
|
|
6313
6378
|
.replace(AUTOLINKED_URL_PATTERN, "$1")
|
|
6314
6379
|
.replace(/^[ \t]*```[\w-]*[ \t]*$/gm, "")
|
|
6315
6380
|
.replace(/\s+/g, "");
|
package/dist/tui.js
CHANGED
|
@@ -186,11 +186,82 @@ const CURSOR_BAR = "▌";
|
|
|
186
186
|
function paint(text, code, color) {
|
|
187
187
|
return color ? `${code}${text}${RESET}` : text;
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
// Color codes take no columns, and must be carried through a cut whole.
|
|
190
|
+
const SGR_PATTERN = /^\u001b\[[0-9;]*m/;
|
|
191
|
+
/**
|
|
192
|
+
* Terminal columns a character takes: two for East Asian wide characters
|
|
193
|
+
* (Hangul, CJK, full-width forms) and the common emoji, none for combining and
|
|
194
|
+
* variation marks, one otherwise. Counting UTF-16 units instead put a Korean
|
|
195
|
+
* conversation title at twice its allowance, so it wrapped into the next row.
|
|
196
|
+
*/
|
|
197
|
+
function charColumns(codePoint) {
|
|
198
|
+
if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0))
|
|
199
|
+
return 0;
|
|
200
|
+
if ((codePoint >= 0x300 && codePoint <= 0x36f) || codePoint === 0x200b || codePoint === 0x200d || (codePoint >= 0xfe00 && codePoint <= 0xfe0f)) {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
if ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
204
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
205
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
206
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
207
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe4f) ||
|
|
208
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
209
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
210
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
|
|
211
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)) {
|
|
212
|
+
return 2;
|
|
213
|
+
}
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
/** Columns a string takes on screen, ignoring color codes. */
|
|
217
|
+
export function displayWidth(text) {
|
|
218
|
+
let columns = 0;
|
|
219
|
+
for (let index = 0; index < text.length;) {
|
|
220
|
+
const sgr = SGR_PATTERN.exec(text.slice(index));
|
|
221
|
+
if (sgr) {
|
|
222
|
+
index += sgr[0].length;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const codePoint = text.codePointAt(index);
|
|
226
|
+
columns += charColumns(codePoint);
|
|
227
|
+
index += codePoint > 0xffff ? 2 : 1;
|
|
228
|
+
}
|
|
229
|
+
return columns;
|
|
230
|
+
}
|
|
231
|
+
/** Pad with spaces to a number of columns, not of UTF-16 units. */
|
|
232
|
+
export function padEndToWidth(text, width) {
|
|
233
|
+
return text + " ".repeat(Math.max(0, width - displayWidth(text)));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Cut a line to the terminal rather than letting it wrap into a second row.
|
|
237
|
+
* Counts columns, keeps color codes whole, never splits a character, and closes
|
|
238
|
+
* a color the cut fell inside: a cut that dropped the reset left the rows
|
|
239
|
+
* below in the label's color.
|
|
240
|
+
*/
|
|
190
241
|
export function truncateToWidth(text, width) {
|
|
191
|
-
if (width <= 0 || text
|
|
242
|
+
if (width <= 0 || displayWidth(text) <= width)
|
|
192
243
|
return text;
|
|
193
|
-
|
|
244
|
+
const budget = width <= 3 ? width : width - 3;
|
|
245
|
+
let out = "";
|
|
246
|
+
let columns = 0;
|
|
247
|
+
let colored = false;
|
|
248
|
+
for (let index = 0; index < text.length;) {
|
|
249
|
+
const sgr = SGR_PATTERN.exec(text.slice(index));
|
|
250
|
+
if (sgr) {
|
|
251
|
+
out += sgr[0];
|
|
252
|
+
colored = sgr[0] !== RESET;
|
|
253
|
+
index += sgr[0].length;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const codePoint = text.codePointAt(index);
|
|
257
|
+
const step = charColumns(codePoint);
|
|
258
|
+
if (columns + step > budget)
|
|
259
|
+
break;
|
|
260
|
+
out += String.fromCodePoint(codePoint);
|
|
261
|
+
columns += step;
|
|
262
|
+
index += codePoint > 0xffff ? 2 : 1;
|
|
263
|
+
}
|
|
264
|
+
return `${out}${width <= 3 ? "" : "..."}${colored ? RESET : ""}`;
|
|
194
265
|
}
|
|
195
266
|
export function renderSelectList(input) {
|
|
196
267
|
const color = input.color ?? true;
|
|
@@ -202,7 +273,7 @@ export function renderSelectList(input) {
|
|
|
202
273
|
const lines = [`${paint(input.title, BOLD, color)}${step}`, ""];
|
|
203
274
|
// Hints line up in their own column; ragged hints read as noise next to the
|
|
204
275
|
// labels they belong to.
|
|
205
|
-
const labelWidth = Math.max(...input.options.map((option) => option.label
|
|
276
|
+
const labelWidth = Math.max(...input.options.map((option) => displayWidth(option.label)), 0);
|
|
206
277
|
input.options.forEach((option, index) => {
|
|
207
278
|
const onCursor = index === input.cursor;
|
|
208
279
|
// A left bar reads as a cursor even once the row is colored, where a ">"
|
|
@@ -213,9 +284,9 @@ export function renderSelectList(input) {
|
|
|
213
284
|
const ordinal = paint(`${index + 1}`, onCursor ? ACCENT : DIM, color);
|
|
214
285
|
const box = input.multi ? (selected.has(index) ? "[x] " : "[ ] ") : "";
|
|
215
286
|
const label = onCursor ? paint(option.label, BOLD, color) : option.label;
|
|
216
|
-
const gap = " ".repeat(Math.max(0, labelWidth - option.label
|
|
287
|
+
const gap = " ".repeat(Math.max(0, labelWidth - displayWidth(option.label)));
|
|
217
288
|
const hint = option.hint ? `${gap} ${paint(option.hint, DIM, color)}` : "";
|
|
218
|
-
lines.push(truncateToWidth(` ${bar} ${ordinal} ${box}${label}${hint}`, width
|
|
289
|
+
lines.push(truncateToWidth(` ${bar} ${ordinal} ${box}${label}${hint}`, width));
|
|
219
290
|
});
|
|
220
291
|
if (input.footer)
|
|
221
292
|
lines.push("", paint(input.footer, DIM, color));
|
|
@@ -234,8 +305,8 @@ export function renderContextPanel(rows, options = {}) {
|
|
|
234
305
|
const texts = rows.map((row) => ` ${row.label.padEnd(labelWidth)} ${row.value} `);
|
|
235
306
|
// Size the frame to its contents, capped by the terminal. A box stretched to
|
|
236
307
|
// the full width is mostly empty space with a border around it.
|
|
237
|
-
const inner = Math.min(Math.max(...texts.map((text) => text
|
|
238
|
-
const body = texts.map((text) => `│${truncateToWidth(text, inner)
|
|
308
|
+
const inner = Math.min(Math.max(...texts.map((text) => displayWidth(text)), 0), Math.max(20, (options.width ?? 72) - 2));
|
|
309
|
+
const body = texts.map((text) => `│${padEndToWidth(truncateToWidth(text, inner), inner)}│`);
|
|
239
310
|
const top = `╭${"─".repeat(inner)}╮`;
|
|
240
311
|
const bottom = `╰${"─".repeat(inner)}╯`;
|
|
241
312
|
return [top, ...body, bottom].map((line) => (color ? paint(line, DIM, color) : line)).join("\n");
|
package/docs/releasing.md
CHANGED
|
@@ -35,6 +35,48 @@ The release adds a guarded, opt-in transition and more accurate blocker reportin
|
|
|
35
35
|
**Open**
|
|
36
36
|
- 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
37
|
|
|
38
|
+
## 0.40.46 Release Record
|
|
39
|
+
|
|
40
|
+
Release commit `807f245` on `main` (cut after PRs #69 and #70, last merge `9253ee9`); tag `v0.40.46`. A batch released on the user's go-ahead: terminal-column widths in the interactive picker (#69) and line breaks kept inside answers from ChatGPT's newer `white-space: pre-line` renderer (#70).
|
|
41
|
+
|
|
42
|
+
**Publishing**
|
|
43
|
+
- `npm run release:verify` passed on the release commit after a build, with nothing else running on the machine.
|
|
44
|
+
- `publish.yml` run 40645773352 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the two entries from `CHANGELOG.md`.
|
|
45
|
+
|
|
46
|
+
**Installed**
|
|
47
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.46, and each installed `dist/chatgpt-browser.js` carries the line-break reader. Running services were not restarted: no host stdio MCP servers on WSL, 1 on M3, none on the Mac mini.
|
|
48
|
+
- The maintainer watchdog checkout was pulled to `807f245` and rebuilt.
|
|
49
|
+
|
|
50
|
+
**Container**
|
|
51
|
+
- `container/redesign-patches` `5d021d1` ports #69 and `ad24ce2` ports #70 (the #70 cherry-pick conflicted in `src/chatgpt-browser.ts` and the PR diff was applied instead; tsc, build, the suite and `release:verify` pass on it).
|
|
52
|
+
- The images were still not rebuilt: `docker compose build` is refused by the agent's permission classifier on WSL and over ssh on M3. The running WSL container is the #49-#58 port and the M3 container the 0.40.42 port.
|
|
53
|
+
|
|
54
|
+
**Verified**
|
|
55
|
+
- Before the release, on the live tab: the reader in 0.40.45 returned "alpha beta" for an answer showing "alpha" and "beta" on two lines; the #70 build returned "alpha\nbeta" from the same page, with the list after it unchanged.
|
|
56
|
+
- After installing, the tab no longer showed a multi-line answer, so the line-break fix was not re-read live on the installed build; the installed reader ran on the current answer without error.
|
|
57
|
+
|
|
58
|
+
## 0.40.45 Release Record
|
|
59
|
+
|
|
60
|
+
Release commit `5e5f965` on `main` (cut after PRs #59 and #63-#68, last merge `52243f4`); tag `v0.40.45`. A batch released on the user's go-ahead: machine-wide send pacing (#59), accurate advice for a busy tab (#63), word-bounded project-name redaction (#64), the IPv6 loopback message (#65), no cancelling of finished sessions (#66), a blank saved default project stopped before the browser (#67), and the project-delete preview (#68).
|
|
61
|
+
|
|
62
|
+
**Publishing**
|
|
63
|
+
- `npm run release:verify` passed on the release commit after a build (nothing else was run on the machine meanwhile, after 0.40.44's load-related flake).
|
|
64
|
+
- `publish.yml` run 40634873154 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the seven entries from `CHANGELOG.md`.
|
|
65
|
+
|
|
66
|
+
**Installed**
|
|
67
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.45. Running services were not restarted: no host stdio MCP servers on WSL, 1 on M3, none on the Mac mini.
|
|
68
|
+
- The maintainer watchdog checkout was pulled to `5e5f965` and rebuilt.
|
|
69
|
+
|
|
70
|
+
**Container**
|
|
71
|
+
- `container/redesign-patches` `7f40247` ports #59, #60 and #63-#68 (full suite, 165 files and 2112 tests, and `release:verify` pass on it). #62 changes main's CI only and is not ported.
|
|
72
|
+
- The images were still not rebuilt: `docker compose build` is refused by the agent's permission classifier on WSL and over ssh on M3. The running WSL container is the #49-#58 port and the M3 container the 0.40.42 port.
|
|
73
|
+
|
|
74
|
+
**Verified live on installed 0.40.45 (WSL, scratch repo, unreachable CDP port)**
|
|
75
|
+
- `setup --host ::1`: "HTTP MCP host ::1 (IPv6 loopback) is not supported ... Use --host 127.0.0.1 or localhost."
|
|
76
|
+
- A repo whose config holds project " ": the send stops with "The default project saved for this repo ... is blank" before any browser step; `--no-project` goes on to the browser.
|
|
77
|
+
- The send-pacing marker was written beside the send lock (`last-browser-send`), not in the repo's `.bridge`.
|
|
78
|
+
- `sessions cancel` on a blocked session: "is already blocked; its outcome is recorded, so there is nothing to cancel."
|
|
79
|
+
|
|
38
80
|
## 0.40.44 Release Record
|
|
39
81
|
|
|
40
82
|
Release commit `060d71c` on `main` (cut after PRs #60-#62, last merge `95c5ddc`); tag `v0.40.44`. Released promptly because of #61: 0.40.43's stale-lock check (#54) derived a holder's wall-clock start from `/proc/stat` btime, which drifts on WSL2 (measured 2026-10-27: 3 to 4 seconds every 30 seconds, 30 minutes over a day), so a send a few minutes into another session's consult freed the live lock and drove the same tab. #60 (picker paths with escaped spaces) went out with it; #62 changes CI only. #59 (machine-wide send pacing) was not ready and is not in this release.
|