const view = document.getElementById("view");
const breadcrumbs = document.getElementById("breadcrumbs");
const repositoryForm = document.getElementById("repository-form");
const repositoryInput = document.getElementById("repository-input");
const repositoryCombobox = document.getElementById("repository-combobox");
const repositoryMenuButton = document.getElementById("repository-menu-button");
const repositoryPanel = document.getElementById("repository-panel");
const repositoryResults = document.getElementById("repository-results");
const accountButton = document.getElementById("account-button");
const accountAvatar = document.getElementById("account-avatar");
const accountFallback = document.getElementById("account-fallback");
const cacheLabel = document.getElementById("cache-label");
const toastRegion = document.getElementById("toast-region");
const capabilityToken =
document.querySelector('meta[name="pr-artifact-explorer-token"]')?.content ?? "";
let bootstrapState = null;
let currentArtifact = null;
let currentPullContext = null;
let currentPlayer = null;
let currentPlayerIsPlaying = false;
let artifactFilters = {
context: null,
query: "",
status: "all",
run: "latest",
};
const artifactDownloadProgress = new Map();
const LARGE_ARTIFACT_PROGRESS_BYTES = 2 * 1024 * 1024;
const MAX_VISIBLE_ARTIFACT_FILES = 1_000;
const EXPAND_ALL_FILE_TREE_LIMIT = 200;
const PULL_PAYLOAD_CACHE_FRESH_MS = 30 * 1_000;
const PULL_PAYLOAD_CACHE_STALE_MS = 10 * 60 * 1_000;
const PULL_PAYLOAD_CACHE_LIMIT = 30;
const PULL_QUERY_QUALIFIERS = [
{
name: "in",
description: "Search within a field",
values: [
["title", "Pull request titles"],
["body", "Pull request descriptions"],
["comments", "Pull request comments"],
],
},
{
name: "is",
description: "Filter by state or type",
values: [
["open", "Open pull requests"],
["closed", "Closed pull requests"],
["merged", "Merged pull requests"],
["unmerged", "Pull requests that are not merged"],
["draft", "Draft pull requests"],
["pr", "Pull requests only"],
],
},
{
name: "author",
description: "Opened by a user",
dynamicValues: "authors",
},
{
name: "assignee",
description: "Assigned to a user",
dynamicValues: "authors",
},
{
name: "label",
description: "Has a label",
dynamicValues: "labels",
},
{
name: "review",
description: "Filter by review state",
values: [
["none", "No reviews"],
["required", "Review required"],
["approved", "Approved"],
["changes_requested", "Changes requested"],
],
},
{
name: "reviewed-by",
description: "Reviewed by a user",
dynamicValues: "authors",
},
{
name: "review-requested",
description: "Review requested from a user",
dynamicValues: "authors",
},
{
name: "draft",
description: "Filter by draft state",
values: [
["true", "Draft pull requests"],
["false", "Ready for review"],
],
},
{
name: "status",
description: "Filter by commit status",
values: [
["success", "Checks succeeded"],
["failure", "Checks failed"],
["pending", "Checks are pending"],
],
},
{
name: "no",
description: "Exclude missing metadata",
values: [
["label", "No labels"],
["assignee", "No assignee"],
["milestone", "No milestone"],
["project", "No project"],
],
},
{
name: "sort",
description: "Choose result order",
values: [
["updated-desc", "Recently updated first"],
["updated-asc", "Least recently updated first"],
["created-desc", "Newest first"],
["created-asc", "Oldest first"],
["comments-desc", "Most commented first"],
["comments-asc", "Least commented first"],
],
},
{ name: "base", description: "Targets a base branch" },
{ name: "head", description: "Comes from a head branch" },
{ name: "milestone", description: "Belongs to a milestone" },
{ name: "mentions", description: "Mentions a user" },
{ name: "commenter", description: "Commented on by a user" },
{ name: "involves", description: "Involves a user" },
{ name: "created", description: "Created on a date or range" },
{ name: "updated", description: "Updated on a date or range" },
{ name: "closed", description: "Closed on a date or range" },
{ name: "merged", description: "Merged on a date or range" },
];
const MANIFEST_FILE_NAMES = new Set([
"bun.lock",
"bun.lockb",
"cargo.lock",
"cargo.toml",
"composer.json",
"composer.lock",
"compose.yaml",
"compose.yml",
"deno.json",
"deno.jsonc",
"directory.build.props",
"directory.build.targets",
"dockerfile",
"gemfile",
"gemfile.lock",
"global.json",
"go.mod",
"go.sum",
"mix.exs",
"mix.lock",
"npm-shrinkwrap.json",
"nuget.config",
"package-lock.json",
"package.json",
"pipfile",
"pipfile.lock",
"pnpm-lock.yaml",
"poetry.lock",
"pubspec.yaml",
"pyproject.toml",
"yarn.lock",
]);
const VENDORED_PATH_SEGMENTS = new Set([
".pnpm",
".venv",
"__pycache__",
"bower_components",
"node_modules",
"site-packages",
"third-party",
"third_party",
"vendor",
"vendors",
]);
const FILE_PATH_COLLATOR = new Intl.Collator("en", {
numeric: true,
sensitivity: "base",
});
const pullPayloadCache = new Map();
let renderSequence = 0;
let repositorySearchTimer = null;
let repositorySearchController = null;
let repositorySearchSequence = 0;
let repositoryRemoteResults = [];
let repositorySearchState = "idle";
let repositorySearchError = null;
let repositoryActiveIndex = -1;
let repositoryPickerQuery = "";
let pullAuthors = [];
let pullAuthorsRepository = null;
let pullAuthorsState = "idle";
let pullAuthorsError = null;
let pullAuthorsLoadSequence = 0;
let pullQueryLabels = [];
let pullQuerySuggestionIndex = -1;
let pullQueryPendingCommit = null;
let pullTableRefreshSequence = 0;
let pullFilterStorageWarningShown = false;
const PULL_FILTER_STORAGE_KEY = "pr-artifact-explorer:pull-filters:v1";
const MAX_LOCAL_PULL_FILTER_REPOSITORIES = 50;
const previewThemeMedia = window.matchMedia("(prefers-color-scheme: dark)");
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function titleHtml(value) {
const source = String(value ?? "");
let result = "";
let cursor = 0;
while (cursor < source.length) {
const opening = source.indexOf("`", cursor);
if (opening === -1) {
result += escapeHtml(source.slice(cursor));
break;
}
result += escapeHtml(source.slice(cursor, opening));
let runLength = 1;
while (source[opening + runLength] === "`") runLength++;
const delimiter = "`".repeat(runLength);
let closing = source.indexOf(delimiter, opening + runLength);
while (
closing !== -1 &&
(source[closing - 1] === "`" || source[closing + runLength] === "`")
) {
closing = source.indexOf(delimiter, closing + runLength);
}
if (closing === -1) {
result += escapeHtml(source.slice(opening));
break;
}
let code = source.slice(opening + runLength, closing).replace(/\s+/g, " ");
if (code.startsWith(" ") && code.endsWith(" ") && code.trim()) {
code = code.slice(1, -1);
}
result += `${escapeHtml(code)}`;
cursor = closing + runLength;
}
return result;
}
function highlightXmlTag(token) {
const opening = token.startsWith("") ? "" : "<";
const ending = token.endsWith("/>") ? "/>" : ">";
const nameStart = opening.length;
const name = token.slice(nameStart).match(/^[^\s/>]+/)?.[0];
if (!name) {
return `${escapeHtml(token)}`;
}
const attributesStart = nameStart + name.length;
const attributesEnd = token.length - ending.length;
const attributes = token.slice(attributesStart, attributesEnd);
const matcher = /([^\s=/>]+)(\s*=\s*)("(?:[^"]*)"|'(?:[^']*)'|[^\s>]+)/g;
let highlightedAttributes = "";
let cursor = 0;
for (const match of attributes.matchAll(matcher)) {
highlightedAttributes += escapeHtml(attributes.slice(cursor, match.index));
highlightedAttributes += `${escapeHtml(match[1])}`;
highlightedAttributes += `${escapeHtml(match[2])}`;
highlightedAttributes += `${escapeHtml(match[3])}`;
cursor = match.index + match[0].length;
}
highlightedAttributes += escapeHtml(attributes.slice(cursor));
return [
`${escapeHtml(opening)}`,
`${escapeHtml(name)}`,
highlightedAttributes,
`${escapeHtml(ending)}`,
].join("");
}
function highlightXml(value) {
const source = String(value ?? "");
let highlighted = "";
let cursor = 0;
while (cursor < source.length) {
const opening = source.indexOf("<", cursor);
if (opening === -1) {
highlighted += escapeHtml(source.slice(cursor));
break;
}
highlighted += escapeHtml(source.slice(cursor, opening));
let closing;
if (source.startsWith("", opening + 4);
closing = index === -1 ? source.length : index + 3;
} else if (source.startsWith("", opening + 9);
closing = index === -1 ? source.length : index + 3;
} else {
let quote = null;
let subsetDepth = 0;
let foundClosing = false;
closing = source.length;
for (let index = opening + 1; index < source.length; index++) {
const character = source[index];
if (quote) {
if (character === quote) quote = null;
continue;
}
if (character === '"' || character === "'") {
quote = character;
} else if (character === "[") {
subsetDepth++;
} else if (character === "]" && subsetDepth > 0) {
subsetDepth--;
} else if (character === ">" && subsetDepth === 0) {
closing = index + 1;
foundClosing = true;
break;
}
}
if (!foundClosing) {
highlighted += escapeHtml(source.slice(opening));
break;
}
}
const token = source.slice(opening, closing);
if (token.startsWith("