3e237e177a
* feat: add code-walkthrough backend (MCP tool, service, persistence, WS events) * feat: render agent code walkthroughs as line-anchored cards + editor window * test: add code-walkthrough re-emit mock-agent scenario * fix: rework code-walkthrough UX (floating card, line reveal, minimize, clickable file) * fix: improve code walkthrough editor UX * fix: backfill walkthrough launcher after websocket connect * fix: improve walkthrough requests and editor anchors * fix: clear walkthrough highlights on close * feat: add walkthrough discard and prompt customization * fix: send walkthrough requests through saved prompt * fix: support postgres built-in prompt seeding * fix: address walkthrough review feedback * fix: address walkthrough review follow-ups * fix: harden walkthrough lifecycle races * fix: reuse walkthrough notification cleanup * fix: guard stale walkthrough file opens * fix: abort stale file opens before paint * fix: stabilize github settings e2e readiness * fix: use stable github settings e2e selector
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
export type WalkthroughTargetFile = {
|
|
path: string;
|
|
repository_name?: string;
|
|
};
|
|
|
|
export type WalkthroughTargetStep = {
|
|
file: string;
|
|
repo?: string;
|
|
};
|
|
|
|
function normalizePath(path: string): string[] {
|
|
return path.trim().replaceAll("\\", "/").replace(/^\.\//, "").split("/").filter(Boolean);
|
|
}
|
|
|
|
function normalizeRepo(repo: string | undefined): string {
|
|
return (repo ?? "").trim();
|
|
}
|
|
|
|
function segmentsMatchSuffix(left: string[], right: string[]): boolean {
|
|
if (left.length === 0 || right.length === 0) return false;
|
|
const shorter = left.length <= right.length ? left : right;
|
|
const longer = left.length <= right.length ? right : left;
|
|
const offset = longer.length - shorter.length;
|
|
return shorter.every((segment, index) => longer[offset + index] === segment);
|
|
}
|
|
|
|
/**
|
|
* Best-effort match between a walkthrough step's `file` and a diff/review file
|
|
* path. Exact match wins; suffix matching is allowed only on path-segment
|
|
* boundaries so `bar.ts` cannot match `foobar.ts`.
|
|
*/
|
|
export function walkthroughFileMatches(diffPath: string, stepFile: string): boolean {
|
|
const diffSegments = normalizePath(diffPath);
|
|
const stepSegments = normalizePath(stepFile);
|
|
return segmentsMatchSuffix(diffSegments, stepSegments);
|
|
}
|
|
|
|
function repositoryMatches(file: WalkthroughTargetFile, step: WalkthroughTargetStep): boolean {
|
|
const stepRepo = normalizeRepo(step.repo);
|
|
const fileRepo = normalizeRepo(file.repository_name);
|
|
if (!stepRepo) return !fileRepo;
|
|
if (!fileRepo) return false;
|
|
return fileRepo === stepRepo;
|
|
}
|
|
|
|
function isRepoAmbiguous(
|
|
file: WalkthroughTargetFile,
|
|
step: WalkthroughTargetStep,
|
|
allFiles: WalkthroughTargetFile[] | undefined,
|
|
): boolean {
|
|
if (step.repo || !allFiles || !file.repository_name) return false;
|
|
const repos = new Set(
|
|
allFiles
|
|
.filter((candidate) => walkthroughFileMatches(candidate.path, step.file))
|
|
.map((candidate) => normalizeRepo(candidate.repository_name))
|
|
.filter(Boolean),
|
|
);
|
|
return repos.size > 1;
|
|
}
|
|
|
|
export function walkthroughStepMatchesFile(
|
|
file: WalkthroughTargetFile,
|
|
step: WalkthroughTargetStep,
|
|
allFiles?: WalkthroughTargetFile[],
|
|
): boolean {
|
|
if (!repositoryMatches(file, step)) return false;
|
|
if (!walkthroughFileMatches(file.path, step.file)) return false;
|
|
if (isRepoAmbiguous(file, step, allFiles)) return false;
|
|
return true;
|
|
}
|