// Frontend 1.53.10 only. No Vue component closures or installed bundles are // modified. The private Pinia registry and DOM assumptions are kept here. export function inspectNativeQueue(app, version) { const stores = app.extensionManager?._p?._s; const execution = stores?.get("execution"); const previews = stores?.get("jobPreview"); const queue = stores?.get("queue"); const settings = app.extensionManager?.queueSettings; const problems = []; if (version !== "1.53.10") problems.push(`Frontend ${version} is unsupported; expected 1.53.10.`); if (!Array.isArray(app.queueItems) || !Number.isInteger(app.nextQueueRequestId)) problems.push("Native request association is unavailable."); if (!execution?.nodeProgressStatesByJob || typeof previews?.setPreviewUrl !== "function" || typeof queue?.update !== "function") { problems.push("Native per-job state stores are unavailable."); } if (!settings || !document.querySelector('[data-testid="queue-mode-menu-trigger"]')) problems.push("Native Run controls are unavailable."); return {problems, execution, previews, queue, settings}; } export function captureDeadlineDestination(selected, mode, settings, readiness) { if (!selected || mode !== "disabled") return null; if (!readiness.ready) throw new Error(readiness.message || "Deadline is not ready. Open the Deadline sidebar."); return {settings: structuredClone(settings)}; } export function installNativeControls({app, state, progress, getDestination, getReadiness, openSetup, selectDestination, retry, stop}) { let frame; let fallback; let disposed = false; let menuJobId; const positions = new Map(); const cancelledLabels = new Map(); const stopping = new Set(); const queueButton = () => document.querySelector('[data-testid="queue-button"]'); const trigger = () => document.querySelector('[data-testid="queue-mode-menu-trigger"]'); const runMenu = () => { const id = trigger()?.id; return id ? document.querySelector(`[role="menu"][aria-labelledby="${CSS.escape(id)}"]`) : null; }; const closeRunMenu = () => { trigger()?.click(); trigger()?.focus(); }; const schedule = () => { if (disposed || frame || fallback) return; frame = requestAnimationFrame(render); // Webviews can suspend animation frames while their page remains // active. Menu actions still need to mount in that state. fallback = setTimeout(render, 100); }; function render() { cancelAnimationFrame(frame); clearTimeout(fallback); frame = undefined; fallback = undefined; if (disposed) return; const farm = getDestination() !== null; const readiness = getReadiness(); const button = queueButton(); if (button) { const text = [...button.childNodes].find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim()); if (farm && text && text.textContent.trim() !== "Run (Deadline)") text.textContent = " Run (Deadline)"; if (farm && button.getAttribute("aria-label") !== "Run (Deadline)") button.setAttribute("aria-label", "Run (Deadline)"); if (!farm && text?.textContent.includes("Deadline")) text.textContent = " Run"; if (!farm && button.getAttribute("aria-label") === "Run (Deadline)") button.removeAttribute("aria-label"); } const menu = runMenu(); if (menu && !menu.querySelector("[data-deadline-mode]")) { const item = document.createElement("button"); item.type = "button"; item.role = "menuitem"; item.tabIndex = -1; item.dataset.deadlineMode = "farm"; item.className = menu.querySelector("button")?.className || ""; item.textContent = "Run (Deadline)"; item.title = "Whole workflows; finite native batch count. Deadline priority governs ordering, including Shift+Run."; item.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); if (!getReadiness().ready) return; state.settings.mode = "disabled"; selectDestination(true); closeRunMenu(); schedule(); }); menu.append(item); } if (menu) { const item = menu.querySelector("[data-deadline-mode]"); item.disabled = !readiness.ready; item.title = readiness.ready ? "Whole workflows; finite native batch count. Deadline priority governs ordering, including Shift+Run." : readiness.message; item.setAttribute("aria-disabled", String(!readiness.ready)); let setup = menu.querySelector("[data-deadline-setup]"); if (!readiness.ready && openSetup && !setup) { setup = document.createElement("button"); setup.type = "button"; setup.role = "menuitem"; setup.tabIndex = -1; setup.dataset.deadlineSetup = ""; setup.className = item.className; setup.textContent = "Open Deadline"; setup.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); closeRunMenu(); openSetup(); }); menu.append(setup); } if (readiness.ready) setup?.remove(); } for (const row of document.querySelectorAll("[data-job-id]")) { const event = progress.runs.get(row.dataset.jobId); const active = event && ["pending", "in_progress"].includes(event.status); let stopButton = row.querySelector("[data-deadline-stop]"); if (active && stop && !stopButton) { stopButton = document.createElement("button"); stopButton.type = "button"; stopButton.dataset.deadlineStop = row.dataset.jobId; stopButton.style.cssText = "position:relative;z-index:1;flex-shrink:0;font-size:11px;line-height:22px;padding:0 7px;background:var(--bg-color,#222);border:1px solid var(--border-color,#555);border-radius:4px;cursor:pointer"; stopButton.addEventListener("click", async click => { click.preventDefault(); click.stopPropagation(); const id = stopButton.dataset.deadlineStop; if (stopping.has(id)) return; stopping.add(id); schedule(); try { await stop(id); } catch (error) { app.extensionManager.toast.addAlert(`Could not stop Deadline run: ${error.message}`); } finally { stopping.delete(id); schedule(); } }); (row.firstElementChild || row).append(stopButton); } if (!active) stopButton?.remove(); else if (stopButton) { stopButton.dataset.deadlineStop = row.dataset.jobId; const requested = event.cancel_requested || stopping.has(row.dataset.jobId); stopButton.disabled = !!requested; const label = requested ? "Stopping…" : "Stop"; if (stopButton.textContent !== label) stopButton.textContent = label; stopButton.setAttribute("aria-label", requested ? "Stopping Deadline run" : "Stop Deadline run"); stopButton.title = requested ? "Waiting for the Deadline task to stop" : "Stop this run on Deadline"; } // This frontend groups cancelled runs in its Failed tab. Keep its // API status intact and distinguish farm Stop in the native row. for (const label of row.querySelectorAll('span.truncate[title]')) { if (event?.status === "cancelled" && label.getAttribute("title") === "Failed") { cancelledLabels.set(label, {text: label.textContent, title: label.title, run: row.dataset.jobId}); label.textContent = "Cancelled"; label.title = "Cancelled"; } else if (cancelledLabels.has(label) && (event?.status !== "cancelled" || cancelledLabels.get(label).run !== row.dataset.jobId)) { const prior = cancelledLabels.get(label); if (label.title === "Cancelled") { label.textContent = prior.text; label.title = prior.title; } cancelledLabels.delete(label); } } let badge = row.querySelector("[data-deadline-progress]"); if (!event || event.status !== "in_progress") { badge?.remove(); continue; } if (!badge) { badge = document.createElement("span"); badge.dataset.deadlineProgress = row.dataset.jobId; badge.style.cssText = "position:absolute;right:44px;bottom:0;font-size:10px;line-height:12px;background:var(--bg-color,#222);padding:0 3px;pointer-events:none"; if (!positions.has(row)) positions.set(row, row.style.position); row.style.position = "relative"; row.append(badge); } const nodes = Object.values(event.nodes || {}); const max = nodes.reduce((sum, node) => sum + node.max, 0); const value = nodes.reduce((sum, node) => sum + node.value, 0); const label = event.cancel_requested ? "Deadline stopping" : max > 0 ? `Deadline ${Math.round(100 * value / max)}%` : "Deadline initializing"; if (badge.textContent !== label) badge.textContent = label; } for (const row of positions.keys()) if (!row.isConnected) positions.delete(row); for (const label of cancelledLabels.keys()) if (!label.isConnected) cancelledLabels.delete(label); // Copy-ID is a stable, inspected native context-menu action on this // supported English frontend. Unsupported menu layouts get no injection. for (const anchor of document.querySelectorAll('button[aria-label="Copy job ID"]')) { const menu = anchor.parentElement; const event = progress.runs.get(menuJobId); const terminal = event && ["completed", "failed", "cancelled"].includes(event.status); let action = menu.querySelector("[data-deadline-retry]"); if (!terminal) { action?.remove(); continue; } if (action?.dataset.deadlineRetry === menuJobId) continue; action?.remove(); action = document.createElement("button"); action.type = "button"; action.dataset.deadlineRetry = menuJobId; action.setAttribute("aria-label", "Retry on Deadline"); action.className = anchor.className; action.textContent = "Retry on Deadline"; const id = menuJobId; action.addEventListener("click", async event => { event.stopPropagation(); action.disabled = true; try { await retry(id); } finally { action.disabled = false; } }); menu.append(action); } } function captureClick(event) { const row = event.target.closest?.("[data-job-id]"); if (row) menuJobId = row.dataset.jobId; const menu = runMenu(); if (menu?.contains(event.target) && !event.target.closest("[data-deadline-mode],[data-deadline-setup]")) { selectDestination(false); const button = queueButton(); const text = [...(button?.childNodes || [])].find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim()); if (text?.textContent.includes("Deadline")) text.textContent = " Run"; button?.removeAttribute("aria-label"); schedule(); } } function keydown(event) { const menu = runMenu(); if (!menu?.contains(event.target)) return; const items = [...menu.querySelectorAll('[role="menuitem"]')].filter(item => !item.disabled); const index = items.indexOf(event.target); const extensionItem = item => item?.matches("[data-deadline-mode],[data-deadline-setup]"); if (index >= 0 && ["ArrowDown", "ArrowUp"].includes(event.key)) { const next = items[(index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length]; if (extensionItem(event.target) || extensionItem(next)) { event.preventDefault(); event.stopImmediatePropagation(); next.focus(); } } else if (extensionItem(event.target) && ["Enter", " "].includes(event.key)) { event.preventDefault(); event.stopImmediatePropagation(); event.target.click(); } } const observer = new MutationObserver(schedule); observer.observe(document.body, {childList: true, subtree: true, characterData: true}); document.addEventListener("click", captureClick, true); document.addEventListener("contextmenu", captureClick, true); document.addEventListener("keydown", keydown, true); schedule(); return { render: schedule, dispose() { disposed = true; observer.disconnect(); cancelAnimationFrame(frame); clearTimeout(fallback); document.removeEventListener("click", captureClick, true); document.removeEventListener("contextmenu", captureClick, true); document.removeEventListener("keydown", keydown, true); for (const element of document.querySelectorAll("[data-deadline-mode],[data-deadline-setup],[data-deadline-progress],[data-deadline-retry],[data-deadline-stop]")) element.remove(); for (const [row, position] of positions) row.style.position = position; for (const [label, prior] of cancelledLabels) { if (label.title === "Cancelled") { label.textContent = prior.text; label.title = prior.title; } } const button = queueButton(); const text = [...(button?.childNodes || [])].find(node => node.nodeType === Node.TEXT_NODE && node.textContent.includes("Deadline")); if (text) text.textContent = " Run"; button?.removeAttribute("aria-label"); } }; }