mirror of
https://github.com/actions/runner.git
synced 2026-01-23 13:01:14 +08:00
978 lines
29 KiB
JavaScript
978 lines
29 KiB
JavaScript
/**
|
|
* Content Script - Debugger UI
|
|
*
|
|
* Injects the debugger pane into GitHub Actions job pages and handles
|
|
* all UI interactions. Supports two layout modes: sidebar and bottom panel.
|
|
*/
|
|
|
|
// State
|
|
let debuggerPane = null;
|
|
let currentFrameId = 0;
|
|
let isConnected = false;
|
|
let replHistory = [];
|
|
let replHistoryIndex = -1;
|
|
let currentLayout = 'bottom'; // 'bottom' | 'sidebar'
|
|
let currentStepElement = null; // Track current step for breakpoint indicator
|
|
|
|
// Layout constants
|
|
const BOTTOM_PANEL_HEIGHT = 350;
|
|
const SIDEBAR_WIDTH = 350;
|
|
|
|
// HTML escape helper
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Strip result indicator suffix from step name
|
|
* e.g., "Run tests [running]" -> "Run tests"
|
|
*/
|
|
function stripResultIndicator(name) {
|
|
return name.replace(/\s*\[(running|success|failure|skipped|cancelled)\]$/i, '');
|
|
}
|
|
|
|
/**
|
|
* Load layout preference from storage
|
|
*/
|
|
function loadLayoutPreference() {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(['debuggerLayout'], (data) => {
|
|
currentLayout = data.debuggerLayout || 'bottom';
|
|
resolve(currentLayout);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save layout preference to storage
|
|
*/
|
|
function saveLayoutPreference(layout) {
|
|
currentLayout = layout;
|
|
chrome.storage.local.set({ debuggerLayout: layout });
|
|
}
|
|
|
|
/**
|
|
* Send DAP request to background script
|
|
*/
|
|
function sendDapRequest(command, args = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
chrome.runtime.sendMessage({ type: 'dap-request', command, args }, (response) => {
|
|
if (chrome.runtime.lastError) {
|
|
reject(new Error(chrome.runtime.lastError.message));
|
|
} else if (response && response.success) {
|
|
resolve(response.body);
|
|
} else {
|
|
reject(new Error(response?.error || 'Unknown error'));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build map of steps from DOM
|
|
*/
|
|
function buildStepMap() {
|
|
const steps = document.querySelectorAll('check-step');
|
|
const map = new Map();
|
|
steps.forEach((el, idx) => {
|
|
map.set(idx, {
|
|
element: el,
|
|
number: parseInt(el.dataset.number),
|
|
name: el.dataset.name,
|
|
conclusion: el.dataset.conclusion,
|
|
externalId: el.dataset.externalId,
|
|
});
|
|
});
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Find step element by name
|
|
*/
|
|
function findStepByName(stepName) {
|
|
return document.querySelector(`check-step[data-name="${CSS.escape(stepName)}"]`);
|
|
}
|
|
|
|
/**
|
|
* Find step element by number
|
|
*/
|
|
function findStepByNumber(stepNumber) {
|
|
return document.querySelector(`check-step[data-number="${stepNumber}"]`);
|
|
}
|
|
|
|
/**
|
|
* Get all step elements
|
|
*/
|
|
function getAllSteps() {
|
|
return document.querySelectorAll('check-step');
|
|
}
|
|
|
|
/**
|
|
* SVG Icons
|
|
*/
|
|
const Icons = {
|
|
bug: `<svg class="octicon" viewBox="0 0 16 16" width="16" height="16">
|
|
<path fill="currentColor" d="M4.72.22a.75.75 0 0 1 1.06 0l1 1a.75.75 0 0 1-1.06 1.06l-.22-.22-.22.22a.75.75 0 0 1-1.06-1.06l1-1Z"/>
|
|
<path fill="currentColor" d="M11.28.22a.75.75 0 0 0-1.06 0l-1 1a.75.75 0 0 0 1.06 1.06l.22-.22.22.22a.75.75 0 0 0 1.06-1.06l-1-1Z"/>
|
|
<path fill="currentColor" d="M8 4a4 4 0 0 0-4 4v1h1v2.5a2.5 2.5 0 0 0 2.5 2.5h1a2.5 2.5 0 0 0 2.5-2.5V9h1V8a4 4 0 0 0-4-4Z"/>
|
|
<path fill="currentColor" d="M5 9H3.5a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 .5.5H5V9ZM11 9h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H11V9Z"/>
|
|
</svg>`,
|
|
close: `<svg viewBox="0 0 16 16" width="16" height="16">
|
|
<path fill="currentColor" d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"/>
|
|
</svg>`,
|
|
layoutBottom: `<svg viewBox="0 0 16 16" width="16" height="16">
|
|
<path fill="currentColor" d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75C0 1.784.784 1 1.75 1Zm0 1.5a.25.25 0 0 0-.25.25v6h13v-6a.25.25 0 0 0-.25-.25H1.75Zm-.25 10.75c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-3h-13v3Z"/>
|
|
</svg>`,
|
|
layoutSidebar: `<svg viewBox="0 0 16 16" width="16" height="16">
|
|
<path fill="currentColor" d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75C0 1.784.784 1 1.75 1ZM1.5 2.75v10.5c0 .138.112.25.25.25h8.5V2.5h-8.5a.25.25 0 0 0-.25.25Zm10.25 10.75h2.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25h-2.5v11Z"/>
|
|
</svg>`,
|
|
reverseContinue: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M2 2v12h2V8.5l5 4V8.5l5 4V2.5l-5 4V2.5l-5 4V2z"/></svg>`,
|
|
stepBack: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M2 2v12h2V2H2zm3 6 7 5V3L5 8z"/></svg>`,
|
|
continue: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M4 2l10 6-10 6z"/></svg>`,
|
|
stepForward: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M12 2v12h2V2h-2zM2 3l7 5-7 5V3z"/></svg>`,
|
|
};
|
|
|
|
/**
|
|
* Create control buttons HTML
|
|
*/
|
|
function createControlButtonsHTML(compact = false) {
|
|
const btnClass = compact ? 'btn-sm' : 'btn-sm';
|
|
return `
|
|
<button class="btn ${btnClass} dap-control-btn" data-action="reverseContinue" title="Reverse Continue (go to first checkpoint)" disabled>
|
|
${Icons.reverseContinue}
|
|
</button>
|
|
<button class="btn ${btnClass} dap-control-btn" data-action="stepBack" title="Step Back" disabled>
|
|
${Icons.stepBack}
|
|
</button>
|
|
<button class="btn ${btnClass} btn-primary dap-control-btn" data-action="continue" title="Continue" disabled>
|
|
${Icons.continue}
|
|
</button>
|
|
<button class="btn ${btnClass} dap-control-btn" data-action="next" title="Step to Next" disabled>
|
|
${Icons.stepForward}
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Create the bottom panel HTML structure
|
|
*/
|
|
function createBottomPaneHTML() {
|
|
return `
|
|
<div class="dap-header d-flex flex-items-center p-2 border-bottom">
|
|
<span class="text-bold">Debugger</span>
|
|
<span class="dap-step-info color-fg-muted ml-2">Connecting...</span>
|
|
|
|
<div class="dap-header-right ml-auto d-flex flex-items-center">
|
|
<div class="dap-controls d-flex flex-items-center mr-3">
|
|
${createControlButtonsHTML(true)}
|
|
</div>
|
|
|
|
<div class="dap-layout-toggles d-flex flex-items-center mr-2">
|
|
<button class="btn btn-sm dap-layout-btn" data-layout="sidebar" title="Sidebar layout">
|
|
${Icons.layoutSidebar}
|
|
</button>
|
|
<button class="btn btn-sm dap-layout-btn active" data-layout="bottom" title="Bottom panel layout">
|
|
${Icons.layoutBottom}
|
|
</button>
|
|
</div>
|
|
|
|
<button class="btn btn-sm dap-close-btn" title="Close debugger">
|
|
${Icons.close}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dap-content d-flex">
|
|
<!-- Scopes Panel -->
|
|
<div class="dap-scopes border-right overflow-auto">
|
|
<div class="dap-scope-header p-2 text-bold border-bottom d-flex flex-items-center">
|
|
<span>Variables</span>
|
|
</div>
|
|
<div class="dap-scope-tree p-2">
|
|
<div class="color-fg-muted">Connect to view variables</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- REPL Console -->
|
|
<div class="dap-repl d-flex flex-column">
|
|
<div class="dap-repl-header p-2 text-bold border-bottom">Console</div>
|
|
<div class="dap-repl-output overflow-auto flex-auto p-2 text-mono text-small">
|
|
<div class="color-fg-muted">Welcome to Actions DAP Debugger</div>
|
|
<div class="color-fg-muted">Enter expressions like: \${{ github.ref }}</div>
|
|
<div class="color-fg-muted">Or shell commands: !ls -la</div>
|
|
</div>
|
|
<div class="dap-repl-input border-top p-2">
|
|
<input type="text" class="form-control input-sm text-mono"
|
|
placeholder="Enter expression or !command" disabled>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Create the sidebar panel HTML structure
|
|
*/
|
|
function createSidebarPaneHTML() {
|
|
return `
|
|
<div class="dap-header d-flex flex-items-center p-2 border-bottom">
|
|
<span class="text-bold">Debugger</span>
|
|
|
|
<div class="dap-header-right ml-auto d-flex flex-items-center">
|
|
<div class="dap-layout-toggles d-flex flex-items-center mr-2">
|
|
<button class="btn btn-sm dap-layout-btn active" data-layout="sidebar" title="Sidebar layout">
|
|
${Icons.layoutSidebar}
|
|
</button>
|
|
<button class="btn btn-sm dap-layout-btn" data-layout="bottom" title="Bottom panel layout">
|
|
${Icons.layoutBottom}
|
|
</button>
|
|
</div>
|
|
|
|
<button class="btn btn-sm dap-close-btn" title="Close debugger">
|
|
${Icons.close}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scopes Panel -->
|
|
<div class="dap-scopes overflow-auto border-bottom">
|
|
<div class="dap-scope-header p-2 text-bold border-bottom d-flex flex-items-center">
|
|
<span>Variables</span>
|
|
</div>
|
|
<div class="dap-scope-tree p-2">
|
|
<div class="color-fg-muted">Connect to view variables</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- REPL Console -->
|
|
<div class="dap-repl d-flex flex-column">
|
|
<div class="dap-repl-header p-2 text-bold border-bottom">Console</div>
|
|
<div class="dap-repl-output overflow-auto flex-auto p-2 text-mono text-small">
|
|
<div class="color-fg-muted">Welcome to Actions DAP Debugger</div>
|
|
<div class="color-fg-muted">Enter expressions like: \${{ github.ref }}</div>
|
|
<div class="color-fg-muted">Or shell commands: !ls -la</div>
|
|
</div>
|
|
<div class="dap-repl-input border-top p-2">
|
|
<input type="text" class="form-control input-sm text-mono"
|
|
placeholder="Enter expression or !command" disabled>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Control buttons -->
|
|
<div class="dap-controls d-flex flex-items-center justify-content-center p-2 border-top">
|
|
${createControlButtonsHTML()}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Inject debugger pane into the page
|
|
*/
|
|
function injectDebuggerPane(layout = null) {
|
|
// Remove existing pane if any
|
|
const existing = document.querySelector('.dap-debugger-pane');
|
|
if (existing) existing.remove();
|
|
|
|
const targetLayout = layout || currentLayout;
|
|
|
|
// Create pane
|
|
const pane = document.createElement('div');
|
|
pane.className = `dap-debugger-pane dap-debugger-${targetLayout}`;
|
|
|
|
if (targetLayout === 'bottom') {
|
|
pane.innerHTML = createBottomPaneHTML();
|
|
} else {
|
|
pane.innerHTML = createSidebarPaneHTML();
|
|
}
|
|
|
|
// Append to body for fixed positioning
|
|
document.body.appendChild(pane);
|
|
|
|
// Add body padding class for bottom layout to ensure content is scrollable
|
|
if (targetLayout === 'bottom') {
|
|
document.body.classList.add('dap-bottom-panel-active');
|
|
} else {
|
|
document.body.classList.remove('dap-bottom-panel-active');
|
|
}
|
|
|
|
// Setup event handlers
|
|
setupPaneEventHandlers(pane);
|
|
|
|
debuggerPane = pane;
|
|
currentLayout = targetLayout;
|
|
|
|
// Update layout toggle button states
|
|
updateLayoutToggleButtons();
|
|
|
|
return pane;
|
|
}
|
|
|
|
/**
|
|
* Switch between layouts
|
|
*/
|
|
function switchLayout(newLayout) {
|
|
if (newLayout === currentLayout) return;
|
|
|
|
// Store current state before switching
|
|
const wasConnected = isConnected;
|
|
const replOutputContent = debuggerPane?.querySelector('.dap-repl-output')?.innerHTML;
|
|
const scopeTreeContent = debuggerPane?.querySelector('.dap-scope-tree')?.innerHTML;
|
|
|
|
// Remove old pane
|
|
if (debuggerPane) {
|
|
debuggerPane.remove();
|
|
debuggerPane = null;
|
|
}
|
|
|
|
// Save preference and create new pane
|
|
saveLayoutPreference(newLayout);
|
|
const pane = injectDebuggerPane(newLayout);
|
|
|
|
// Update body padding class based on new layout
|
|
if (newLayout === 'bottom') {
|
|
document.body.classList.add('dap-bottom-panel-active');
|
|
} else {
|
|
document.body.classList.remove('dap-bottom-panel-active');
|
|
}
|
|
|
|
// Restore state
|
|
if (replOutputContent) {
|
|
const output = pane.querySelector('.dap-repl-output');
|
|
if (output) output.innerHTML = replOutputContent;
|
|
}
|
|
if (scopeTreeContent) {
|
|
const scopeTree = pane.querySelector('.dap-scope-tree');
|
|
if (scopeTree) scopeTree.innerHTML = scopeTreeContent;
|
|
}
|
|
|
|
// Re-enable controls if connected
|
|
if (wasConnected) {
|
|
enableControls(true);
|
|
}
|
|
|
|
// Update debug button state
|
|
const btn = document.querySelector('.dap-debug-btn');
|
|
if (btn) btn.classList.add('selected');
|
|
|
|
// Update layout toggle button states
|
|
updateLayoutToggleButtons();
|
|
}
|
|
|
|
/**
|
|
* Update layout toggle button active states
|
|
*/
|
|
function updateLayoutToggleButtons() {
|
|
if (!debuggerPane) return;
|
|
|
|
debuggerPane.querySelectorAll('.dap-layout-btn').forEach((btn) => {
|
|
const layout = btn.dataset.layout;
|
|
btn.classList.toggle('active', layout === currentLayout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Close the debugger pane
|
|
*/
|
|
function closeDebuggerPane() {
|
|
// Clear breakpoint indicator
|
|
clearBreakpointIndicator();
|
|
|
|
// Remove body padding class
|
|
document.body.classList.remove('dap-bottom-panel-active');
|
|
|
|
if (debuggerPane) {
|
|
debuggerPane.remove();
|
|
debuggerPane = null;
|
|
}
|
|
|
|
// Update debug button state
|
|
const btn = document.querySelector('.dap-debug-btn');
|
|
if (btn) btn.classList.remove('selected');
|
|
}
|
|
|
|
/**
|
|
* Setup event handlers for debugger pane
|
|
*/
|
|
function setupPaneEventHandlers(pane) {
|
|
// Control buttons
|
|
pane.querySelectorAll('[data-action]').forEach((btn) => {
|
|
btn.addEventListener('click', async () => {
|
|
const action = btn.dataset.action;
|
|
enableControls(false);
|
|
updateStatus('RUNNING');
|
|
|
|
try {
|
|
await sendDapRequest(action, { threadId: 1 });
|
|
} catch (error) {
|
|
console.error(`[Content] DAP ${action} failed:`, error);
|
|
appendOutput(`Error: ${error.message}`, 'error');
|
|
enableControls(true);
|
|
updateStatus('ERROR');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Layout toggle buttons
|
|
pane.querySelectorAll('.dap-layout-btn').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const layout = btn.dataset.layout;
|
|
if (layout && layout !== currentLayout) {
|
|
switchLayout(layout);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close button
|
|
const closeBtn = pane.querySelector('.dap-close-btn');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', closeDebuggerPane);
|
|
}
|
|
|
|
// REPL input
|
|
const input = pane.querySelector('.dap-repl-input input');
|
|
if (input) {
|
|
input.addEventListener('keydown', handleReplKeydown);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle REPL input keydown
|
|
*/
|
|
async function handleReplKeydown(e) {
|
|
const input = e.target;
|
|
|
|
if (e.key === 'Enter') {
|
|
const command = input.value.trim();
|
|
if (!command) return;
|
|
|
|
replHistory.push(command);
|
|
replHistoryIndex = replHistory.length;
|
|
input.value = '';
|
|
|
|
// Show command
|
|
appendOutput(`> ${command}`, 'input');
|
|
|
|
// Send to DAP
|
|
try {
|
|
const response = await sendDapRequest('evaluate', {
|
|
expression: command,
|
|
frameId: currentFrameId,
|
|
context: command.startsWith('!') ? 'repl' : 'watch',
|
|
});
|
|
// Only show result if it's NOT an exit code summary
|
|
// (shell command output is already streamed via output events)
|
|
if (response.result && !/^\(exit code: -?\d+\)$/.test(response.result)) {
|
|
appendOutput(response.result, 'result');
|
|
}
|
|
} catch (error) {
|
|
appendOutput(error.message, 'error');
|
|
}
|
|
} else if (e.key === 'ArrowUp') {
|
|
if (replHistoryIndex > 0) {
|
|
replHistoryIndex--;
|
|
input.value = replHistory[replHistoryIndex];
|
|
}
|
|
e.preventDefault();
|
|
} else if (e.key === 'ArrowDown') {
|
|
if (replHistoryIndex < replHistory.length - 1) {
|
|
replHistoryIndex++;
|
|
input.value = replHistory[replHistoryIndex];
|
|
} else {
|
|
replHistoryIndex = replHistory.length;
|
|
input.value = '';
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append output to REPL console
|
|
*/
|
|
function appendOutput(text, type) {
|
|
const output = document.querySelector('.dap-repl-output');
|
|
if (!output) return;
|
|
|
|
// Handle multi-line output - each line gets its own div
|
|
const lines = text.split('\n');
|
|
lines.forEach((l) => {
|
|
const div = document.createElement('div');
|
|
div.className = `dap-output-${type}`;
|
|
if (type === 'error') div.classList.add('color-fg-danger');
|
|
if (type === 'input') div.classList.add('color-fg-muted');
|
|
div.textContent = l;
|
|
output.appendChild(div);
|
|
});
|
|
|
|
output.scrollTop = output.scrollHeight;
|
|
}
|
|
|
|
/**
|
|
* Enable/disable control buttons
|
|
*/
|
|
function enableControls(enabled) {
|
|
if (!debuggerPane) return;
|
|
|
|
debuggerPane.querySelectorAll('.dap-control-btn').forEach((btn) => {
|
|
btn.disabled = !enabled;
|
|
});
|
|
|
|
const input = debuggerPane.querySelector('.dap-repl-input input');
|
|
if (input) {
|
|
input.disabled = !enabled;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update status display
|
|
*/
|
|
function updateStatus(status, extra) {
|
|
if (!debuggerPane) return;
|
|
|
|
// Update step info text
|
|
const stepInfo = debuggerPane.querySelector('.dap-step-info');
|
|
if (stepInfo && extra) {
|
|
stepInfo.textContent = extra;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update breakpoint indicator - highlights the current step
|
|
*/
|
|
function updateBreakpointIndicator(stepElement) {
|
|
// Clear previous indicator
|
|
clearBreakpointIndicator();
|
|
|
|
if (!stepElement) return;
|
|
|
|
// Add indicator class to current step
|
|
stepElement.classList.add('dap-current-step');
|
|
currentStepElement = stepElement;
|
|
|
|
// Scroll step into view if needed
|
|
stepElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
/**
|
|
* Clear breakpoint indicator from all steps
|
|
*/
|
|
function clearBreakpointIndicator() {
|
|
if (currentStepElement) {
|
|
currentStepElement.classList.remove('dap-current-step');
|
|
currentStepElement = null;
|
|
}
|
|
// Also clear any other steps that might have the class
|
|
document.querySelectorAll('.dap-current-step').forEach((el) => {
|
|
el.classList.remove('dap-current-step');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load scopes for current frame
|
|
*/
|
|
async function loadScopes(frameId) {
|
|
const scopesContainer = document.querySelector('.dap-scope-tree');
|
|
if (!scopesContainer) return;
|
|
|
|
scopesContainer.innerHTML = '<div class="color-fg-muted">Loading...</div>';
|
|
|
|
try {
|
|
console.log('[Content] Loading scopes for frame:', frameId);
|
|
const response = await sendDapRequest('scopes', { frameId });
|
|
console.log('[Content] Scopes response:', response);
|
|
|
|
scopesContainer.innerHTML = '';
|
|
|
|
if (!response.scopes || response.scopes.length === 0) {
|
|
scopesContainer.innerHTML = '<div class="color-fg-muted">No scopes available</div>';
|
|
return;
|
|
}
|
|
|
|
for (const scope of response.scopes) {
|
|
console.log('[Content] Creating tree node for scope:', scope.name, 'variablesRef:', scope.variablesReference);
|
|
// Only mark as expandable if variablesReference > 0
|
|
const isExpandable = scope.variablesReference > 0;
|
|
const node = createTreeNode(scope.name, scope.variablesReference, isExpandable);
|
|
scopesContainer.appendChild(node);
|
|
}
|
|
} catch (error) {
|
|
console.error('[Content] Failed to load scopes:', error);
|
|
scopesContainer.innerHTML = `<div class="color-fg-danger">Error: ${escapeHtml(error.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a tree node for scope/variable display
|
|
*/
|
|
function createTreeNode(name, variablesReference, isExpandable, value) {
|
|
const node = document.createElement('div');
|
|
node.className = 'dap-tree-node';
|
|
node.dataset.variablesRef = variablesReference;
|
|
|
|
const content = document.createElement('div');
|
|
content.className = 'dap-tree-content';
|
|
|
|
// Expand icon
|
|
const expandIcon = document.createElement('span');
|
|
expandIcon.className = 'dap-expand-icon';
|
|
expandIcon.textContent = isExpandable ? '\u25B6' : ' '; // triangleright or space
|
|
content.appendChild(expandIcon);
|
|
|
|
// Name
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.className = 'text-bold';
|
|
nameSpan.textContent = name;
|
|
content.appendChild(nameSpan);
|
|
|
|
// Value (if provided)
|
|
if (value !== undefined) {
|
|
const valueSpan = document.createElement('span');
|
|
valueSpan.className = 'color-fg-muted';
|
|
valueSpan.textContent = `: ${value}`;
|
|
content.appendChild(valueSpan);
|
|
}
|
|
|
|
node.appendChild(content);
|
|
|
|
if (isExpandable && variablesReference > 0) {
|
|
content.style.cursor = 'pointer';
|
|
content.addEventListener('click', () => toggleTreeNode(node));
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Toggle tree node expansion
|
|
*/
|
|
async function toggleTreeNode(node) {
|
|
const children = node.querySelector('.dap-tree-children');
|
|
const expandIcon = node.querySelector('.dap-expand-icon');
|
|
|
|
if (children) {
|
|
// Toggle visibility
|
|
children.hidden = !children.hidden;
|
|
expandIcon.textContent = children.hidden ? '\u25B6' : '\u25BC'; // triangleright or triangledown
|
|
return;
|
|
}
|
|
|
|
// Fetch children
|
|
const variablesRef = parseInt(node.dataset.variablesRef);
|
|
if (!variablesRef) return;
|
|
|
|
expandIcon.textContent = '...';
|
|
|
|
try {
|
|
const response = await sendDapRequest('variables', { variablesReference: variablesRef });
|
|
|
|
const childContainer = document.createElement('div');
|
|
childContainer.className = 'dap-tree-children ml-3';
|
|
|
|
for (const variable of response.variables) {
|
|
const hasChildren = variable.variablesReference > 0;
|
|
const childNode = createTreeNode(
|
|
variable.name,
|
|
variable.variablesReference,
|
|
hasChildren,
|
|
variable.value
|
|
);
|
|
childContainer.appendChild(childNode);
|
|
}
|
|
|
|
node.appendChild(childContainer);
|
|
expandIcon.textContent = '\u25BC'; // triangledown
|
|
} catch (error) {
|
|
console.error('[Content] Failed to load variables:', error);
|
|
expandIcon.textContent = '\u25B6'; // triangleright
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle stopped event from DAP
|
|
*/
|
|
async function handleStoppedEvent(body) {
|
|
console.log('[Content] Stopped event:', body);
|
|
|
|
isConnected = true;
|
|
updateStatus('PAUSED', body.reason || 'paused');
|
|
enableControls(true);
|
|
|
|
// Get current location
|
|
try {
|
|
const stackTrace = await sendDapRequest('stackTrace', { threadId: 1 });
|
|
|
|
if (stackTrace.stackFrames && stackTrace.stackFrames.length > 0) {
|
|
const currentFrame = stackTrace.stackFrames[0];
|
|
currentFrameId = currentFrame.id;
|
|
|
|
// Strip result indicator from step name for DOM lookup
|
|
const rawStepName = stripResultIndicator(currentFrame.name);
|
|
let stepElement = findStepByName(rawStepName);
|
|
|
|
if (!stepElement && currentFrame.line > 0) {
|
|
// Fallback: use step number from Line property
|
|
// Add 1 to account for "Set up job" which is always step 1 in GitHub UI but not in DAP
|
|
stepElement = findStepByNumber(currentFrame.line + 1);
|
|
}
|
|
|
|
// Update breakpoint indicator
|
|
if (stepElement) {
|
|
updateBreakpointIndicator(stepElement);
|
|
}
|
|
|
|
// Update step info in header
|
|
const stepInfo = debuggerPane?.querySelector('.dap-step-info');
|
|
if (stepInfo) {
|
|
stepInfo.textContent = `Paused before: ${rawStepName}`;
|
|
}
|
|
|
|
// Load scopes
|
|
await loadScopes(currentFrame.id);
|
|
}
|
|
} catch (error) {
|
|
console.error('[Content] Failed to get stack trace:', error);
|
|
appendOutput(`Error: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle output event from DAP
|
|
*/
|
|
function handleOutputEvent(body) {
|
|
if (body.output) {
|
|
const category = body.category === 'stderr' ? 'error' : 'stdout';
|
|
appendOutput(body.output.trimEnd(), category);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle terminated event from DAP
|
|
*/
|
|
function handleTerminatedEvent() {
|
|
isConnected = false;
|
|
updateStatus('TERMINATED', 'Session ended');
|
|
enableControls(false);
|
|
clearBreakpointIndicator();
|
|
}
|
|
|
|
/**
|
|
* Load current debug state (used when page loads while already paused)
|
|
*/
|
|
async function loadCurrentDebugState() {
|
|
if (!debuggerPane) return;
|
|
|
|
try {
|
|
const stackTrace = await sendDapRequest('stackTrace', { threadId: 1 });
|
|
if (stackTrace.stackFrames && stackTrace.stackFrames.length > 0) {
|
|
const currentFrame = stackTrace.stackFrames[0];
|
|
currentFrameId = currentFrame.id;
|
|
|
|
// Strip result indicator from step name for DOM lookup
|
|
const rawStepName = stripResultIndicator(currentFrame.name);
|
|
let stepElement = findStepByName(rawStepName);
|
|
|
|
if (!stepElement && currentFrame.line > 0) {
|
|
// Fallback: use step number from Line property
|
|
stepElement = findStepByNumber(currentFrame.line + 1);
|
|
}
|
|
|
|
// Update breakpoint indicator
|
|
if (stepElement) {
|
|
updateBreakpointIndicator(stepElement);
|
|
}
|
|
|
|
// Update step info in header
|
|
const stepInfo = debuggerPane.querySelector('.dap-step-info');
|
|
if (stepInfo) {
|
|
stepInfo.textContent = `Paused before: ${rawStepName}`;
|
|
}
|
|
|
|
// Load scopes
|
|
await loadScopes(currentFrame.id);
|
|
}
|
|
} catch (error) {
|
|
console.error('[Content] Failed to load current debug state:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle status change from background
|
|
*/
|
|
function handleStatusChange(status) {
|
|
console.log('[Content] Status changed:', status);
|
|
|
|
switch (status) {
|
|
case 'connected':
|
|
isConnected = true;
|
|
updateStatus('CONNECTED', 'Waiting for debug event...');
|
|
break;
|
|
|
|
case 'paused':
|
|
isConnected = true;
|
|
updateStatus('PAUSED');
|
|
enableControls(true);
|
|
loadCurrentDebugState();
|
|
break;
|
|
|
|
case 'running':
|
|
isConnected = true;
|
|
updateStatus('RUNNING', 'Running...');
|
|
enableControls(false);
|
|
break;
|
|
|
|
case 'disconnected':
|
|
isConnected = false;
|
|
updateStatus('DISCONNECTED', 'Disconnected');
|
|
enableControls(false);
|
|
clearBreakpointIndicator();
|
|
break;
|
|
|
|
case 'error':
|
|
isConnected = false;
|
|
updateStatus('ERROR', 'Connection error');
|
|
enableControls(false);
|
|
clearBreakpointIndicator();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listen for messages from background script
|
|
*/
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
console.log('[Content] Received message:', message.type);
|
|
|
|
switch (message.type) {
|
|
case 'dap-event':
|
|
const event = message.event;
|
|
switch (event.event) {
|
|
case 'stopped':
|
|
handleStoppedEvent(event.body);
|
|
break;
|
|
case 'output':
|
|
handleOutputEvent(event.body);
|
|
break;
|
|
case 'terminated':
|
|
handleTerminatedEvent();
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'status-changed':
|
|
handleStatusChange(message.status);
|
|
break;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Inject debug button into GitHub Actions UI header
|
|
*/
|
|
function injectDebugButton() {
|
|
// Try multiple possible container selectors
|
|
let container = document.querySelector('.js-check-run-search');
|
|
|
|
// Alternative: look for the header area with search box
|
|
if (!container) {
|
|
container = document.querySelector('.PageLayout-content .Box-header');
|
|
}
|
|
|
|
if (!container || container.querySelector('.dap-debug-btn-container')) {
|
|
return; // Already injected or container not found
|
|
}
|
|
|
|
const buttonContainer = document.createElement('div');
|
|
buttonContainer.className = 'ml-2 dap-debug-btn-container';
|
|
buttonContainer.innerHTML = `
|
|
<button type="button" class="btn btn-sm dap-debug-btn" title="Toggle DAP Debugger">
|
|
${Icons.bug}
|
|
<span class="ml-1 dap-debug-btn-text">Debug</span>
|
|
</button>
|
|
`;
|
|
|
|
const button = buttonContainer.querySelector('button');
|
|
button.addEventListener('click', async () => {
|
|
let pane = document.querySelector('.dap-debugger-pane');
|
|
if (pane) {
|
|
// Close pane
|
|
closeDebuggerPane();
|
|
} else {
|
|
// Load preference and create pane
|
|
await loadLayoutPreference();
|
|
pane = injectDebuggerPane();
|
|
if (pane) {
|
|
button.classList.add('selected');
|
|
// Check connection status after creating pane
|
|
chrome.runtime.sendMessage({ type: 'get-status' }, (response) => {
|
|
if (response && response.status) {
|
|
handleStatusChange(response.status);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Insert at the beginning of the container
|
|
container.insertBefore(buttonContainer, container.firstChild);
|
|
console.log('[Content] Debug button injected');
|
|
}
|
|
|
|
/**
|
|
* Initialize content script
|
|
*/
|
|
async function init() {
|
|
console.log('[Content] Actions DAP Debugger content script loaded');
|
|
|
|
// Load layout preference
|
|
await loadLayoutPreference();
|
|
|
|
// Check if we're on a job page
|
|
const steps = getAllSteps();
|
|
if (steps.length === 0) {
|
|
console.log('[Content] No steps found, waiting for DOM...');
|
|
// Wait for steps to appear
|
|
const observer = new MutationObserver((mutations) => {
|
|
const steps = getAllSteps();
|
|
if (steps.length > 0) {
|
|
observer.disconnect();
|
|
console.log('[Content] Steps found, injecting debug button');
|
|
injectDebugButton();
|
|
}
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
return;
|
|
}
|
|
|
|
// Inject debug button in header (user can click to show debugger pane)
|
|
injectDebugButton();
|
|
|
|
// Check current connection status
|
|
chrome.runtime.sendMessage({ type: 'get-status' }, async (response) => {
|
|
if (response && response.status) {
|
|
// If already connected/paused, auto-show the debugger pane
|
|
if (response.status === 'paused' || response.status === 'connected') {
|
|
const pane = document.querySelector('.dap-debugger-pane');
|
|
if (!pane) {
|
|
injectDebuggerPane();
|
|
const btn = document.querySelector('.dap-debug-btn');
|
|
if (btn) btn.classList.add('selected');
|
|
}
|
|
|
|
handleStatusChange(response.status);
|
|
|
|
// If already paused, load the current debug state
|
|
if (response.status === 'paused') {
|
|
await loadCurrentDebugState();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|