Files
openclaw/ui/src/lib/chat/session-diff.ts
Peter Steinberger 096cb91175 feat(tooling): enforce noUncheckedIndexedAccess in core and ui lanes (NUIA phase 3c) (#104981)
* fix(ui): make indexed access explicit across the Control UI

Burns down all 322 ui-lane noUncheckedIndexedAccess errors: untrusted
markdown/diff/patch parsing gets miss-tolerant guards (renderer rules
degrade to empty output instead of throwing mid-render), accumulators
use canonical sparse initialization, DOM lookups stay null-guarded, and
length-checked constructions carry named invariants.

* feat(tooling): enforce noUncheckedIndexedAccess in the core and ui lanes

Flips the flag on for tsconfig.core.json and tsconfig.ui.json (covering
src, all packages including the two deferred ones, and ui) and retires
the strict-ratchet lane wholesale: config, script, projects reference,
check/CI wiring, changed-lane routing, and sync test. The assertion-ban
oxlint override stays as an independent surface.
2026-07-12 06:36:36 +01:00

75 lines
2.3 KiB
TypeScript

/**
* Session diff panel parsing: turns the per-file unified patches returned by
* the `sessions.diff` gateway method into renderable DiffLine rows, with
* hunk-gap markers ("N unmodified lines") instead of bare separators.
*/
import type { DiffLine } from "./tool-call-diff.ts";
/** Per-file render bound; the panel shows a truncation notice past this. */
export const MAX_SESSION_DIFF_FILE_LINES = 600;
export type ParsedFilePatch = {
lines: DiffLine[];
truncated: boolean;
};
/**
* Parses one file's unified patch (header lines + hunks) into DiffLine rows.
* Gaps between hunks become "skip" rows whose text carries the formatted
* unmodified-line count supplied by the caller (kept out of this lib so the
* parser stays i18n-free).
*/
export function parseSessionDiffPatch(
patch: string,
formatGap: (count: number) => string,
maxLines = MAX_SESSION_DIFF_FILE_LINES,
): ParsedFilePatch {
const lines: DiffLine[] = [];
let truncated = false;
let inHunk = false;
let oldNo = 0;
let newNo = 0;
// Next expected old-file line after the previous hunk; drives gap counts.
let oldNext: number | undefined;
const rawLines = patch.replace(/\r\n/g, "\n").split("\n");
if (rawLines.at(-1) === "") {
rawLines.pop();
}
for (const raw of rawLines) {
const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
if (hunk) {
const oldStart = Number.parseInt(hunk[1] ?? "", 10);
const newStart = Number.parseInt(hunk[2] ?? "", 10);
const gap = oldNext === undefined ? oldStart - 1 : oldStart - oldNext;
if (gap > 0) {
lines.push({ kind: "skip", text: formatGap(gap) });
}
oldNo = oldStart;
newNo = newStart;
inHunk = true;
continue;
}
if (!inHunk || raw.startsWith("\\")) {
// Header lines before the first hunk and "\ No newline at end of file".
continue;
}
if (lines.length >= maxLines) {
truncated = true;
break;
}
if (raw.startsWith("+")) {
lines.push({ kind: "add", lineNo: newNo, text: raw.slice(1) });
newNo += 1;
} else if (raw.startsWith("-")) {
lines.push({ kind: "del", lineNo: oldNo, text: raw.slice(1) });
oldNo += 1;
} else {
lines.push({ kind: "ctx", lineNo: newNo, text: raw.slice(1) });
oldNo += 1;
newNo += 1;
}
oldNext = oldNo;
}
return { lines, truncated };
}