create-walle 0.10.0 → 0.10.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/README.md +84 -41
- package/lib/runtime-closure-policy.json +2 -2
- package/package.json +10 -4
- package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
- package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
- package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +20 -0
- package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
- package/template/coding-task-manager/lib/codex-local-links.js +267 -0
- package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
- package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
- package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
- package/template/coding-task-manager/lib/feature-manifest.js +0 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +38 -11
- package/template/coding-task-manager/lib/headless-term-service.js +5 -6
- package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
- package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
- package/template/coding-task-manager/lib/lead-model-switch.js +8 -0
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
- package/template/coding-task-manager/lib/login-tripwire.js +17 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +4 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
- package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
- package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
- package/template/coding-task-manager/lib/native-history-control.js +60 -12
- package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
- package/template/coding-task-manager/lib/raw-history-block.js +3 -1
- package/template/coding-task-manager/lib/read-pool-client.js +31 -22
- package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
- package/template/coding-task-manager/lib/remote-command-runtime.js +5 -4
- package/template/coding-task-manager/lib/root-prompt-admission.js +10 -9
- package/template/coding-task-manager/lib/root-prompt-flow.js +8 -5
- package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
- package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
- package/template/coding-task-manager/lib/session-create/provider-process.js +4 -2
- package/template/coding-task-manager/lib/session-lifecycle.js +2 -50
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +59 -3
- package/template/coding-task-manager/lib/session-resource-sampler.js +48 -0
- package/template/coding-task-manager/lib/session-status.js +2 -2
- package/template/coding-task-manager/lib/typing-interaction-trace.js +44 -2
- package/template/coding-task-manager/lib/typing-worker-stages.js +148 -1
- package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
- package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
- package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
- package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
- package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
- package/template/coding-task-manager/lib/ws/input.js +1 -0
- package/template/coding-task-manager/protocol/message-table.json +0 -10
- package/template/coding-task-manager/protocol/messages.js +0 -10
- package/template/coding-task-manager/protocol/ws-schema.json +0 -20
- package/template/coding-task-manager/protocol/ws-schema.md +0 -2
- package/template/coding-task-manager/public/css/app.css +0 -1
- package/template/coding-task-manager/public/index.html +3 -17
- package/template/coding-task-manager/public/js/client-health-core.js +81 -0
- package/template/coding-task-manager/public/js/client-health-main.js +61 -0
- package/template/coding-task-manager/public/js/prompt-submission-client.js +5 -1
- package/template/coding-task-manager/public/js/protocol.js +0 -10
- package/template/coding-task-manager/public/js/row-join.js +14 -1
- package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +0 -2
- package/template/coding-task-manager/server.js +62 -186
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/wall-e/coding/host-capabilities.js +2 -2
- package/template/wall-e/fly.toml +1 -1
- package/template/wall-e/tools/local-tools.js +6 -1
- package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
// blob"; we still guard internally so one bad row can't sink the whole render.
|
|
32
32
|
|
|
33
33
|
const { takeMarkdownTable, isTableRowLine } = require('./codex-markdown-table');
|
|
34
|
+
const { createListTracker } = require('./codex-markdown-lists');
|
|
35
|
+
const { isLocalPathLikeLink, renderLocalLinkTarget } = require('./codex-local-links');
|
|
34
36
|
|
|
35
37
|
const STYLE_TABLE_VERSION = 2;
|
|
36
38
|
|
|
@@ -340,14 +342,15 @@ function renderTextBlock(text, cols, style) {
|
|
|
340
342
|
});
|
|
341
343
|
}
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
// `ctx` is the render opts: `ctx.cwd` is the rollout session cwd the local-link rule shortens under.
|
|
346
|
+
function stripMarkdownInline(text, ctx) {
|
|
344
347
|
let s = String(text == null ? '' : text);
|
|
345
|
-
// Terminal projection: keep the information, remove Markdown control syntax.
|
|
346
|
-
s
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
});
|
|
350
|
-
s = s.replace(/\[([^\]]
|
|
348
|
+
// Terminal projection: keep the information, remove Markdown control syntax. A local path-like
|
|
349
|
+
// destination draws Codex's display target (`local_links.rs`): `label (target)`, or `target` alone
|
|
350
|
+
// when the label is blank; every other destination keeps its URL.
|
|
351
|
+
const dest = (url) => (isLocalPathLikeLink(url) ? (renderLocalLinkTarget(url, ctx && ctx.cwd) || url) : url);
|
|
352
|
+
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, alt, url) => `${String(alt || 'image').trim() || 'image'} (${dest(url)})`);
|
|
353
|
+
s = s.replace(/\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, label, url) => (String(label).trim() ? `${label} (${dest(url)})` : dest(url)));
|
|
351
354
|
s = s.replace(/<((?:https?:|file:)[^>\s]+)>/g, '$1');
|
|
352
355
|
s = s.replace(/`([^`\n]+)`/g, '$1');
|
|
353
356
|
s = s.replace(/\*\*\*([^*\n][\s\S]*?[^*\n])\*\*\*/g, '$1');
|
|
@@ -366,13 +369,13 @@ function isMarkdownTableSeparator(line) {
|
|
|
366
369
|
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(s);
|
|
367
370
|
}
|
|
368
371
|
|
|
369
|
-
function formatMarkdownTableRow(line) {
|
|
372
|
+
function formatMarkdownTableRow(line, ctx) {
|
|
370
373
|
const raw = String(line || '').trim();
|
|
371
374
|
const inner = raw.replace(/^\|/, '').replace(/\|$/, '');
|
|
372
|
-
return inner.split('|').map((cell) => stripMarkdownInline(cell.trim())).join(' | ');
|
|
375
|
+
return inner.split('|').map((cell) => stripMarkdownInline(cell.trim(), ctx)).join(' | ');
|
|
373
376
|
}
|
|
374
377
|
|
|
375
|
-
function renderMarkdownTextBlock(text, cols, style) {
|
|
378
|
+
function renderMarkdownTextBlock(text, cols, style, ctx) {
|
|
376
379
|
const prefix = style.prefix || '';
|
|
377
380
|
const prefixW = strWidth(prefix);
|
|
378
381
|
const baseBodySgr = style.bodySgr || [];
|
|
@@ -397,8 +400,12 @@ function renderMarkdownTextBlock(text, cols, style) {
|
|
|
397
400
|
};
|
|
398
401
|
|
|
399
402
|
const rawLines = String(text == null ? '' : text).replace(/\t/g, ' ').split('\n');
|
|
403
|
+
const lists = createListTracker();
|
|
400
404
|
for (let lineIndex = 0; lineIndex < rawLines.length; lineIndex += 1) {
|
|
401
405
|
const line = rawLines[lineIndex].replace(/\r$/, '');
|
|
406
|
+
// Fed for EVERY line (a blank line does not close the container stack, and a non-list line can
|
|
407
|
+
// be a continuation paragraph of the innermost item); fenced content is verbatim, never a list.
|
|
408
|
+
const item = inFence ? null : lists.consume(line);
|
|
402
409
|
const fence = line.match(/^\s*(```+|~~~+)/);
|
|
403
410
|
if (fence) {
|
|
404
411
|
inFence = !inFence;
|
|
@@ -408,47 +415,39 @@ function renderMarkdownTextBlock(text, cols, style) {
|
|
|
408
415
|
emitWrapped(line, [SGR.fgGray], { indent: 2, hang: 2 });
|
|
409
416
|
continue;
|
|
410
417
|
}
|
|
418
|
+
if (item && item.marker) {
|
|
419
|
+
// R-1: the marker already carries its own column, so indent 0 and the absolute hang; the
|
|
420
|
+
// message prefix (`• ` on the first row, ` ` after) is still applied by emitWrapped.
|
|
421
|
+
emitWrapped(item.marker + stripMarkdownInline(item.text, ctx), baseBodySgr, { indent: item.indent, hang: item.hang });
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
411
424
|
if (!line.trim()) {
|
|
412
425
|
out.push('');
|
|
413
426
|
continue;
|
|
414
427
|
}
|
|
415
428
|
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
416
429
|
if (heading) {
|
|
417
|
-
emitWrapped(stripMarkdownInline(heading[1]), [SGR.bold]);
|
|
430
|
+
emitWrapped(stripMarkdownInline(heading[1], ctx), [SGR.bold]);
|
|
418
431
|
continue;
|
|
419
432
|
}
|
|
420
433
|
const quote = line.match(/^\s{0,3}>\s?(.*)$/);
|
|
421
434
|
if (quote) {
|
|
422
|
-
emitWrapped('| ' + stripMarkdownInline(quote[1]), [SGR.dim], { hang: 2 });
|
|
435
|
+
emitWrapped('| ' + stripMarkdownInline(quote[1], ctx), [SGR.dim], { hang: 2 });
|
|
423
436
|
continue;
|
|
424
437
|
}
|
|
425
|
-
const tbl = takeMarkdownTable(rawLines, lineIndex, { width: cols - prefixW, indent: prefixW, lead: prefixPending ? stylize(prefix, style.prefixSgr) : null, strWidth, stylize, isSeparator: isMarkdownTableSeparator, strip: stripMarkdownInline, codes: { bold: [SGR.bold], dim: [SGR.dim], body: baseBodySgr } });
|
|
438
|
+
const tbl = takeMarkdownTable(rawLines, lineIndex, { width: cols - prefixW, indent: prefixW, lead: prefixPending ? stylize(prefix, style.prefixSgr) : null, strWidth, stylize, isSeparator: isMarkdownTableSeparator, strip: (cell) => stripMarkdownInline(cell, ctx), codes: { bold: [SGR.bold], dim: [SGR.dim], body: baseBodySgr } });
|
|
426
439
|
if (tbl) { lineIndex = tbl.nextIndex - 1; out.push(...tbl.rows); prefixPending = false; continue; }
|
|
427
440
|
if (isMarkdownTableSeparator(line)) continue;
|
|
428
441
|
const tableRow = isTableRowLine(line);
|
|
429
442
|
if (tableRow) {
|
|
430
|
-
emitWrapped(formatMarkdownTableRow(line), baseBodySgr, { hang: 4 });
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
const unordered = line.match(/^(\s*)([-*+])\s+(.+)$/);
|
|
434
|
-
if (unordered) {
|
|
435
|
-
const indent = Math.min(8, Math.floor(unordered[1].length / 2) * 2);
|
|
436
|
-
const marker = '- ';
|
|
437
|
-
emitWrapped(marker + stripMarkdownInline(unordered[3]), baseBodySgr, { indent, hang: indent + strWidth(marker) });
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
const ordered = line.match(/^(\s*)(\d{1,4}[.)])\s+(.+)$/);
|
|
441
|
-
if (ordered) {
|
|
442
|
-
const indent = Math.min(8, Math.floor(ordered[1].length / 2) * 2);
|
|
443
|
-
const marker = ordered[2] + ' ';
|
|
444
|
-
emitWrapped(marker + stripMarkdownInline(ordered[3]), baseBodySgr, { indent, hang: indent + strWidth(marker) });
|
|
443
|
+
emitWrapped(formatMarkdownTableRow(line, ctx), baseBodySgr, { hang: 4 });
|
|
445
444
|
continue;
|
|
446
445
|
}
|
|
447
446
|
if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
|
|
448
447
|
emitWrapped('─'.repeat(Math.max(8, Math.min(40, cols - prefixW))), [SGR.dim]);
|
|
449
448
|
continue;
|
|
450
449
|
}
|
|
451
|
-
emitWrapped(stripMarkdownInline(line), baseBodySgr);
|
|
450
|
+
emitWrapped(stripMarkdownInline(item ? item.text : line, ctx), baseBodySgr, item ? { indent: item.indent, hang: item.hang } : {}); // a list item's continuation paragraph hangs at its level
|
|
452
451
|
}
|
|
453
452
|
|
|
454
453
|
return out;
|
|
@@ -524,8 +523,9 @@ function jsonToolResultPreview(text, cols, style) {
|
|
|
524
523
|
].join('\n');
|
|
525
524
|
}
|
|
526
525
|
|
|
527
|
-
// Returns { lines: string[], known: boolean } for one message.
|
|
528
|
-
|
|
526
|
+
// Returns { lines: string[], known: boolean } for one message. `ctx` is the render opts (its cwd
|
|
527
|
+
// drives the local-link display target).
|
|
528
|
+
function renderMessage(msg, cols, table, { fullHistory = false, ctx } = {}) {
|
|
529
529
|
const meta = messageMeta(msg);
|
|
530
530
|
const kind = meta && meta.kind ? meta.kind : null;
|
|
531
531
|
const role = msg && msg.role;
|
|
@@ -533,7 +533,7 @@ function renderMessage(msg, cols, table, { fullHistory = false } = {}) {
|
|
|
533
533
|
// Plain assistant / user text (no structured kind).
|
|
534
534
|
if (!kind) {
|
|
535
535
|
if (role === 'user') return { lines: renderTextBlock(String(msg.text || ''), cols, table.user), known: true };
|
|
536
|
-
if (role === 'assistant') return { lines: renderMarkdownTextBlock(String(msg.text || ''), cols, table.assistant), known: true };
|
|
536
|
+
if (role === 'assistant') return { lines: renderMarkdownTextBlock(String(msg.text || ''), cols, table.assistant, ctx), known: true };
|
|
537
537
|
return { lines: [], known: false };
|
|
538
538
|
}
|
|
539
539
|
|
|
@@ -715,7 +715,7 @@ function renderOneMessage(msg, cols, table, fullHistory, ctx) {
|
|
|
715
715
|
const meta = messageMeta(msg);
|
|
716
716
|
kind = (meta && meta.kind) || (msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'text');
|
|
717
717
|
rendered = (table.owned === true && owned.renderOwnedMessage(msg, cols, table, ctx))
|
|
718
|
-
|| renderMessage(msg, cols, table, { fullHistory: fullHistory === true });
|
|
718
|
+
|| renderMessage(msg, cols, table, { fullHistory: fullHistory === true, ctx });
|
|
719
719
|
} catch { rendered = { lines: [], known: false }; } // one bad row must never sink the whole history
|
|
720
720
|
return { kind, rendered };
|
|
721
721
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Port of codex-rs/tui/src/markdown_render/local_links.rs (plus the `#L..` fragment helper at
|
|
4
|
+
// codex-rs/utils/string/src/lib.rs:73-104).
|
|
5
|
+
//
|
|
6
|
+
// Codex keeps a local link's label and ALSO shows its target, shortening an absolute destination
|
|
7
|
+
// relative to the session working directory when that path lies strictly underneath it. A local
|
|
8
|
+
// link therefore draws as `label (target)`, or as `target` alone when the label is blank. Rows CTM
|
|
9
|
+
// paints must equal the rows the live Codex screen paints, so this is a display rule, not a
|
|
10
|
+
// filesystem lookup: it never resolves files, symlinks or the frontend home, and it never collapses
|
|
11
|
+
// `.` / `..`.
|
|
12
|
+
//
|
|
13
|
+
// Names follow the Rust 1:1 in camelCase. `renderLocalLinkTarget` returns null only for a `file://`
|
|
14
|
+
// URL that does not parse into a local path; plain path-like input always yields a target.
|
|
15
|
+
|
|
16
|
+
/** Path-like destinations: `file://`, absolute, `~/`, `./`, `../`, `\\`, or `X:/` / `X:\`. */
|
|
17
|
+
function isLocalPathLikeLink(destUrl) {
|
|
18
|
+
const value = String(destUrl == null ? '' : destUrl);
|
|
19
|
+
return value.startsWith('file://') || value.startsWith('/') || value.startsWith('~/')
|
|
20
|
+
|| value.startsWith('./') || value.startsWith('../') || value.startsWith('\\\\')
|
|
21
|
+
|| /^[A-Za-z]:[\\/]/.test(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Trailing `:line`, `:line:col` or `:a-b` run (an en dash is accepted, as in Codex's regex). */
|
|
25
|
+
const COLON_LOCATION_SUFFIX_RE = /:\d+(?::\d+)?(?:[-\u2013]\d+(?::\d+)?)?$/;
|
|
26
|
+
/** `L12`, `L12C3`, `L12-L14`, `L12C3-L14C9` — a markdown `#L..` location fragment. */
|
|
27
|
+
const HASH_LOCATION_SUFFIX_RE = /^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$/;
|
|
28
|
+
const HEX_PAIR_RE = /^[0-9A-Fa-f]{2}$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A local link's display target, or null when a `file://` URL does not parse into a local path.
|
|
32
|
+
* The path is normalized and shortened relative to `cwd` when it lies strictly under it; the
|
|
33
|
+
* location suffix (`#L..` normalised to `:line[:col]`) is appended back.
|
|
34
|
+
*/
|
|
35
|
+
function renderLocalLinkTarget(destUrl, cwd) {
|
|
36
|
+
const parsed = parseLocalLinkTarget(destUrl);
|
|
37
|
+
if (!parsed) return null;
|
|
38
|
+
let rendered = displayLocalLinkPath(parsed[0], cwd);
|
|
39
|
+
if (parsed[1] != null) rendered += parsed[1];
|
|
40
|
+
return rendered;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Split a local-link destination into `[normalizedPathText, locationSuffixOrNull]`, or null. */
|
|
44
|
+
function parseLocalLinkTarget(destUrl) {
|
|
45
|
+
const dest = String(destUrl == null ? '' : destUrl);
|
|
46
|
+
if (dest.startsWith('file://')) {
|
|
47
|
+
let url;
|
|
48
|
+
try { url = new URL(dest); } catch { return null; } // an unparseable file:// URL has no target
|
|
49
|
+
// `PathUri::parse(&url[..Position::AfterPath])`: the query and fragment are not part of the path.
|
|
50
|
+
const pathText = pathUriNativePath(`${url.protocol}//${url.host}${url.pathname}`);
|
|
51
|
+
if (pathText == null) return null;
|
|
52
|
+
const fragment = url.hash === '' ? null : url.hash.slice(1);
|
|
53
|
+
const locationSuffix = fragment == null ? null : normalizeHashLocationSuffixFragment(fragment);
|
|
54
|
+
return [normalizeLocalLinkPathText(pathText), locationSuffix];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let pathText = dest;
|
|
58
|
+
let locationSuffix = null;
|
|
59
|
+
// Prefer `#L..` when both forms are present so `path#L10` is not misread as a path ending in `:10`.
|
|
60
|
+
const hash = dest.lastIndexOf('#');
|
|
61
|
+
if (hash >= 0) {
|
|
62
|
+
const normalized = normalizeHashLocationSuffixFragment(dest.slice(hash + 1));
|
|
63
|
+
if (normalized != null) { pathText = dest.slice(0, hash); locationSuffix = normalized; }
|
|
64
|
+
}
|
|
65
|
+
if (locationSuffix == null) {
|
|
66
|
+
const suffix = extractColonLocationSuffix(pathText);
|
|
67
|
+
if (suffix != null) { pathText = pathText.slice(0, pathText.length - suffix.length); locationSuffix = suffix; }
|
|
68
|
+
}
|
|
69
|
+
return [normalizeLocalLinkPathText(urlDecode(pathText)), locationSuffix];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `#L12`-style fragment -> `:12[:3][-14[:9]]`, or null when the fragment is not a location. */
|
|
73
|
+
function normalizeHashLocationSuffixFragment(fragment) {
|
|
74
|
+
const text = String(fragment == null ? '' : fragment);
|
|
75
|
+
return HASH_LOCATION_SUFFIX_RE.test(text) ? normalizeMarkdownHashLocationSuffix(`#${text}`) : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `codex_utils_string::normalize_markdown_hash_location_suffix` (`#L74C3-L76C9` -> `:74:3-76:9`). */
|
|
79
|
+
function normalizeMarkdownHashLocationSuffix(suffix) {
|
|
80
|
+
const text = String(suffix == null ? '' : suffix);
|
|
81
|
+
if (!text.startsWith('#')) return null;
|
|
82
|
+
const fragment = text.slice(1);
|
|
83
|
+
const dash = fragment.indexOf('-');
|
|
84
|
+
const start = parseMarkdownHashLocationPoint(dash < 0 ? fragment : fragment.slice(0, dash));
|
|
85
|
+
if (!start) return null;
|
|
86
|
+
let normalized = `:${start.line}`;
|
|
87
|
+
if (start.column != null) normalized += `:${start.column}`;
|
|
88
|
+
if (dash >= 0) {
|
|
89
|
+
const end = parseMarkdownHashLocationPoint(fragment.slice(dash + 1));
|
|
90
|
+
if (!end) return null;
|
|
91
|
+
normalized += `-${end.line}`;
|
|
92
|
+
if (end.column != null) normalized += `:${end.column}`;
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseMarkdownHashLocationPoint(point) {
|
|
98
|
+
const text = String(point == null ? '' : point);
|
|
99
|
+
if (!text.startsWith('L')) return null;
|
|
100
|
+
const rest = text.slice(1);
|
|
101
|
+
const column = rest.indexOf('C');
|
|
102
|
+
return column < 0 ? { line: rest, column: null } : { line: rest.slice(0, column), column: rest.slice(column + 1) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A trailing `:line[:col][-line[:col]]` run at the very end of a plain path-like string. */
|
|
106
|
+
function extractColonLocationSuffix(pathText) {
|
|
107
|
+
const matched = COLON_LOCATION_SUFFIX_RE.exec(String(pathText == null ? '' : pathText));
|
|
108
|
+
return matched ? matched[0] : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Local-path display normalisation: forward slashes everywhere, and UNC-style `\\server\share`
|
|
113
|
+
* rewritten to `//server/share` so the prefix checks below see one stable spelling.
|
|
114
|
+
*/
|
|
115
|
+
function normalizeLocalLinkPathText(pathText) {
|
|
116
|
+
const text = String(pathText == null ? '' : pathText);
|
|
117
|
+
if (text.startsWith('\\\\')) return `//${text.slice(2).replace(/\\/g, '/').replace(/^\/+/, '')}`;
|
|
118
|
+
return text.replace(/\\/g, '/');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isAbsoluteLocalLinkPath(pathText) {
|
|
122
|
+
const text = String(pathText == null ? '' : pathText);
|
|
123
|
+
return text.startsWith('/') || text.startsWith('//') || /^[A-Za-z]:\//.test(text);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Trim trailing separators without destroying root semantics (`/`, `//`, `C:/` stay intact). */
|
|
127
|
+
function trimTrailingLocalPathSeparator(pathText) {
|
|
128
|
+
const text = String(pathText == null ? '' : pathText);
|
|
129
|
+
if (text === '/' || text === '//' || /^[A-Za-z]:\/$/.test(text)) return text;
|
|
130
|
+
return text.replace(/\/+$/, '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The part of `pathText` strictly under `cwdText`, without a leading slash; null when it is not. */
|
|
134
|
+
function stripLocalPathPrefix(pathText, cwdText) {
|
|
135
|
+
const path = trimTrailingLocalPathSeparator(pathText);
|
|
136
|
+
const cwd = trimTrailingLocalPathSeparator(cwdText);
|
|
137
|
+
if (path === cwd) return null; // the path IS the cwd: keep it whole rather than render an empty target
|
|
138
|
+
if (cwd === '/' || cwd === '//') return path.startsWith('/') ? path.slice(1) : null;
|
|
139
|
+
if (!path.startsWith(cwd)) return null;
|
|
140
|
+
const rest = path.slice(cwd.length);
|
|
141
|
+
return rest.startsWith('/') ? rest.slice(1) : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Relative paths (including `~/...`) stay relative; absolute paths prefer the cwd-relative form. */
|
|
145
|
+
function displayLocalLinkPath(pathText, cwd) {
|
|
146
|
+
const path = normalizeLocalLinkPathText(pathText);
|
|
147
|
+
if (!isAbsoluteLocalLinkPath(path)) return path;
|
|
148
|
+
if (typeof cwd === 'string') {
|
|
149
|
+
const stripped = stripLocalPathPrefix(path, normalizeLocalLinkPathText(cwd));
|
|
150
|
+
if (stripped != null) return stripped;
|
|
151
|
+
}
|
|
152
|
+
return path;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- PathUri (codex-rs/utils/path-uri): only what a `file://` link destination needs ---
|
|
156
|
+
|
|
157
|
+
/** The native path a `file:` URI names, or null when it is not a usable local path URI. */
|
|
158
|
+
function pathUriNativePath(uri) {
|
|
159
|
+
let url;
|
|
160
|
+
try { url = new URL(uri); } catch { return null; }
|
|
161
|
+
if (url.protocol !== 'file:') return null;
|
|
162
|
+
if (url.username !== '' || url.password !== '' || url.port !== '') return null;
|
|
163
|
+
// A decoded NUL cannot be a native path (Codex rejects it the same way).
|
|
164
|
+
if (decodeBinaryUtf8(url.pathname).includes('\u0000')) return null;
|
|
165
|
+
const host = url.hostname === 'localhost' ? '' : url.hostname; // a localhost authority is dropped
|
|
166
|
+
const pathname = host === '' ? withNormalizedWindowsDriveLetter(url.pathname) : url.pathname;
|
|
167
|
+
const segments = pathSegments(pathname);
|
|
168
|
+
const rendered = inferPathConvention(host, segments) === 'windows'
|
|
169
|
+
? renderWindowsPath(host, segments) : renderPosixPath(host, segments);
|
|
170
|
+
return rendered == null ? `file://${host}${pathname}` : rendered; // fallback: the canonical URI text
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function pathSegments(pathname) {
|
|
174
|
+
const path = String(pathname);
|
|
175
|
+
return path.startsWith('/') ? path.slice(1).split('/') : path.split('/');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Uppercase a leading `c:` drive segment so later comparison is stable across spellings. */
|
|
179
|
+
function withNormalizedWindowsDriveLetter(pathname) {
|
|
180
|
+
const matched = /^(\/*)([^/]*)([\s\S]*)$/.exec(String(pathname));
|
|
181
|
+
if (!matched) return pathname;
|
|
182
|
+
const [, slashes, drive, rest] = matched;
|
|
183
|
+
if (!isWindowsDriveUriSegment(drive) || !/^[a-z]/.test(drive)) return pathname;
|
|
184
|
+
return `${slashes}${drive[0].toUpperCase()}${drive.slice(1)}${rest}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isWindowsDriveUriSegment(segment) {
|
|
188
|
+
const text = String(segment == null ? '' : segment);
|
|
189
|
+
return /^[A-Za-z]:$/.test(text) || /^[A-Za-z]%3[Aa]$/.test(text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** An authority means Windows (UNC); otherwise a leading drive segment does, else POSIX. */
|
|
193
|
+
function inferPathConvention(host, segments) {
|
|
194
|
+
if (host !== '') return 'windows';
|
|
195
|
+
const first = segments.find((segment) => segment !== '');
|
|
196
|
+
return first != null && isWindowsDriveUriSegment(first) ? 'windows' : 'posix';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderPosixPath(host, segments) {
|
|
200
|
+
if (host !== '') return null; // a UNC authority cannot be spelled as a POSIX path
|
|
201
|
+
return segments.map((segment) => `/${decodeNativeSegment(segment)}`).join('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderWindowsPath(host, segments) {
|
|
205
|
+
let rendered;
|
|
206
|
+
let rest;
|
|
207
|
+
if (host !== '') {
|
|
208
|
+
// `file://server/share/file` -> `\\server\share\file`; the share name must be present.
|
|
209
|
+
if (!segments.length) return null;
|
|
210
|
+
const share = decodeNativeSegment(segments[0]);
|
|
211
|
+
if (share === '') return null;
|
|
212
|
+
rendered = `\\\\${host}\\${share}`;
|
|
213
|
+
rest = segments.slice(1);
|
|
214
|
+
} else {
|
|
215
|
+
// Without an authority Windows needs a drive root: `file:///C:/src` -> `C:\src`.
|
|
216
|
+
if (!segments.length) return null;
|
|
217
|
+
const drive = decodeNativeSegment(segments[0]);
|
|
218
|
+
if (!/^[A-Za-z]:$/.test(drive)) return null;
|
|
219
|
+
rendered = drive;
|
|
220
|
+
rest = segments.slice(1);
|
|
221
|
+
}
|
|
222
|
+
for (const segment of rest) rendered += `\\${decodeNativeSegment(segment)}`;
|
|
223
|
+
// `file:///C:` and `file:///C:/` both name the drive root, never the drive-relative `C:`.
|
|
224
|
+
return /^[A-Za-z]:$/.test(rendered) ? `${rendered}\\` : rendered;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** `urlencoding::decode_binary` + lossy UTF-8: a URI path segment as native path text. */
|
|
228
|
+
function decodeNativeSegment(segment) {
|
|
229
|
+
return decodeBinaryUtf8(segment);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** `urlencoding::decode`: percent-decoding that fails (and keeps the input) on invalid UTF-8. */
|
|
233
|
+
function urlDecode(text) {
|
|
234
|
+
const decoded = decodeBinaryUtf8(text, true);
|
|
235
|
+
return decoded == null ? String(text) : decoded;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function decodeBinaryUtf8(text, strict = false) {
|
|
239
|
+
const bytes = [];
|
|
240
|
+
const value = String(text == null ? '' : text);
|
|
241
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
242
|
+
const pair = value[index] === '%' ? value.slice(index + 1, index + 3) : '';
|
|
243
|
+
if (HEX_PAIR_RE.test(pair)) { bytes.push(parseInt(pair, 16)); index += 2; continue; }
|
|
244
|
+
const codePoint = value.codePointAt(index);
|
|
245
|
+
const character = String.fromCodePoint(codePoint);
|
|
246
|
+
if (character.length === 2) index += 1; // a surrogate pair is one code point
|
|
247
|
+
for (const byte of Buffer.from(character, 'utf8')) bytes.push(byte);
|
|
248
|
+
}
|
|
249
|
+
const buffer = Buffer.from(bytes);
|
|
250
|
+
if (!strict) return buffer.toString('utf8');
|
|
251
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(buffer); } catch { return null; }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
isLocalPathLikeLink,
|
|
256
|
+
renderLocalLinkTarget,
|
|
257
|
+
// Exported for the ported Codex test table (same functions, same behaviour).
|
|
258
|
+
_internal: {
|
|
259
|
+
parseLocalLinkTarget,
|
|
260
|
+
displayLocalLinkPath,
|
|
261
|
+
normalizeLocalLinkPathText,
|
|
262
|
+
trimTrailingLocalPathSeparator,
|
|
263
|
+
stripLocalPathPrefix,
|
|
264
|
+
extractColonLocationSuffix,
|
|
265
|
+
normalizeMarkdownHashLocationSuffix,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Codex list markers, at parity with what the TUI paints, ported from
|
|
4
|
+
// codex-rs/tui/src/markdown_render.rs start_item (:1014-1090), prefix_spans (:2535-2565)
|
|
5
|
+
// codex-rs/tui/src/markdown_render/task_lists.rs task_list_marker
|
|
6
|
+
// codex-rs/tui/src/markdown_render_tests.rs list_unordered_single / list_nested / nested_five_levels_mixed_lists / ...
|
|
7
|
+
//
|
|
8
|
+
// Why this module exists: the reader joins CTM's rendered rows to the rows the live Codex screen
|
|
9
|
+
// actually shows, by identity of the row text. Codex draws unordered items as `• ` (PR #47322, on
|
|
10
|
+
// by default) and nests by a FIXED 4-column step, while the first CTM renderer drew `- ` indented by
|
|
11
|
+
// floor(spaces/2)*2 — so every list row missed the join and the reader drew it twice.
|
|
12
|
+
//
|
|
13
|
+
// The two facts that make the port non-obvious:
|
|
14
|
+
// * depth is the list nesting level; `width = depth*4 - 3`. The marker of an item at depth d is
|
|
15
|
+
// ONLY its own marker: prefix_spans skips every list context except the innermost, so an outer
|
|
16
|
+
// level contributes nothing to a nested row (no cumulative indent).
|
|
17
|
+
// * depth is NOT floor(spaces/2). It comes from a CommonMark container stack over the source
|
|
18
|
+
// lines; each open level remembers its item's CONTENT column, and a list line either opens a
|
|
19
|
+
// deeper level, lands as a sibling, or closes levels.
|
|
20
|
+
//
|
|
21
|
+
// Pure: no DOM, no clock, no I/O. Same input => same output. Tabs are already expanded to 4 spaces
|
|
22
|
+
// by the caller (renderMarkdownTextBlock does this before the line loop).
|
|
23
|
+
|
|
24
|
+
const BULLET = '•';
|
|
25
|
+
const UNCHECKED = '☐';
|
|
26
|
+
const CHECKED = '☑';
|
|
27
|
+
|
|
28
|
+
// Codex nests by a fixed 4-column step (markdown_render.rs:1042: `width = depth * 4 - 3`).
|
|
29
|
+
const HANG_STEP = 4;
|
|
30
|
+
|
|
31
|
+
// `- x` / `* x` / `+ x` and `1. x` / `1) x`. The content must be non-blank, so a bare rule or a
|
|
32
|
+
// lone marker keeps falling through to the renderer's paragraph path exactly as before.
|
|
33
|
+
const BULLET_LINE = /^( *)([-*+])( +)(\S.*)$/;
|
|
34
|
+
const ORDERED_LINE = /^( *)(\d{1,4})([.)])( +)(\S.*)$/;
|
|
35
|
+
// A GFM task checkbox, right after the marker (task_lists.rs:8-24). `[X]` counts as checked.
|
|
36
|
+
const TASK = /^\[([ xX])\](?: |$)/;
|
|
37
|
+
|
|
38
|
+
// `width` for a depth (markdown_render.rs:1042): depth 1 => 1, depth 2 => 5, depth 3 => 9 ...
|
|
39
|
+
function markerWidthFor(depth) {
|
|
40
|
+
return depth * HANG_STEP - 3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The column a wrapped (continuation) row of this item hangs at (markdown_render.rs:1057-1061):
|
|
44
|
+
// unordered `width + 1`, ordered `width + 2` whatever the digit count. A task item hangs at its whole
|
|
45
|
+
// marker's display width instead (itemOf).
|
|
46
|
+
function hangFor(entry) {
|
|
47
|
+
return entry.width + (entry.ordered ? 2 : 1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// markdown_render.rs:1043-1056 + task_lists.rs:15-19: the task checkbox REPLACES a trailing `• `
|
|
51
|
+
// and keeps an ordered `N. ` prefix.
|
|
52
|
+
function markerFor(entry, index) {
|
|
53
|
+
const glyph = entry.task ? (entry.checked ? CHECKED : UNCHECKED) : BULLET;
|
|
54
|
+
if (!entry.ordered) return ' '.repeat(Math.max(0, entry.width - 1)) + glyph + ' ';
|
|
55
|
+
return String(index).padStart(entry.width) + '. ' + (entry.task ? glyph + ' ' : '');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Builds the row a list item paints: the marker (which already carries its own leading columns)
|
|
59
|
+
// plus the source text. `indent` is 0 for an item row because the marker string holds its column,
|
|
60
|
+
// and `hang` is the absolute continuation column. The caller adds the message prefix in front
|
|
61
|
+
// (rule R-1.6) and strips inline markdown from `text` alone, so the marker never joins a match.
|
|
62
|
+
function itemOf(entry, index, text) {
|
|
63
|
+
const marker = markerFor(entry, index);
|
|
64
|
+
// A task item hangs at its WHOLE marker's display width (task_lists.rs:22), so `10. ☐ ` hangs 6;
|
|
65
|
+
// every char of a marker is one column wide.
|
|
66
|
+
return { marker, text, indent: 0, hang: entry.task ? marker.length : hangFor(entry) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A tracker owns the CommonMark container stack for one text block. `consume(line)` must be called
|
|
70
|
+
// with EVERY source line, in order, including blank ones: a blank line does not close the stack,
|
|
71
|
+
// and a non-list line may be a continuation paragraph of the innermost item.
|
|
72
|
+
function createListTracker() {
|
|
73
|
+
const stack = []; // innermost last; each entry = { ordered, task, checked, width, contentCol, next }
|
|
74
|
+
|
|
75
|
+
const push = (fields) => {
|
|
76
|
+
const entry = {
|
|
77
|
+
ordered: fields.ordered,
|
|
78
|
+
task: fields.task,
|
|
79
|
+
checked: fields.checked,
|
|
80
|
+
width: markerWidthFor(stack.length + 1),
|
|
81
|
+
contentCol: fields.contentCol,
|
|
82
|
+
next: fields.next,
|
|
83
|
+
};
|
|
84
|
+
stack.push(entry);
|
|
85
|
+
return entry;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
// -> null when the line is not a list item (or a continuation of one); otherwise the row
|
|
90
|
+
// { marker, text, indent, hang } the renderer draws. `marker` is '' for a continuation row.
|
|
91
|
+
consume(rawLine) {
|
|
92
|
+
const line = String(rawLine == null ? '' : rawLine);
|
|
93
|
+
if (!line.trim()) return null; // a blank line does not close (R-1.5)
|
|
94
|
+
|
|
95
|
+
const bullet = BULLET_LINE.exec(line);
|
|
96
|
+
const ordered = bullet ? null : ORDERED_LINE.exec(line);
|
|
97
|
+
|
|
98
|
+
if (!bullet && !ordered) {
|
|
99
|
+
const top = stack[stack.length - 1];
|
|
100
|
+
const indent = line.length - line.replace(/^ */, '').length;
|
|
101
|
+
if (top && indent >= top.contentCol) {
|
|
102
|
+
// A non-blank, non-list line indented to the item's content column continues that item,
|
|
103
|
+
// rendered at the level's hang (R-1.5).
|
|
104
|
+
const hang = hangFor(top);
|
|
105
|
+
return { marker: '', text: line.trim(), indent: hang, hang };
|
|
106
|
+
}
|
|
107
|
+
stack.length = 0; // any other non-blank line closes every level
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const markerIndent = (bullet ? bullet[1] : ordered[1]).length;
|
|
112
|
+
// The marker LENGTH is `-` (1) or `N.` (digits + the bracket) — the checkbox is not part of
|
|
113
|
+
// it, so `- [ ] a` still has content column 2.
|
|
114
|
+
const markerLen = bullet ? 1 : ordered[2].length + 1;
|
|
115
|
+
const gap = (bullet ? bullet[3] : ordered[4]).length;
|
|
116
|
+
const contentCol = markerIndent + markerLen + gap;
|
|
117
|
+
const content = bullet ? bullet[4] : ordered[5];
|
|
118
|
+
const task = TASK.exec(content);
|
|
119
|
+
const isOrdered = !bullet;
|
|
120
|
+
const text = task ? content.slice(task[0].length) : content;
|
|
121
|
+
const fields = {
|
|
122
|
+
ordered: isOrdered,
|
|
123
|
+
task: !!task,
|
|
124
|
+
checked: !!task && task[1] !== ' ',
|
|
125
|
+
contentCol,
|
|
126
|
+
next: isOrdered ? Number(ordered[2]) : 0,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const top = stack[stack.length - 1];
|
|
130
|
+
let entry;
|
|
131
|
+
if (top && markerIndent >= top.contentCol) {
|
|
132
|
+
entry = push(fields); // deeper than the innermost item's content -> a nested level
|
|
133
|
+
} else {
|
|
134
|
+
// Pop while the marker column sits left of the content column of the level BELOW the top.
|
|
135
|
+
while (stack.length >= 2 && markerIndent < stack[stack.length - 2].contentCol) stack.pop();
|
|
136
|
+
const sibling = stack[stack.length - 1];
|
|
137
|
+
if (!sibling || sibling.ordered !== isOrdered) {
|
|
138
|
+
// A sibling of the other type starts a NEW list at this depth; its numbering restarts
|
|
139
|
+
// from its own first source number.
|
|
140
|
+
if (sibling) stack.pop();
|
|
141
|
+
entry = push(fields);
|
|
142
|
+
} else {
|
|
143
|
+
entry = sibling; // same list: keep the level, refresh the per-item task state
|
|
144
|
+
entry.task = fields.task;
|
|
145
|
+
entry.checked = fields.checked;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let index = 0;
|
|
150
|
+
if (entry.ordered) {
|
|
151
|
+
index = entry.next; // counts from the list's first source number, per sibling
|
|
152
|
+
entry.next += 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return itemOf(entry, index, text);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { createListTracker };
|
|
@@ -191,6 +191,10 @@ function findCanonicalRolloutsByFilenameId(threadId, storageRoots) {
|
|
|
191
191
|
const candidate = path.join(dir, entry.name);
|
|
192
192
|
if (entry.isDirectory()) { stack.push({ dir: candidate, depth: depth + 1 }); continue; }
|
|
193
193
|
if (!entry.isFile() || sessionHistory.codexRolloutIdFromPath(candidate).toLowerCase() !== wanted) continue;
|
|
194
|
+
// Codex rust-v0.163.0 rollout/src/compression.rs should_skip_compressed_sibling: one rollout is
|
|
195
|
+
// one physical file, and a `.jsonl.zst` is hidden while its plain `.jsonl` exists (Codex
|
|
196
|
+
// compresses an idle thread, then appends to a re-materialized plain file beside the stale copy).
|
|
197
|
+
if (candidate.endsWith('.jsonl.zst') && transcriptSource.statTranscriptSync(candidate.slice(0, -'.zst'.length)).exists) continue;
|
|
194
198
|
const info = transcriptSource.statTranscriptSync(candidate);
|
|
195
199
|
if (!info.exists) throw Object.assign(new Error('native history candidate changed during lookup'), { code: 'native_history_lineage_changed' });
|
|
196
200
|
let canonical;
|