feat(maestro): wire React app as sole web entry point
Deletes the vanilla shell and mounts App.tsx on #root. main.ts and the 23 views it drove are gone; App.tsx composes the ported components — sidebar, multi-view, command bar, spawn modal, help and restart overlays, daemon link, drawer — around the catalog and layout stores. The pure parts of four deleted modules moved next to the components that still use them: the spawn-form model and spawnSimilar into components/spawn, the transcript's markdown/format helpers and the diff into components/structured, the help overlay's grouping into components/dialogs. Document-level event names now live in uiEvents.ts. Deduplicated two pairs the migration left side by side: the sidebar uses dialogs/KillDialog and common/ContextMenu. It also listens for the command bar's kill/rename events, which nothing had picked up since catalog.ts went away. styles.css stays as the token and base layer, with `#app` renamed to `#root` and the pane-body rules re-keyed off the classes the React panes actually carry. Closes #253
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// Line-level diff for a `ToolDiff` row's `Diff` content block (#140). ACP
|
||||
// hands over full `oldText`/`newText`, not a unified-diff patch, so the
|
||||
// coloured hunks the design calls for have to be computed here.
|
||||
|
||||
export type DiffLineKind = "add" | "del" | "ctx" | "hunk" | "eof";
|
||||
|
||||
export interface DiffLine {
|
||||
kind: DiffLineKind;
|
||||
/** The line's text (a `hunk` line is a human summary, not source text). */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** Interior unchanged runs longer than this collapse to a summary line
|
||||
* flanked by this many lines of real context on each side. */
|
||||
const CONTEXT = 3;
|
||||
|
||||
/** Bounds the O(n·m) LCS table: above this cell count, the changed middle is
|
||||
* shown as one coarse del-then-add block instead of a fine-grained diff —
|
||||
* still correct, just not minimal. */
|
||||
const LCS_CELL_LIMIT = 250_000;
|
||||
|
||||
interface SourceLine {
|
||||
text: string;
|
||||
terminated: boolean;
|
||||
}
|
||||
|
||||
function splitLines(text: string): SourceLine[] {
|
||||
if (text === "") return [];
|
||||
const parts = text.split("\n");
|
||||
const hasFinalNewline = text.endsWith("\n");
|
||||
if (hasFinalNewline) parts.pop();
|
||||
return parts.map((line, index) => ({
|
||||
text: line,
|
||||
terminated: index < parts.length - 1 || hasFinalNewline,
|
||||
}));
|
||||
}
|
||||
|
||||
type Op = { kind: "add" | "del" | "ctx"; line: SourceLine };
|
||||
|
||||
function sameLine(a: SourceLine, b: SourceLine): boolean {
|
||||
return a.text === b.text && a.terminated === b.terminated;
|
||||
}
|
||||
|
||||
/** Longest-common-subsequence diff of two line arrays, already trimmed of any
|
||||
* shared prefix/suffix by the caller. */
|
||||
function lcsDiff(a: SourceLine[], b: SourceLine[]): Op[] {
|
||||
const n = a.length;
|
||||
const m = b.length;
|
||||
if (n === 0) return b.map((line) => ({ kind: "add", line }));
|
||||
if (m === 0) return a.map((line) => ({ kind: "del", line }));
|
||||
if (n * m > LCS_CELL_LIMIT) {
|
||||
return [...a.map((line) => ({ kind: "del" as const, line })), ...b.map((line) => ({ kind: "add" as const, line }))];
|
||||
}
|
||||
// dp[i][j] = length of the LCS of a[i:] and b[j:].
|
||||
const columns = m + 1;
|
||||
const dp = new Uint32Array((n + 1) * columns);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
for (let j = m - 1; j >= 0; j--) {
|
||||
dp[i * columns + j] = sameLine(a[i], b[j])
|
||||
? dp[(i + 1) * columns + j + 1] + 1
|
||||
: Math.max(dp[(i + 1) * columns + j], dp[i * columns + j + 1]);
|
||||
}
|
||||
}
|
||||
const ops: Op[] = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < n && j < m) {
|
||||
if (sameLine(a[i], b[j])) {
|
||||
ops.push({ kind: "ctx", line: a[i] });
|
||||
i++;
|
||||
j++;
|
||||
} else if (dp[(i + 1) * columns + j] >= dp[i * columns + j + 1]) {
|
||||
ops.push({ kind: "del", line: a[i] });
|
||||
i++;
|
||||
} else {
|
||||
ops.push({ kind: "add", line: b[j] });
|
||||
j++;
|
||||
}
|
||||
}
|
||||
while (i < n) ops.push({ kind: "del", line: a[i++] });
|
||||
while (j < m) ops.push({ kind: "add", line: b[j++] });
|
||||
return ops;
|
||||
}
|
||||
|
||||
function diffLines(a: SourceLine[], b: SourceLine[]): Op[] {
|
||||
let pre = 0;
|
||||
while (pre < a.length && pre < b.length && sameLine(a[pre], b[pre])) pre++;
|
||||
let suf = 0;
|
||||
while (
|
||||
suf < a.length - pre &&
|
||||
suf < b.length - pre &&
|
||||
sameLine(a[a.length - 1 - suf], b[b.length - 1 - suf])
|
||||
) {
|
||||
suf++;
|
||||
}
|
||||
const prefixCtx: Op[] = a.slice(0, pre).map((line) => ({ kind: "ctx", line }));
|
||||
const suffixCtx: Op[] = a.slice(a.length - suf).map((line) => ({ kind: "ctx", line }));
|
||||
const middle = lcsDiff(a.slice(pre, a.length - suf), b.slice(pre, b.length - suf));
|
||||
return [...prefixCtx, ...middle, ...suffixCtx];
|
||||
}
|
||||
|
||||
/** Collapse a long run of unchanged lines to a summary, keeping up to
|
||||
* {@link CONTEXT} real lines adjacent to each neighbouring change — same
|
||||
* shape a unified diff gives a reader, without claiming line numbers this
|
||||
* module never tracked. A run touching the start/end of the whole diff has
|
||||
* no change to be adjacent to on that side, so it keeps no context there
|
||||
* (unless it is the *only* run, i.e. nothing changed at all). */
|
||||
function hunk(ops: Op[]): DiffLine[] {
|
||||
const out: DiffLine[] = [];
|
||||
const push = (op: Op) => {
|
||||
out.push({ kind: op.kind, text: op.line.text });
|
||||
if (!op.line.terminated) {
|
||||
out.push({ kind: "eof", text: "\\ No newline at end of file" });
|
||||
}
|
||||
};
|
||||
let i = 0;
|
||||
while (i < ops.length) {
|
||||
if (ops[i].kind !== "ctx") {
|
||||
push(ops[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
let j = i;
|
||||
while (j < ops.length && ops[j].kind === "ctx") j++;
|
||||
const run = ops.slice(i, j);
|
||||
const wholeDiff = i === 0 && j === ops.length;
|
||||
const keepHead = wholeDiff || i > 0 ? CONTEXT : 0;
|
||||
const keepTail = wholeDiff || j < ops.length ? CONTEXT : 0;
|
||||
if (run.length <= keepHead + keepTail) {
|
||||
run.forEach(push);
|
||||
} else {
|
||||
if (keepHead) run.slice(0, keepHead).forEach(push);
|
||||
const collapsed = run.slice(keepHead, run.length - keepTail);
|
||||
out.push({
|
||||
kind: "hunk",
|
||||
text: `⋯ ${collapsed.length} unchanged lines ⋯`,
|
||||
});
|
||||
if (collapsed.some((op) => !op.line.terminated)) {
|
||||
out.push({ kind: "eof", text: "\\ No newline at end of file" });
|
||||
}
|
||||
if (keepTail) run.slice(run.length - keepTail).forEach(push);
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Coloured, hunked diff lines for a `Diff` content block. `oldText` absent
|
||||
* (a new file) renders every line of `newText` as added. */
|
||||
export function computeDiffLines(oldText: string | null | undefined, newText: string): DiffLine[] {
|
||||
const newLines = splitLines(newText);
|
||||
if (oldText == null) return hunk(newLines.map((line) => ({ kind: "add", line })));
|
||||
const ops = diffLines(splitLines(oldText), newLines);
|
||||
return hunk(ops);
|
||||
}
|
||||
|
||||
/** `+added -removed` counts straight off the diff — real derived data, not a
|
||||
* fabricated duration or line count. */
|
||||
export function diffStats(lines: DiffLine[]): { added: number; removed: number } {
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
for (const line of lines) {
|
||||
if (line.kind === "add") added++;
|
||||
else if (line.kind === "del") removed++;
|
||||
}
|
||||
return { added, removed };
|
||||
}
|
||||
Reference in New Issue
Block a user