# DAP Browser Extension for GitHub Actions Debugging **Status:** Planned **Date:** January 2026 **Related:** [dap-debugging.md](./dap-debugging.md), [dap-step-backwards.md](./dap-step-backwards.md) ## Overview A Chrome extension that injects a debugger UI into GitHub Actions job pages, connecting to a runner's DAP server via a WebSocket-to-TCP proxy. This enables interactive debugging (variable inspection, REPL, step control) directly in the browser. **Goal:** Demonstrate the power of implementing a standard protocol like DAP - the same debugging capabilities available in nvim-dap can now work in a browser with minimal effort. ## Architecture ``` ┌──────────────────────────────────────────────────────────────────────────────────┐ │ GitHub Actions Job Page │ │ │ │ ┌──────────────────────────────────────────────────────────────────────────┐ │ │ │ Content Script (injected) │ │ │ │ - Observes DOM for step elements │ │ │ │ - Injects debugger pane above the "next pending step" │ │ │ │ - Renders scopes tree, REPL console, control buttons │ │ │ │ - Communicates with background script via chrome.runtime messaging │ │ │ └───────────────────────────────────────┬──────────────────────────────────┘ │ │ │ chrome.runtime.sendMessage │ └──────────────────────────────────────────┼──────────────────────────────────────┘ │ ┌──────────────────────────────────────────┼──────────────────────────────────────┐ │ Background Script (Service Worker) │ │ ┌───────────────────────────────────────┴──────────────────────────────────┐ │ │ │ DAP Client │ │ │ │ - Connects to WebSocket proxy (ws://localhost:4712) │ │ │ │ - Implements DAP request/response handling │ │ │ │ - Relays events to content script │ │ │ │ - Manages connection state │ │ │ └───────────────────────────────────────┬──────────────────────────────────┘ │ │ │ WebSocket │ └──────────────────────────────────────────┼──────────────────────────────────────┘ │ ┌──────────────────────────────────────────┼──────────────────────────────────────┐ │ WebSocket-to-TCP Proxy (Node.js) │ │ ┌───────────────────────────────────────┴──────────────────────────────────┐ │ │ │ - Listens on ws://localhost:4712 │ │ │ │ - Connects to tcp://localhost:4711 (DAP server) │ │ │ │ - Handles DAP message framing (Content-Length headers) │ │ │ │ - Bidirectional message relay │ │ │ └───────────────────────────────────────┬──────────────────────────────────┘ │ │ │ TCP │ └──────────────────────────────────────────┼──────────────────────────────────────┘ │ ┌──────────────────────────────────────────┼──────────────────────────────────────┐ │ Runner DAP Server (existing) │ │ ┌───────────────────────────────────────┴──────────────────────────────────┐ │ │ │ tcp://localhost:4711 │ │ │ │ - DapServer.cs (existing implementation) │ │ │ │ - DapDebugSession.cs (handles all debug operations) │ │ │ └──────────────────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────────┘ ``` ## Directory Structure ``` browser-ext/ ├── manifest.json # Chrome extension manifest v3 ├── background/ │ └── background.js # Service worker - DAP client, WebSocket connection ├── content/ │ ├── content.js # DOM manipulation, pane injection, UI logic │ └── content.css # Styling for debugger pane ├── popup/ │ ├── popup.html # Extension popup (connection status, settings) │ ├── popup.js │ └── popup.css ├── lib/ │ └── dap-protocol.js # DAP message types and helpers ├── proxy/ │ ├── proxy.js # WebSocket-to-TCP bridge │ └── package.json # Proxy dependencies (ws) └── icons/ ├── icon16.png ├── icon48.png └── icon128.png ``` ## GitHub Actions Job Page DOM Structure ### Step Container Element: `` Each step is a custom element with rich data attributes: ```html data-number="4" data-conclusion="failure" data-external-id="759535e9-..." data-expand="true" data-started-at="2026-01-15T..." data-completed-at="2026-01-15T..." data-log-url="..." data-job-completed="" > ``` ### Key Selectors | Target | Selector | |--------|----------| | All steps | `check-step` or `check-steps > check-step` | | Step by number | `check-step[data-number="4"]` | | Failed steps | `check-step[data-conclusion="failure"]` | | In-progress steps | `check-step[data-conclusion="in-progress"]` | | Pending steps | `check-step:not([data-conclusion])` | | Step name | `check-step[data-name]` attribute | | Steps container | `check-steps` | | Step details (expandable) | `details.CheckStep` inside `check-step` | ### Dark Mode Detection GitHub stores theme info in: ```html ``` Can also check: `document.documentElement.dataset.colorMode` or CSS custom properties. --- ## Implementation Phases ### Phase 1: WebSocket-to-TCP Proxy (~1-2 hours) **File: `browser-ext/proxy/proxy.js`** A minimal Node.js script that bridges WebSocket to the DAP TCP server. **Responsibilities:** 1. Listen for WebSocket connections on port 4712 2. For each WebSocket client, open TCP connection to localhost:4711 3. Handle DAP message framing: - WS→TCP: Wrap JSON with `Content-Length: N\r\n\r\n` - TCP→WS: Parse headers, extract JSON, send to WebSocket 4. Log messages for debugging 5. Clean disconnect handling **Key implementation:** ```javascript const WebSocket = require('ws'); const net = require('net'); const WS_PORT = 4712; const DAP_HOST = '127.0.0.1'; const DAP_PORT = 4711; const wss = new WebSocket.Server({ port: WS_PORT }); wss.on('connection', (ws) => { console.log('[Proxy] WebSocket client connected'); const tcp = net.createConnection({ host: DAP_HOST, port: DAP_PORT }); let buffer = ''; // WebSocket → TCP (add Content-Length framing) ws.on('message', (data) => { const json = data.toString(); const framed = `Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`; tcp.write(framed); }); // TCP → WebSocket (parse Content-Length framing) tcp.on('data', (chunk) => { buffer += chunk.toString(); // Parse DAP messages from buffer... // For each complete message, ws.send(json) }); // Handle disconnects ws.on('close', () => tcp.end()); tcp.on('close', () => ws.close()); }); ``` **File: `browser-ext/proxy/package.json`** ```json { "name": "dap-websocket-proxy", "version": "1.0.0", "main": "proxy.js", "dependencies": { "ws": "^8.16.0" } } ``` --- ### Phase 2: Chrome Extension Scaffold (~1 hour) **File: `browser-ext/manifest.json`** ```json { "manifest_version": 3, "name": "Actions DAP Debugger", "version": "0.1.0", "description": "Debug GitHub Actions workflows with DAP", "permissions": ["activeTab", "storage"], "host_permissions": ["https://github.com/*"], "background": { "service_worker": "background/background.js" }, "content_scripts": [{ "matches": ["https://github.com/*/*/actions/runs/*/job/*"], "js": ["lib/dap-protocol.js", "content/content.js"], "css": ["content/content.css"], "run_at": "document_idle" }], "action": { "default_popup": "popup/popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } } } ``` **Icons:** Create simple debug-themed icons (bug icon or similar). --- ### Phase 3: Background Script - DAP Client (~2-3 hours) **File: `browser-ext/background/background.js`** **Responsibilities:** 1. **Connection management:** - Connect to WebSocket proxy on user action (popup button) - Handle disconnect/reconnect - Track connection state (disconnected, connecting, connected, paused, running) 2. **DAP protocol handling:** - Sequence number tracking - Request/response correlation (pending requests Map) - Event dispatch to content script 3. **DAP commands to implement:** | Command | Purpose | |---------|---------| | `initialize` | Exchange capabilities | | `attach` | Attach to running debug session | | `configurationDone` | Signal ready to receive events | | `threads` | Get thread list (single thread for job) | | `stackTrace` | Get current step + history as frames | | `scopes` | Get scope categories for a frame | | `variables` | Get variables for a scope/object | | `evaluate` | Expression eval + REPL commands | | `continue` | Run to end or next breakpoint | | `next` | Step to next step | | `stepBack` | Step back to previous checkpoint | | `reverseContinue` | Go back to first checkpoint | | `disconnect` | End debug session | 4. **Message relay structure:** ```javascript // Content script → Background chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'dap-request') { sendDapRequest(message.command, message.args) .then(response => sendResponse({ success: true, body: response })) .catch(error => sendResponse({ success: false, error: error.message })); return true; // Async response } if (message.type === 'connect') { connectToProxy(message.host, message.port); } }); // Background → Content script (events) function broadcastEvent(event) { chrome.tabs.query({ url: 'https://github.com/*/*/actions/runs/*/job/*' }, (tabs) => { tabs.forEach(tab => { chrome.tabs.sendMessage(tab.id, { type: 'dap-event', event }); }); }); } ``` --- ### Phase 4: Content Script - UI Injection (~3-4 hours) **File: `browser-ext/content/content.js`** #### 4.1 Step Detection & Mapping ```javascript // Build step map: DAP frame index → DOM element 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; } // Match DAP frame to DOM step // DAP stackTrace returns frames with name = step display name function findStepByName(stepName) { return document.querySelector(`check-step[data-name="${CSS.escape(stepName)}"]`); } ``` #### 4.2 Debugger Pane Structure ```html
... Debugger Paused before: Run cat doesnotexist PAUSED
Variables
Console
Step 4 of 8 · Checkpoints: 3
``` #### 4.3 Pane Injection ```javascript function injectDebuggerPane(beforeStep) { // Remove existing pane if any const existing = document.querySelector('.dap-debugger-pane'); if (existing) existing.remove(); // Create pane const pane = document.createElement('div'); pane.className = 'dap-debugger-pane px-2 mb-2 border rounded-2'; pane.innerHTML = PANE_HTML; // Template from above // Insert before the target step beforeStep.parentNode.insertBefore(pane, beforeStep); // Setup event handlers setupPaneEventHandlers(pane); return pane; } function moveDebuggerPane(newStepIndex, stepName) { const pane = document.querySelector('.dap-debugger-pane'); const steps = document.querySelectorAll('check-step'); const targetStep = steps[newStepIndex]; if (pane && targetStep) { targetStep.parentNode.insertBefore(pane, targetStep); pane.querySelector('.dap-header .color-fg-muted').textContent = `Paused before: ${stepName}`; pane.dataset.dapStep = targetStep.dataset.number; } } ``` #### 4.4 Scopes Tree Rendering ```javascript async function loadScopes(frameId) { const response = await sendDapRequest('scopes', { frameId }); const scopesContainer = document.querySelector('.dap-scope-tree'); scopesContainer.innerHTML = ''; for (const scope of response.scopes) { const node = createTreeNode(scope.name, scope.variablesReference, true); scopesContainer.appendChild(node); } } function createTreeNode(name, variablesReference, isExpandable) { const node = document.createElement('div'); node.className = 'dap-tree-node'; node.dataset.variablesRef = variablesReference; node.innerHTML = ` ${isExpandable ? '▶' : ' '} ${escapeHtml(name)} `; if (isExpandable) { node.addEventListener('click', () => toggleTreeNode(node)); } return node; } async function toggleTreeNode(node) { const children = node.querySelector('.dap-tree-children'); if (children) { children.hidden = !children.hidden; node.querySelector('.dap-expand-icon').textContent = children.hidden ? '▶' : '▼'; return; } // Fetch children const variablesRef = parseInt(node.dataset.variablesRef); 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) { if (variable.variablesReference > 0) { // Expandable const childNode = createTreeNode(variable.name, variable.variablesReference, true); childNode.querySelector('.text-bold').insertAdjacentHTML('afterend', `: ${escapeHtml(variable.value)}`); childContainer.appendChild(childNode); } else { // Leaf const leaf = document.createElement('div'); leaf.className = 'dap-tree-leaf'; leaf.innerHTML = ` ${escapeHtml(variable.name)}: ${escapeHtml(variable.value)} `; childContainer.appendChild(leaf); } } node.appendChild(childContainer); node.querySelector('.dap-expand-icon').textContent = '▼'; } ``` #### 4.5 REPL Console ```javascript function setupReplInput(pane) { const input = pane.querySelector('.dap-repl-input input'); const output = pane.querySelector('.dap-repl-output'); const history = []; let historyIndex = -1; input.addEventListener('keydown', async (e) => { if (e.key === 'Enter') { const command = input.value.trim(); if (!command) return; history.push(command); historyIndex = history.length; input.value = ''; // Show command appendOutput(output, `> ${command}`, 'input'); // Send to DAP try { const response = await sendDapRequest('evaluate', { expression: command, context: command.startsWith('!') ? 'repl' : 'watch' }); appendOutput(output, response.result, 'result'); } catch (error) { appendOutput(output, error.message, 'error'); } } else if (e.key === 'ArrowUp') { if (historyIndex > 0) { historyIndex--; input.value = history[historyIndex]; } e.preventDefault(); } else if (e.key === 'ArrowDown') { if (historyIndex < history.length - 1) { historyIndex++; input.value = history[historyIndex]; } else { historyIndex = history.length; input.value = ''; } e.preventDefault(); } }); } function appendOutput(container, text, type) { const line = document.createElement('div'); line.className = `dap-output-${type}`; if (type === 'error') line.classList.add('color-fg-danger'); if (type === 'input') line.classList.add('color-fg-muted'); line.textContent = text; container.appendChild(line); container.scrollTop = container.scrollHeight; } ``` #### 4.6 DAP Event Handling ```javascript chrome.runtime.onMessage.addListener((message) => { if (message.type !== 'dap-event') return; const event = message.event; switch (event.event) { case 'stopped': handleStoppedEvent(event.body); break; case 'output': handleOutputEvent(event.body); break; case 'terminated': handleTerminatedEvent(); break; } }); async function handleStoppedEvent(body) { // Update status updateStatus('PAUSED', body.reason); enableControls(true); // Get current location const stackTrace = await sendDapRequest('stackTrace', { threadId: 1 }); if (stackTrace.stackFrames.length > 0) { const currentFrame = stackTrace.stackFrames[0]; moveDebuggerPane(currentFrame.id, currentFrame.name); await loadScopes(currentFrame.id); } } function handleOutputEvent(body) { const output = document.querySelector('.dap-repl-output'); if (output) { const category = body.category === 'stderr' ? 'error' : 'stdout'; appendOutput(output, body.output.trimEnd(), category); } } function handleTerminatedEvent() { updateStatus('TERMINATED'); enableControls(false); } ``` #### 4.7 Control Buttons ```javascript function setupControlButtons(pane) { 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(`DAP ${action} failed:`, error); appendOutput(document.querySelector('.dap-repl-output'), `Error: ${error.message}`, 'error'); enableControls(true); updateStatus('ERROR'); } }); }); } function enableControls(enabled) { document.querySelectorAll('.dap-controls button').forEach(btn => { btn.disabled = !enabled; }); } function updateStatus(status, reason) { const label = document.querySelector('.dap-header .Label'); if (label) { label.textContent = status; label.className = 'Label ml-auto ' + { 'PAUSED': 'Label--attention', 'RUNNING': 'Label--success', 'TERMINATED': 'Label--secondary', 'ERROR': 'Label--danger' }[status]; } } ``` --- ### Phase 5: Styling (~1 hour) **File: `browser-ext/content/content.css`** ```css /* Match GitHub's Primer design system */ .dap-debugger-pane { background-color: var(--bgColor-default, #0d1117); border-color: var(--borderColor-default, #30363d) !important; margin-left: 8px; margin-right: 8px; } .dap-header { background-color: var(--bgColor-muted, #161b22); } .dap-scopes { border-color: var(--borderColor-default, #30363d) !important; } .dap-scope-tree { font-size: 12px; } .dap-tree-node { cursor: pointer; padding: 2px 0; } .dap-tree-node:hover { background-color: var(--bgColor-muted, #161b22); } .dap-tree-leaf { padding: 2px 0; padding-left: 16px; } .dap-expand-icon { display: inline-block; width: 16px; text-align: center; color: var(--fgColor-muted, #8b949e); } .dap-repl-output { background-color: var(--bgColor-inset, #010409); font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; } .dap-output-input { color: var(--fgColor-muted, #8b949e); } .dap-output-result { color: var(--fgColor-default, #e6edf3); } .dap-output-stdout { color: var(--fgColor-default, #e6edf3); } .dap-output-error { color: var(--fgColor-danger, #f85149); } .dap-repl-input input { font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace; font-size: 12px; background-color: var(--bgColor-inset, #010409); border-color: var(--borderColor-default, #30363d); color: var(--fgColor-default, #e6edf3); } .dap-controls { background-color: var(--bgColor-muted, #161b22); } .dap-controls button { min-width: 32px; } .dap-controls button:disabled { opacity: 0.5; cursor: not-allowed; } /* Status labels */ .Label--attention { background-color: #9e6a03; color: #ffffff; } .Label--success { background-color: #238636; color: #ffffff; } .Label--danger { background-color: #da3633; color: #ffffff; } .Label--secondary { background-color: #30363d; color: #8b949e; } ``` --- ### Phase 6: Popup UI (~1 hour) **File: `browser-ext/popup/popup.html`** ```html ``` **File: `browser-ext/popup/popup.js`** ```javascript document.addEventListener('DOMContentLoaded', () => { const statusIndicator = document.getElementById('status-indicator'); const statusText = document.getElementById('status-text'); const connectBtn = document.getElementById('connect-btn'); const disconnectBtn = document.getElementById('disconnect-btn'); const hostInput = document.getElementById('proxy-host'); const portInput = document.getElementById('proxy-port'); // Load saved config chrome.storage.local.get(['proxyHost', 'proxyPort'], (data) => { if (data.proxyHost) hostInput.value = data.proxyHost; if (data.proxyPort) portInput.value = data.proxyPort; }); // Get current status from background chrome.runtime.sendMessage({ type: 'get-status' }, (response) => { updateStatusUI(response.status); }); connectBtn.addEventListener('click', () => { const host = hostInput.value; const port = parseInt(portInput.value); // Save config chrome.storage.local.set({ proxyHost: host, proxyPort: port }); // Connect chrome.runtime.sendMessage({ type: 'connect', host, port }, (response) => { updateStatusUI(response.status); }); }); disconnectBtn.addEventListener('click', () => { chrome.runtime.sendMessage({ type: 'disconnect' }, (response) => { updateStatusUI(response.status); }); }); function updateStatusUI(status) { statusText.textContent = status; statusIndicator.className = 'status-indicator status-' + status.toLowerCase(); connectBtn.disabled = (status !== 'disconnected'); disconnectBtn.disabled = (status === 'disconnected'); } }); ``` **File: `browser-ext/popup/popup.css`** ```css body { width: 300px; padding: 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 14px; background-color: #0d1117; color: #e6edf3; } h3 { margin: 0 0 16px 0; font-size: 16px; } .status-section { display: flex; align-items: center; margin-bottom: 16px; padding: 8px; background-color: #161b22; border-radius: 6px; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; } .status-disconnected { background-color: #6e7681; } .status-connecting { background-color: #9e6a03; } .status-connected { background-color: #238636; } .status-paused { background-color: #9e6a03; } .status-error { background-color: #da3633; } .config-section { margin-bottom: 16px; } .config-section label { display: block; margin-bottom: 8px; font-size: 12px; color: #8b949e; } .config-section input { width: 100%; padding: 8px; margin-top: 4px; background-color: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 14px; } .actions-section { display: flex; gap: 8px; margin-bottom: 16px; } button { flex: 1; padding: 8px 16px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; } button:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background-color: #238636; color: white; } .btn-secondary { background-color: #30363d; color: #e6edf3; } .help-section { font-size: 12px; color: #8b949e; } .help-section p { margin: 4px 0; } .help-section code { background-color: #161b22; padding: 2px 4px; border-radius: 3px; font-family: ui-monospace, monospace; } ``` --- ### Phase 7: DAP Protocol Helpers **File: `browser-ext/lib/dap-protocol.js`** ```javascript // DAP message types and constants const DapCommands = { INITIALIZE: 'initialize', ATTACH: 'attach', CONFIGURATION_DONE: 'configurationDone', THREADS: 'threads', STACK_TRACE: 'stackTrace', SCOPES: 'scopes', VARIABLES: 'variables', EVALUATE: 'evaluate', CONTINUE: 'continue', NEXT: 'next', STEP_BACK: 'stepBack', REVERSE_CONTINUE: 'reverseContinue', DISCONNECT: 'disconnect' }; const DapEvents = { STOPPED: 'stopped', OUTPUT: 'output', TERMINATED: 'terminated', INITIALIZED: 'initialized' }; // Helper to create DAP request function createDapRequest(seq, command, args = {}) { return { seq, type: 'request', command, arguments: args }; } // Helper to parse DAP response/event function parseDapMessage(json) { const msg = JSON.parse(json); return { isResponse: msg.type === 'response', isEvent: msg.type === 'event', seq: msg.seq, requestSeq: msg.request_seq, command: msg.command, event: msg.event, success: msg.success, body: msg.body, message: msg.message }; } // Export for use in other scripts if (typeof module !== 'undefined') { module.exports = { DapCommands, DapEvents, createDapRequest, parseDapMessage }; } ``` --- ## DAP Protocol Flow ``` Extension Proxy Runner │ │ │ │──── WebSocket connect ──►│ │ │ │──── TCP connect ────────►│ │◄─── "connected" ─────────│ │ │ │ │ │──── initialize ─────────►│──── initialize ─────────►│ │◄─── InitializeResponse ──│◄─── InitializeResponse ──│ │ │ │ │──── attach ─────────────►│──── attach ─────────────►│ │◄─── AttachResponse ──────│◄─── AttachResponse ──────│ │ │ │ │──── configurationDone ──►│──── configurationDone ──►│ │◄─── ConfigDoneResponse ──│◄─── ConfigDoneResponse ──│ │ │ │ │◄─── stopped event ───────│◄─── stopped event ───────│ (paused at step) │ │ │ │──── threads ────────────►│──── threads ────────────►│ │◄─── ThreadsResponse ─────│◄─── ThreadsResponse ─────│ │ │ │ │──── stackTrace ─────────►│──── stackTrace ─────────►│ │◄─── StackTraceResponse ──│◄─── StackTraceResponse ──│ │ │ │ │──── scopes ─────────────►│──── scopes ─────────────►│ │◄─── ScopesResponse ──────│◄─── ScopesResponse ──────│ │ │ │ │──── variables ──────────►│──── variables ──────────►│ (user expands scope) │◄─── VariablesResponse ───│◄─── VariablesResponse ───│ │ │ │ │──── evaluate ───────────►│──── evaluate ───────────►│ (REPL command) │◄─── output events ───────│◄─── output events ───────│ (streaming) │◄─── EvaluateResponse ────│◄─── EvaluateResponse ────│ │ │ │ │──── next ───────────────►│──── next ───────────────►│ (step to next) │◄─── NextResponse ────────│◄─── NextResponse ────────│ │◄─── stopped event ───────│◄─── stopped event ───────│ (paused at next step) ``` --- ## Files Summary | File | Lines Est. | Purpose | |------|------------|---------| | `proxy/proxy.js` | ~100 | WebSocket↔TCP bridge with DAP framing | | `proxy/package.json` | ~10 | Proxy dependencies | | `manifest.json` | ~35 | Extension configuration | | `background/background.js` | ~300 | DAP client, WebSocket, message relay | | `content/content.js` | ~450 | DOM manipulation, pane injection, UI | | `content/content.css` | ~150 | Debugger pane styling | | `lib/dap-protocol.js` | ~50 | DAP message helpers | | `popup/popup.html` | ~40 | Popup structure | | `popup/popup.js` | ~80 | Popup logic | | `popup/popup.css` | ~80 | Popup styling | **Total: ~1,300 lines** --- ## Testing Plan ### 1. Proxy Testing - [ ] Start proxy, verify WebSocket accepts connection - [ ] Test with simple WebSocket client (wscat) - [ ] Connect nvim-dap through proxy to verify passthrough works ### 2. Extension Load Testing - [ ] Load unpacked extension in Chrome - [ ] Navigate to GitHub Actions job page - [ ] Verify content script activates (check console) - [ ] Click popup, verify UI renders ### 3. Integration Testing - [ ] Start proxy - [ ] Run workflow with "Enable debug logging" - [ ] Connect extension via popup - [ ] Verify debugger pane appears - [ ] Test scope expansion - [ ] Test REPL commands: `${{ github.ref }}`, `!env | grep GITHUB` - [ ] Test step controls: next, continue - [ ] Test step-back functionality - [ ] Verify pane moves between steps ### 4. Edge Cases - [ ] Disconnect/reconnect handling - [ ] Proxy not running error - [ ] DAP server timeout - [ ] Large variable values - [ ] Special characters in step names --- ## Demo Flow 1. **Setup:** ```bash cd browser-ext/proxy && npm install && node proxy.js ``` 2. **Load Extension:** - Chrome → Extensions → Load unpacked → select `browser-ext/` 3. **Start Debug Session:** - Go to GitHub repo → Actions → Re-run job with "Enable debug logging" - Wait for runner to show "DAP debugger waiting for connection..." 4. **Connect:** - Open job page in Chrome - Click extension popup → "Connect" - Debugger pane appears above first step 5. **Demo Features:** - Expand scopes to show `github`, `env`, `steps` contexts - Run REPL: `${{ github.event_name }}` → shows "push" - Run REPL: `!ls -la` → shows files - Click "Step" → pane moves to next step - Click "Step Back" → demonstrate time-travel - Modify env via REPL, step forward, show fix works --- ## Estimated Effort | Phase | Effort | |-------|--------| | Phase 1: WebSocket proxy | 1-2 hours | | Phase 2: Extension scaffold | 1 hour | | Phase 3: Background/DAP client | 2-3 hours | | Phase 4: Content script/UI | 3-4 hours | | Phase 5: Styling | 1 hour | | Phase 6: Popup UI | 1 hour | | Phase 7: DAP helpers | 0.5 hours | | Testing & Polish | 1-2 hours | | **Total** | **~11-14 hours** | --- ## Future Enhancements (Out of Scope) - Firefox extension support - Breakpoint setting UI (click on step to set breakpoint) - Watch expressions panel - Call stack visualization - Integration with GitHub Codespaces (direct connection without proxy) - Persistent connection across page navigations - Multiple job debugging