extension ui improvements

This commit is contained in:
Francesco Renzi
2026-01-21 21:29:35 +00:00
committed by GitHub
parent 11ca211a3a
commit 38514d5278
3 changed files with 908 additions and 198 deletions

View File

@@ -0,0 +1,214 @@
# DAP Browser Extension UI Improvements
**Status:** Implemented
**Date:** January 2026
**Related:** [dap-browser-extension.md](./dap-browser-extension.md), [dap-debugging.md](./dap-debugging.md)
## Overview
This document describes UI improvements made to the DAP browser extension debugger based on designer feedback. The original implementation inserted the debugger pane inline between workflow steps, causing it to "bounce around" as the user stepped through the job. The new implementation uses fixed positioning with two layout options.
## Problem Statement
The original debugger UI had these issues:
1. **Bouncing pane**: The debugger pane was inserted between steps in the DOM, so it moved position each time the user stepped to a new step
2. **No layout flexibility**: Users couldn't choose where they wanted the debugger positioned
3. **No breakpoint indicator**: There was no visual indication of which step the debugger was currently paused at
## Solution
Implemented two fixed-position layout options inspired by browser DevTools:
### 1. Bottom Panel Layout (Default)
- Fixed at the bottom of the viewport
- Height: 280px
- Variables panel on left (33%), Console on right (67%)
- Control buttons in the header row (right side)
- Similar to Chrome/Firefox DevTools
```
+------------------------------------------------------------------+
| Debugger [step info] [controls] [layout] [X] |
+------------------------------------------------------------------+
| Variables (1/3) | Console (2/3) |
| > github | Welcome message... |
| > env | > command output |
| > runner | |
| | [input field ] |
+------------------------------------------------------------------+
```
### 2. Sidebar Layout
- Fixed on the right side of the viewport
- Width: 350px
- Full height of viewport
- Variables on top, Console in middle, Controls at bottom
```
+------------------+
| Debugger [X] |
| [layout btns] |
+------------------+
| Variables |
| > github |
| > env |
+------------------+
| Console |
| output... |
| |
| [input ] |
+------------------+
| [ controls ] |
+------------------+
```
### 3. Breakpoint Indicator
Visual marker showing the current step where the debugger is paused:
- Red accent bar on the right edge of the step row
- Red bottom border on the step header
- Triangle pointer pointing toward the debugger panel
- Subtle gradient background highlight
## Implementation Details
### Files Modified
| File | Changes |
|------|---------|
| `browser-ext/content/content.js` | Complete refactor: new layout system, breakpoint indicator, layout toggle |
| `browser-ext/content/content.css` | New styles for layouts, breakpoint indicator, toggle buttons |
### Key Functions Added (`content.js`)
#### Layout Management
```javascript
// Load layout preference from chrome.storage.local
async function loadLayoutPreference()
// Save layout preference to chrome.storage.local
function saveLayoutPreference(layout)
// Switch between 'bottom' and 'sidebar' layouts
// Preserves console output and variable tree state during switch
function switchLayout(newLayout)
// Create the bottom panel HTML structure
function createBottomPaneHTML()
// Create the sidebar panel HTML structure
function createSidebarPaneHTML()
```
#### Breakpoint Indicator
```javascript
// Highlights the current step with CSS class 'dap-current-step'
function updateBreakpointIndicator(stepElement)
// Clears the indicator from all steps
function clearBreakpointIndicator()
```
#### Panel Controls
```javascript
// Close the debugger panel and clear indicators
function closeDebuggerPane()
// Update layout toggle button active states
function updateLayoutToggleButtons()
```
### CSS Classes Added (`content.css`)
| Class | Purpose |
|-------|---------|
| `.dap-debugger-bottom` | Bottom panel layout (fixed position) |
| `.dap-debugger-sidebar` | Sidebar layout (fixed position) |
| `.dap-layout-toggles` | Container for layout toggle buttons |
| `.dap-layout-btn` | Individual layout toggle button |
| `.dap-layout-btn.active` | Active state for selected layout |
| `.dap-close-btn` | Close button styling |
| `check-step.dap-current-step` | Breakpoint indicator on step element |
### State Variables
```javascript
let currentLayout = 'bottom'; // 'bottom' | 'sidebar'
let currentStepElement = null; // Track current step for breakpoint indicator
```
### Storage
Layout preference is persisted to `chrome.storage.local` under the key `debuggerLayout`.
## Removed Functionality
- `moveDebuggerPane()` - No longer needed since debugger uses fixed positioning
- Inline pane injection between steps - Replaced with fixed position panels
## Design Mockups Reference
The implementation was based on these mockup frames:
- **Frame 4/5/6**: Sidebar layout on right side
- **Frame 7**: Bottom panel layout with controls in header
- All frames showed the breakpoint indicator as a red/orange accent on the current step
## Future Improvements
Potential enhancements for future iterations:
1. **Resizable panels**: Allow users to drag to resize the panel width/height
2. **Minimize/maximize**: Add ability to minimize the panel to just a header bar
3. **Detached window**: Option to pop out debugger into separate browser window
4. **Keyboard shortcuts**: Add shortcuts for layout switching and panel toggle
5. **Remember panel size**: Persist user's preferred panel dimensions
6. **Breakpoint list**: Show list of all breakpoints with ability to navigate
7. **Step indicator in panel**: Show step name/number in the panel header with prev/next navigation
## Testing Checklist
- [ ] Bottom panel displays correctly at viewport bottom
- [ ] Sidebar panel displays correctly on right side
- [ ] Layout toggle buttons switch between layouts
- [ ] Layout preference persists across page reloads
- [ ] Close button removes panel and updates Debug button state
- [ ] Breakpoint indicator appears on current step when paused
- [ ] Breakpoint indicator moves when stepping to next step
- [ ] Breakpoint indicator clears when disconnected/terminated
- [ ] Console output preserves when switching layouts
- [ ] Variables tree preserves when switching layouts
- [ ] Works correctly in both light and dark mode
- [ ] Panel doesn't interfere with page scrolling
- [ ] Step scrolls into view when breakpoint changes
## Architecture Notes
### Why Fixed Positioning?
The original inline injection approach had issues:
1. Required complex DOM manipulation to move the pane between steps
2. Caused layout shifts in the GitHub page
3. Made it difficult to maintain console scroll position
4. Required finding the correct insertion point for each step
Fixed positioning solves these:
1. Panel stays in place - no DOM movement needed
2. No layout shifts in the main page content
3. Panel state (console, variables) naturally preserved
4. Simpler CSS and JavaScript
### Layout Toggle UX
The toggle is a button group in the panel header showing both layout options:
- Sidebar icon (vertical split)
- Bottom icon (horizontal split)
The active layout is highlighted. Clicking the other option triggers `switchLayout()`.
### Breakpoint Indicator Implementation
Uses CSS class `.dap-current-step` added to the `<check-step>` element:
- `::after` pseudo-element creates the red accent bar
- `::before` pseudo-element creates the triangle pointer
- Direct child selectors style the step header background and border
The indicator is updated in `handleStoppedEvent()` when the debugger pauses at a new step.

View File

@@ -1,25 +1,142 @@
/**
* Content Script Styles
*
*
* Matches GitHub's Primer design system for seamless integration.
* Uses CSS custom properties for light/dark mode support.
*
* Supports two layout modes:
* - Bottom panel: Fixed at bottom of viewport (like browser DevTools)
* - Sidebar: Fixed on right side of viewport
*/
/* Debugger Pane Container */
/* ==========================================================================
Base Debugger Pane Styles
========================================================================== */
.dap-debugger-pane {
background-color: var(--bgColor-default, #0d1117);
border-color: var(--borderColor-default, #30363d) !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
font-size: 14px;
z-index: 999;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
}
/* Header */
.dap-header {
/* ==========================================================================
Bottom Panel Layout
========================================================================== */
/* Add padding to body when bottom panel is open so all content is accessible */
body.dap-bottom-panel-active {
padding-bottom: 360px !important; /* Slightly more than panel height for breathing room */
}
.dap-debugger-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 350px;
border-top: 1px solid var(--borderColor-default, #30363d);
display: flex;
flex-direction: column;
}
.dap-debugger-bottom .dap-header {
background-color: var(--bgColor-muted, #161b22);
flex-shrink: 0;
padding: 8px 12px;
}
.dap-header .octicon {
color: var(--fgColor-muted, #8b949e);
.dap-debugger-bottom .dap-content {
flex: 1;
min-height: 0;
display: flex;
}
.dap-debugger-bottom .dap-scopes {
width: 33%;
min-width: 200px;
max-width: 400px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--borderColor-default, #30363d);
}
.dap-debugger-bottom .dap-repl {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.dap-debugger-bottom .dap-controls {
gap: 4px;
}
/* ==========================================================================
Sidebar Layout
========================================================================== */
.dap-debugger-sidebar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 350px;
border-left: 1px solid var(--borderColor-default, #30363d);
display: flex;
flex-direction: column;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
}
.dap-debugger-sidebar .dap-header {
background-color: var(--bgColor-muted, #161b22);
flex-shrink: 0;
padding: 8px 12px;
}
.dap-debugger-sidebar .dap-scopes {
flex: 0 0 auto;
max-height: 40%;
min-height: 150px;
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--borderColor-default, #30363d);
}
.dap-debugger-sidebar .dap-repl {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.dap-debugger-sidebar .dap-controls {
background-color: var(--bgColor-muted, #161b22);
flex-shrink: 0;
gap: 8px;
justify-content: center;
}
/* ==========================================================================
Header Styles
========================================================================== */
.dap-header {
display: flex;
align-items: center;
border-bottom: 1px solid var(--borderColor-default, #30363d);
}
.dap-header .text-bold {
font-weight: 600;
color: var(--fgColor-default, #e6edf3);
}
.dap-header-right {
display: flex;
align-items: center;
}
.dap-step-info {
@@ -27,57 +144,135 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fgColor-muted, #8b949e);
font-size: 12px;
}
/* Status Labels */
.dap-status-label {
flex-shrink: 0;
/* ==========================================================================
Layout Toggle Buttons
========================================================================== */
.dap-layout-toggles {
display: flex;
gap: 2px;
background-color: var(--bgColor-inset, #010409);
border-radius: 6px;
padding: 2px;
}
.Label--attention {
background-color: #9e6a03 !important;
color: #ffffff !important;
.dap-layout-btn {
padding: 4px 6px !important;
background-color: transparent !important;
border: none !important;
color: var(--fgColor-muted, #8b949e) !important;
border-radius: 4px !important;
display: flex;
align-items: center;
justify-content: center;
}
.Label--success {
background-color: #238636 !important;
color: #ffffff !important;
.dap-layout-btn:hover {
background-color: var(--bgColor-neutral-muted, #6e768166) !important;
color: var(--fgColor-default, #e6edf3) !important;
}
.dap-layout-btn.active {
background-color: var(--bgColor-accent-muted, #388bfd26) !important;
color: var(--fgColor-accent, #58a6ff) !important;
}
.dap-layout-btn svg {
width: 14px;
height: 14px;
}
/* ==========================================================================
Close Button
========================================================================== */
.dap-close-btn {
padding: 4px 6px !important;
background-color: transparent !important;
border: none !important;
color: var(--fgColor-muted, #8b949e) !important;
display: flex;
align-items: center;
justify-content: center;
}
.Label--danger {
background-color: #da3633 !important;
color: #ffffff !important;
border: none !important;
.dap-close-btn:hover {
background-color: var(--bgColor-danger-muted, #da363326) !important;
color: var(--fgColor-danger, #f85149) !important;
}
.Label--secondary {
background-color: #30363d !important;
color: #8b949e !important;
border: none !important;
.dap-close-btn svg {
width: 16px;
height: 16px;
}
/* Content Area */
.dap-content {
min-height: 200px;
max-height: 400px;
/* ==========================================================================
Control Buttons
========================================================================== */
.dap-controls {
display: flex;
align-items: center;
padding: 8px;
}
/* Scopes Panel */
.dap-control-btn {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 !important;
margin-right: 4px;
}
.dap-control-btn:last-child {
margin-right: 0;
}
.dap-control-btn svg {
width: 16px;
height: 16px;
display: block;
margin: auto !important;
}
.dap-control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.dap-control-btn:not(:disabled):hover {
background-color: var(--bgColor-accent-muted, #388bfd26);
}
/* ==========================================================================
Scopes Panel
========================================================================== */
.dap-scopes {
border-color: var(--borderColor-default, #30363d) !important;
min-width: 150px;
overflow: hidden;
}
.dap-scope-header {
background-color: var(--bgColor-muted, #161b22);
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
padding: 8px;
border-bottom: 1px solid var(--borderColor-default, #30363d);
}
.dap-scope-tree {
font-size: 12px;
line-height: 1.6;
padding: 8px;
overflow-y: auto;
flex: 1;
}
/* Tree Nodes */
@@ -123,16 +318,23 @@
word-break: break-word;
}
/* REPL Console */
/* ==========================================================================
REPL Console
========================================================================== */
.dap-repl {
display: flex;
flex-direction: column;
overflow: hidden;
}
.dap-repl-header {
background-color: var(--bgColor-muted, #161b22);
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
padding: 8px;
border-bottom: 1px solid var(--borderColor-default, #30363d);
}
.dap-repl-output {
@@ -143,7 +345,7 @@
padding: 8px;
flex: 1;
overflow-y: auto;
min-height: 100px;
min-height: 60px;
}
.dap-output-input {
@@ -165,6 +367,8 @@
/* REPL Input */
.dap-repl-input {
flex-shrink: 0;
padding: 8px;
border-top: 1px solid var(--borderColor-default, #30363d);
}
.dap-repl-input input {
@@ -174,6 +378,9 @@
border-color: var(--borderColor-default, #30363d) !important;
color: var(--fgColor-default, #e6edf3) !important;
width: 100%;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--borderColor-default, #30363d);
}
.dap-repl-input input:focus {
@@ -191,51 +398,112 @@
color: var(--fgColor-muted, #8b949e);
}
/* Control Buttons */
.dap-controls {
background-color: var(--bgColor-muted, #161b22);
/* ==========================================================================
Breakpoint Indicator
========================================================================== */
/* Current step indicator - shown on the step where debugger is paused
Breakpoints are BEFORE steps, so we indicate the boundary above the step */
check-step.dap-current-step {
position: relative;
}
.dap-controls button {
min-width: 32px;
height: 28px;
/* Highlight the step row with a subtle background and red accent line at TOP */
check-step.dap-current-step > details,
check-step.dap-current-step > div {
position: relative;
}
check-step.dap-current-step > details > summary,
check-step.dap-current-step > div:first-child {
background: linear-gradient(90deg, rgba(248, 81, 73, 0.08) 0%, rgba(248, 81, 73, 0.02) 100%) !important;
border-top: 2px solid #f85149 !important;
}
/* Red accent bar at top of step - horizontal line showing "breakpoint here" */
check-step.dap-current-step::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
height: 2px;
background: linear-gradient(90deg, #f85149 0%, #da3633 100%);
}
/* Triangle pointer at top of step - points to the boundary between steps */
check-step.dap-current-step::before {
content: '';
position: absolute;
right: -8px;
top: 0;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-left: 10px solid #f85149;
z-index: 100;
filter: drop-shadow(2px 0 2px rgba(0, 0, 0, 0.2));
}
/* ==========================================================================
Debug Button in Header
========================================================================== */
.dap-debug-btn-container {
display: flex;
align-items: center;
margin-right: 8px;
}
.dap-debug-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 8px;
gap: 4px;
font-weight: 500;
padding: 5px 12px;
height: 32px;
}
.dap-controls button svg {
width: 14px;
height: 14px;
.dap-debug-btn-text {
font-size: var(--text-body-size-medium, .875rem);
}
.dap-controls button:disabled {
opacity: 0.4;
cursor: not-allowed;
.dap-debug-btn .octicon {
width: 16px;
height: 16px;
}
.dap-controls button:not(:disabled):hover {
.dap-debug-btn.selected {
background-color: var(--bgColor-accent-muted, #388bfd26);
border-color: var(--borderColor-accent-emphasis, #388bfd);
color: var(--fgColor-accent, #58a6ff);
}
.dap-step-counter {
flex-shrink: 0;
.dap-debug-btn:hover:not(:disabled) {
background-color: var(--bgColor-neutral-muted, #6e768166);
}
/* Utility Classes (in case GitHub's aren't loaded) */
/* ==========================================================================
Utility Classes
========================================================================== */
.d-flex { display: flex; }
.flex-column { flex-direction: column; }
.flex-items-center { align-items: center; }
.flex-auto { flex: 1 1 auto; }
.justify-content-center { justify-content: center; }
.p-2 { padding: 8px; }
.px-2 { padding-left: 8px; padding-right: 8px; }
.mx-2 { margin-left: 8px; margin-right: 8px; }
.mb-2 { margin-bottom: 8px; }
.ml-1 { margin-left: 4px; }
.ml-2 { margin-left: 8px; }
.ml-3 { margin-left: 16px; }
.mr-2 { margin-right: 8px; }
.mr-3 { margin-right: 16px; }
.ml-auto { margin-left: auto; }
.border { border: 1px solid var(--borderColor-default, #30363d); }
@@ -253,11 +521,19 @@
.color-fg-danger { color: var(--fgColor-danger, #f85149); }
.color-fg-default { color: var(--fgColor-default, #e6edf3); }
/* Light mode overrides */
/* ==========================================================================
Light Mode Overrides
========================================================================== */
@media (prefers-color-scheme: light) {
.dap-debugger-pane {
background-color: var(--bgColor-default, #ffffff);
border-color: var(--borderColor-default, #d0d7de) !important;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.dap-debugger-sidebar {
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
}
.dap-header,
@@ -279,6 +555,12 @@
.color-fg-muted {
color: var(--fgColor-muted, #656d76);
}
check-step.dap-current-step > details > summary,
check-step.dap-current-step > div:first-child {
background: linear-gradient(90deg, rgba(248, 81, 73, 0.1) 0%, rgba(248, 81, 73, 0.02) 100%) !important;
border-top: 2px solid #f85149 !important;
}
}
/* Respect GitHub's color mode data attribute */
@@ -286,6 +568,12 @@
html[data-color-mode="light"] .dap-debugger-pane {
background-color: #ffffff;
border-color: #d0d7de !important;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
[data-color-mode="light"] .dap-debugger-sidebar,
html[data-color-mode="light"] .dap-debugger-sidebar {
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
}
[data-color-mode="light"] .dap-header,
@@ -306,27 +594,17 @@ html[data-color-mode="light"] .dap-repl-input input {
background-color: #f6f8fa !important;
}
/* Debug Button in Header */
.dap-debug-btn-container {
display: flex;
align-items: center;
[data-color-mode="light"] .dap-layout-toggles,
html[data-color-mode="light"] .dap-layout-toggles {
background-color: #eaeef2;
}
.dap-debug-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 500;
}
.dap-debug-btn.selected {
background-color: var(--bgColor-accent-muted, #388bfd26);
border-color: var(--borderColor-accent-emphasis, #388bfd);
}
.dap-debug-btn:hover:not(:disabled) {
background-color: var(--bgColor-neutral-muted, #6e768166);
[data-color-mode="light"] check-step.dap-current-step > details > summary,
[data-color-mode="light"] check-step.dap-current-step > div:first-child,
html[data-color-mode="light"] check-step.dap-current-step > details > summary,
html[data-color-mode="light"] check-step.dap-current-step > div:first-child {
background: linear-gradient(90deg, rgba(248, 81, 73, 0.1) 0%, rgba(248, 81, 73, 0.02) 100%) !important;
border-top: 2px solid #f85149 !important;
}
/* Light mode for debug button */

View File

@@ -2,7 +2,7 @@
* Content Script - Debugger UI
*
* Injects the debugger pane into GitHub Actions job pages and handles
* all UI interactions.
* all UI interactions. Supports two layout modes: sidebar and bottom panel.
*/
// State
@@ -11,6 +11,12 @@ 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) {
@@ -27,6 +33,26 @@ 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
*/
@@ -84,33 +110,93 @@ function getAllSteps() {
}
/**
* Create the debugger pane HTML
* SVG Icons
*/
function createDebuggerPaneHTML() {
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">
<svg class="octicon mr-2" 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>
<span class="text-bold">Debugger</span>
<span class="dap-step-info color-fg-muted ml-2">Connecting...</span>
<span class="Label dap-status-label ml-auto">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" style="height: 300px;">
<div class="dap-content d-flex">
<!-- Scopes Panel -->
<div class="dap-scopes border-right overflow-auto" style="width: 33%;">
<div class="dap-scope-header p-2 text-bold border-bottom">Variables</div>
<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" style="width: 67%;">
<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>
@@ -123,24 +209,60 @@ function createDebuggerPaneHTML() {
</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 p-2 border-top">
<button class="btn btn-sm mr-2" data-action="reverseContinue" title="Reverse Continue (go to first checkpoint)" disabled>
<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>
</button>
<button class="btn btn-sm mr-2" data-action="stepBack" title="Step Back" disabled>
<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M2 2v12h2V2H2zm3 6 7 5V3L5 8z"/></svg>
</button>
<button class="btn btn-sm btn-primary mr-2" data-action="continue" title="Continue" disabled>
<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M4 2l10 6-10 6z"/></svg>
</button>
<button class="btn btn-sm mr-2" data-action="next" title="Step to Next" disabled>
<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M2 3l7 5-7 5V3zm7 5l5 0V2h2v12h-2V8.5l-5 0z"/></svg>
</button>
<span class="dap-step-counter color-fg-muted ml-auto text-small">
Not connected
</span>
<div class="dap-controls d-flex flex-items-center justify-content-center p-2 border-top">
${createControlButtonsHTML()}
</div>
`;
}
@@ -148,49 +270,126 @@ function createDebuggerPaneHTML() {
/**
* Inject debugger pane into the page
*/
function injectDebuggerPane() {
function injectDebuggerPane(layout = null) {
// Remove existing pane if any
const existing = document.querySelector('.dap-debugger-pane');
if (existing) existing.remove();
// Find where to inject
const stepsContainer = document.querySelector('check-steps');
if (!stepsContainer) {
console.warn('[Content] No check-steps container found');
return null;
}
const targetLayout = layout || currentLayout;
// Create pane
const pane = document.createElement('div');
pane.className = 'dap-debugger-pane mx-2 mb-2 border rounded-2';
pane.innerHTML = createDebuggerPaneHTML();
pane.className = `dap-debugger-pane dap-debugger-${targetLayout}`;
// Insert before the first real workflow step (skip "Set up job" at index 0)
const steps = stepsContainer.querySelectorAll('check-step');
const targetStep = steps.length > 1 ? steps[1] : stepsContainer.firstChild;
stepsContainer.insertBefore(pane, targetStep);
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;
}
/**
* Move debugger pane to before a specific step
* Switch between layouts
*/
function moveDebuggerPane(stepElement, stepName) {
if (!debuggerPane || !stepElement) return;
function switchLayout(newLayout) {
if (newLayout === currentLayout) return;
// Move the pane
stepElement.parentNode.insertBefore(debuggerPane, stepElement);
// Store current state before switching
const wasConnected = isConnected;
const replOutputContent = debuggerPane?.querySelector('.dap-repl-output')?.innerHTML;
const scopeTreeContent = debuggerPane?.querySelector('.dap-scope-tree')?.innerHTML;
// Update step info
const stepInfo = debuggerPane.querySelector('.dap-step-info');
if (stepInfo) {
stepInfo.textContent = `Paused before: ${stepName}`;
// 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');
}
/**
@@ -215,6 +414,22 @@ function setupPaneEventHandlers(pane) {
});
});
// 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) {
@@ -299,7 +514,7 @@ function appendOutput(text, type) {
function enableControls(enabled) {
if (!debuggerPane) return;
debuggerPane.querySelectorAll('.dap-controls button').forEach((btn) => {
debuggerPane.querySelectorAll('.dap-control-btn').forEach((btn) => {
btn.disabled = !enabled;
});
@@ -315,37 +530,42 @@ function enableControls(enabled) {
function updateStatus(status, extra) {
if (!debuggerPane) return;
const label = debuggerPane.querySelector('.dap-status-label');
if (label) {
label.textContent = status;
label.className = 'Label dap-status-label ml-auto ';
switch (status) {
case 'PAUSED':
label.classList.add('Label--attention');
break;
case 'RUNNING':
label.classList.add('Label--success');
break;
case 'TERMINATED':
case 'DISCONNECTED':
label.classList.add('Label--secondary');
break;
case 'ERROR':
label.classList.add('Label--danger');
break;
default:
label.classList.add('Label--secondary');
}
// Update step info text
const stepInfo = debuggerPane.querySelector('.dap-step-info');
if (stepInfo && extra) {
stepInfo.textContent = extra;
}
}
// Update step counter if extra info provided
if (extra) {
const counter = debuggerPane.querySelector('.dap-step-counter');
if (counter) {
counter.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');
});
}
/**
@@ -396,7 +616,7 @@ function createTreeNode(name, variablesReference, isExpandable, value) {
// Expand icon
const expandIcon = document.createElement('span');
expandIcon.className = 'dap-expand-icon';
expandIcon.textContent = isExpandable ? '\u25B6' : ' '; // or space
expandIcon.textContent = isExpandable ? '\u25B6' : ' '; // triangleright or space
content.appendChild(expandIcon);
// Name
@@ -433,7 +653,7 @@ async function toggleTreeNode(node) {
if (children) {
// Toggle visibility
children.hidden = !children.hidden;
expandIcon.textContent = children.hidden ? '\u25B6' : '\u25BC'; // ▶ or ▼
expandIcon.textContent = children.hidden ? '\u25B6' : '\u25BC'; // triangleright or triangledown
return;
}
@@ -461,10 +681,10 @@ async function toggleTreeNode(node) {
}
node.appendChild(childContainer);
expandIcon.textContent = '\u25BC'; //
expandIcon.textContent = '\u25BC'; // triangledown
} catch (error) {
console.error('[Content] Failed to load variables:', error);
expandIcon.textContent = '\u25B6'; //
expandIcon.textContent = '\u25B6'; // triangleright
}
}
@@ -487,7 +707,6 @@ async function handleStoppedEvent(body) {
currentFrameId = currentFrame.id;
// Strip result indicator from step name for DOM lookup
// e.g., "Run tests [running]" -> "Run tests"
const rawStepName = stripResultIndicator(currentFrame.name);
let stepElement = findStepByName(rawStepName);
@@ -497,14 +716,15 @@ async function handleStoppedEvent(body) {
stepElement = findStepByNumber(currentFrame.line + 1);
}
// Update breakpoint indicator
if (stepElement) {
moveDebuggerPane(stepElement, rawStepName);
updateBreakpointIndicator(stepElement);
}
// Update step counter
const counter = debuggerPane?.querySelector('.dap-step-counter');
if (counter) {
counter.textContent = `Step ${currentFrame.line || currentFrame.id} of ${stackTrace.stackFrames.length}`;
// Update step info in header
const stepInfo = debuggerPane?.querySelector('.dap-step-info');
if (stepInfo) {
stepInfo.textContent = `Paused before: ${rawStepName}`;
}
// Load scopes
@@ -531,13 +751,9 @@ function handleOutputEvent(body) {
*/
function handleTerminatedEvent() {
isConnected = false;
updateStatus('TERMINATED');
updateStatus('TERMINATED', 'Session ended');
enableControls(false);
const stepInfo = debuggerPane?.querySelector('.dap-step-info');
if (stepInfo) {
stepInfo.textContent = 'Session ended';
}
clearBreakpointIndicator();
}
/**
@@ -552,25 +768,24 @@ async function loadCurrentDebugState() {
const currentFrame = stackTrace.stackFrames[0];
currentFrameId = currentFrame.id;
// Move pane to current step
// 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) {
moveDebuggerPane(stepElement, rawStepName);
updateBreakpointIndicator(stepElement);
}
// Update step counter
const counter = debuggerPane.querySelector('.dap-step-counter');
if (counter) {
counter.textContent = `Step ${currentFrame.line || currentFrame.id} of ${stackTrace.stackFrames.length}`;
// Update step info in header
const stepInfo = debuggerPane.querySelector('.dap-step-info');
if (stepInfo) {
stepInfo.textContent = `Paused before: ${rawStepName}`;
}
// Load scopes
@@ -590,11 +805,7 @@ function handleStatusChange(status) {
switch (status) {
case 'connected':
isConnected = true;
updateStatus('CONNECTED');
const stepInfo = debuggerPane?.querySelector('.dap-step-info');
if (stepInfo) {
stepInfo.textContent = 'Waiting for debug event...';
}
updateStatus('CONNECTED', 'Waiting for debug event...');
break;
case 'paused':
@@ -606,20 +817,22 @@ function handleStatusChange(status) {
case 'running':
isConnected = true;
updateStatus('RUNNING');
updateStatus('RUNNING', 'Running...');
enableControls(false);
break;
case 'disconnected':
isConnected = false;
updateStatus('DISCONNECTED');
updateStatus('DISCONNECTED', 'Disconnected');
enableControls(false);
clearBreakpointIndicator();
break;
case 'error':
isConnected = false;
updateStatus('ERROR');
updateStatus('ERROR', 'Connection error');
enableControls(false);
clearBreakpointIndicator();
break;
}
}
@@ -656,7 +869,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
* Inject debug button into GitHub Actions UI header
*/
function injectDebugButton() {
const container = document.querySelector('.js-check-run-search');
// 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
}
@@ -665,25 +885,20 @@ function injectDebugButton() {
buttonContainer.className = 'ml-2 dap-debug-btn-container';
buttonContainer.innerHTML = `
<button type="button" class="btn btn-sm dap-debug-btn" title="Toggle DAP Debugger">
<svg viewBox="0 0 16 16" width="16" height="16" class="octicon mr-1" style="vertical-align: text-bottom;">
<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>
Debug
${Icons.bug}
<span class="ml-1 dap-debug-btn-text">Debug</span>
</button>
`;
const button = buttonContainer.querySelector('button');
button.addEventListener('click', () => {
button.addEventListener('click', async () => {
let pane = document.querySelector('.dap-debugger-pane');
if (pane) {
// Toggle visibility
pane.hidden = !pane.hidden;
button.classList.toggle('selected', !pane.hidden);
// Close pane
closeDebuggerPane();
} else {
// Create and show pane
// Load preference and create pane
await loadLayoutPreference();
pane = injectDebuggerPane();
if (pane) {
button.classList.add('selected');
@@ -705,9 +920,12 @@ function injectDebugButton() {
/**
* Initialize content script
*/
function init() {
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) {
@@ -731,8 +949,6 @@ function init() {
// Check current connection status
chrome.runtime.sendMessage({ type: 'get-status' }, async (response) => {
if (response && response.status) {
handleStatusChange(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');
@@ -742,6 +958,8 @@ function init() {
if (btn) btn.classList.add('selected');
}
handleStatusChange(response.status);
// If already paused, load the current debug state
if (response.status === 'paused') {
await loadCurrentDebugState();