editing jobs

This commit is contained in:
Francesco Renzi
2026-01-21 22:30:19 +00:00
committed by GitHub
parent 9bc9aff86f
commit 008594a3ee
14 changed files with 6450 additions and 9 deletions

View File

@@ -613,3 +613,411 @@ html[data-color-mode="light"] .dap-debug-btn.selected {
background-color: #ddf4ff;
border-color: #54aeff;
}
/* ==========================================================================
Steps Panel
========================================================================== */
.dap-steps-panel {
display: flex;
flex-direction: column;
border-right: 1px solid var(--borderColor-default, #30363d);
min-width: 200px;
max-width: 280px;
width: 25%;
}
.dap-debugger-sidebar .dap-steps-panel {
border-right: none;
border-bottom: 1px solid var(--borderColor-default, #30363d);
max-width: none;
width: auto;
max-height: 35%;
min-height: 120px;
}
.dap-steps-header {
background-color: var(--bgColor-muted, #161b22);
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.dap-steps-list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.dap-steps-empty {
font-size: 12px;
}
.dap-steps-footer {
flex-shrink: 0;
background-color: var(--bgColor-muted, #161b22);
}
.dap-add-step-btn,
.dap-export-btn {
display: inline-flex;
align-items: center;
gap: 4px;
}
.dap-add-step-btn svg,
.dap-export-btn svg {
width: 14px;
height: 14px;
}
.dap-add-step-btn:disabled,
.dap-export-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Step Items */
.dap-step-item {
display: flex;
align-items: center;
padding: 4px 8px;
font-size: 12px;
cursor: default;
gap: 4px;
border-left: 3px solid transparent;
}
.dap-step-item:hover {
background-color: var(--bgColor-muted, #161b22);
}
.dap-step-item.completed {
opacity: 0.6;
}
.dap-step-item.current {
background-color: var(--bgColor-accent-muted, #388bfd26);
border-left-color: var(--fgColor-accent, #58a6ff);
}
.dap-step-item.pending[data-editable="true"] {
cursor: pointer;
}
.dap-step-status-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.dap-step-status-icon svg {
width: 12px;
height: 12px;
}
.dap-step-status-icon.completed {
color: var(--fgColor-success, #3fb950);
}
.dap-step-status-icon.current {
color: var(--fgColor-accent, #58a6ff);
}
.dap-step-status-icon.pending {
color: var(--fgColor-muted, #8b949e);
}
.dap-step-index {
color: var(--fgColor-muted, #8b949e);
font-size: 11px;
min-width: 20px;
}
.dap-step-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fgColor-default, #e6edf3);
}
.dap-step-badge {
font-size: 10px;
padding: 1px 4px;
border-radius: 3px;
font-weight: 500;
flex-shrink: 0;
}
.dap-step-badge-added {
background-color: var(--bgColor-success-muted, #2ea04326);
color: var(--fgColor-success, #3fb950);
}
.dap-step-badge-modified {
background-color: var(--bgColor-attention-muted, #bb800926);
color: var(--fgColor-attention, #d29922);
}
.dap-step-type {
color: var(--fgColor-muted, #8b949e);
font-size: 10px;
flex-shrink: 0;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dap-step-menu-btn {
background: none;
border: none;
color: var(--fgColor-muted, #8b949e);
cursor: pointer;
padding: 2px 6px;
border-radius: 3px;
font-weight: bold;
letter-spacing: 1px;
opacity: 0;
transition: opacity 0.1s;
}
.dap-step-item:hover .dap-step-menu-btn {
opacity: 1;
}
.dap-step-menu-btn:hover {
background-color: var(--bgColor-neutral-muted, #6e768166);
color: var(--fgColor-default, #e6edf3);
}
/* ==========================================================================
Context Menu
========================================================================== */
.dap-context-menu {
background-color: var(--bgColor-default, #0d1117);
border: 1px solid var(--borderColor-default, #30363d);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
z-index: 10000;
min-width: 140px;
padding: 4px 0;
}
.dap-context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 12px;
color: var(--fgColor-default, #e6edf3);
cursor: pointer;
}
.dap-context-menu-item:hover {
background-color: var(--bgColor-accent-muted, #388bfd26);
}
.dap-context-menu-item.danger {
color: var(--fgColor-danger, #f85149);
}
.dap-context-menu-item.danger:hover {
background-color: var(--bgColor-danger-muted, #da363326);
}
.dap-context-menu-item svg {
width: 14px;
height: 14px;
}
.dap-context-menu-divider {
height: 1px;
background-color: var(--borderColor-default, #30363d);
margin: 4px 0;
}
/* ==========================================================================
Modal
========================================================================== */
.dap-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
}
.dap-modal {
background-color: var(--bgColor-default, #0d1117);
border: 1px solid var(--borderColor-default, #30363d);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
width: 90%;
max-width: 500px;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.dap-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--borderColor-default, #30363d);
}
.dap-modal-header .text-bold {
font-size: 14px;
color: var(--fgColor-default, #e6edf3);
}
.dap-modal-close-btn {
background: none !important;
border: none !important;
color: var(--fgColor-muted, #8b949e) !important;
padding: 4px !important;
cursor: pointer;
}
.dap-modal-close-btn:hover {
color: var(--fgColor-default, #e6edf3) !important;
}
.dap-modal-close-btn svg {
width: 16px;
height: 16px;
}
.dap-modal-content {
padding: 16px;
overflow-y: auto;
flex: 1;
}
.dap-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--borderColor-default, #30363d);
}
/* Form elements */
.dap-form-group {
margin-bottom: 12px;
}
.dap-form-group:last-child {
margin-bottom: 0;
}
.dap-label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--fgColor-default, #e6edf3);
margin-bottom: 4px;
}
.dap-modal .form-control {
width: 100%;
background-color: var(--bgColor-inset, #010409) !important;
border-color: var(--borderColor-default, #30363d) !important;
color: var(--fgColor-default, #e6edf3) !important;
font-size: 12px;
padding: 6px 8px;
border-radius: 6px;
}
.dap-modal .form-control:focus {
border-color: var(--focus-outlineColor, #1f6feb) !important;
outline: none;
box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.3);
}
.dap-modal select.form-control {
appearance: auto;
}
.dap-modal textarea.form-control {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
resize: vertical;
}
/* Export Modal */
.dap-export-stats {
font-size: 12px;
}
.dap-export-yaml {
background-color: var(--bgColor-inset, #010409);
border: 1px solid var(--borderColor-default, #30363d);
border-radius: 6px;
overflow: auto;
max-height: 300px;
}
.dap-yaml-content {
margin: 0;
padding: 12px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 11px;
line-height: 1.5;
color: var(--fgColor-default, #e6edf3);
white-space: pre;
}
/* ==========================================================================
Light Mode Overrides for New Components
========================================================================== */
[data-color-mode="light"] .dap-context-menu,
html[data-color-mode="light"] .dap-context-menu {
background-color: #ffffff;
border-color: #d0d7de;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
[data-color-mode="light"] .dap-context-menu-item,
html[data-color-mode="light"] .dap-context-menu-item {
color: #1f2328;
}
[data-color-mode="light"] .dap-modal,
html[data-color-mode="light"] .dap-modal {
background-color: #ffffff;
border-color: #d0d7de;
}
[data-color-mode="light"] .dap-modal-header .text-bold,
html[data-color-mode="light"] .dap-modal-header .text-bold {
color: #1f2328;
}
[data-color-mode="light"] .dap-step-name,
html[data-color-mode="light"] .dap-step-name {
color: #1f2328;
}
[data-color-mode="light"] .dap-export-yaml,
html[data-color-mode="light"] .dap-export-yaml {
background-color: #f6f8fa;
}
[data-color-mode="light"] .dap-yaml-content,
html[data-color-mode="light"] .dap-yaml-content {
color: #1f2328;
}

View File

@@ -13,6 +13,9 @@ let replHistory = [];
let replHistoryIndex = -1;
let currentLayout = 'bottom'; // 'bottom' | 'sidebar'
let currentStepElement = null; // Track current step for breakpoint indicator
let stepsList = []; // Cached steps from DAP
let activeContextMenu = null; // Track open context menu
let activeModal = null; // Track open modal
// Layout constants
const BOTTOM_PANEL_HEIGHT = 350;
@@ -132,6 +135,16 @@ const Icons = {
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>`,
plus: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>`,
download: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Zm-1.97-7.22a.75.75 0 0 0 1.06 1.06L7 2.69v8.56a.75.75 0 0 0 1.5 0V2.69l5.16 5.15a.75.75 0 1 0 1.06-1.06l-6.5-6.5a.75.75 0 0 0-1.06 0l-6.5 6.5Z" transform="rotate(180 8 8)"/></svg>`,
check: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>`,
play: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"/></svg>`,
clock: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5a.75.75 0 0 1 1.5 0Z"/></svg>`,
pencil: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"/></svg>`,
trash: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15ZM6.5 1.75V3h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z"/></svg>`,
arrowUp: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M3.47 7.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018L9 4.81v7.44a.75.75 0 0 1-1.5 0V4.81L4.53 7.78a.75.75 0 0 1-1.06 0Z"/></svg>`,
arrowDown: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M13.03 8.22a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.47 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018l2.97 2.97V3.75a.75.75 0 0 1 1.5 0v7.44l2.97-2.97a.75.75 0 0 1 1.06 0Z"/></svg>`,
copy: `<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25ZM5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>`,
};
/**
@@ -155,6 +168,31 @@ function createControlButtonsHTML(compact = false) {
`;
}
/**
* Create the steps panel HTML
*/
function createStepsPanelHTML() {
return `
<div class="dap-steps-panel">
<div class="dap-steps-header p-2 text-bold border-bottom d-flex flex-items-center">
<span>Steps</span>
<button class="btn btn-sm dap-add-step-btn ml-auto" title="Add Step" disabled>
${Icons.plus}
</button>
</div>
<div class="dap-steps-list overflow-auto flex-auto">
<div class="dap-steps-empty p-2 color-fg-muted">Connect to view steps</div>
</div>
<div class="dap-steps-footer p-2 border-top">
<button class="btn btn-sm dap-export-btn" title="Export Changes" disabled>
${Icons.download}
<span class="ml-1">Export</span>
</button>
</div>
</div>
`;
}
/**
* Create the bottom panel HTML structure
*/
@@ -185,6 +223,9 @@ function createBottomPaneHTML() {
</div>
<div class="dap-content d-flex">
<!-- Steps Panel -->
${createStepsPanelHTML()}
<!-- 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">
@@ -236,6 +277,9 @@ function createSidebarPaneHTML() {
</div>
</div>
<!-- Steps Panel -->
${createStepsPanelHTML()}
<!-- 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">
@@ -382,6 +426,10 @@ function closeDebuggerPane() {
// Remove body padding class
document.body.classList.remove('dap-bottom-panel-active');
// Close any open context menu or modal
closeContextMenu();
closeModal();
if (debuggerPane) {
debuggerPane.remove();
debuggerPane = null;
@@ -392,6 +440,628 @@ function closeDebuggerPane() {
if (btn) btn.classList.remove('selected');
}
// ==========================================================================
// Step Manipulation UI
// ==========================================================================
/**
* Get status icon for step
*/
function getStepStatusIcon(status) {
switch (status) {
case 'completed':
return `<span class="dap-step-status-icon completed">${Icons.check}</span>`;
case 'current':
return `<span class="dap-step-status-icon current">${Icons.play}</span>`;
case 'pending':
default:
return `<span class="dap-step-status-icon pending">${Icons.clock}</span>`;
}
}
/**
* Render step list from data
*/
function renderStepList(steps) {
const container = debuggerPane?.querySelector('.dap-steps-list');
if (!container) return;
if (!steps || steps.length === 0) {
container.innerHTML = '<div class="dap-steps-empty p-2 color-fg-muted">No steps available</div>';
return;
}
stepsList = steps;
const html = steps.map((step) => {
const statusIcon = getStepStatusIcon(step.status);
const changeBadge = step.change ? `<span class="dap-step-badge dap-step-badge-${step.change.toLowerCase()}">[${step.change}]</span>` : '';
const typeLabel = step.type === 'uses' ? step.typeDetail || step.type : step.type;
const isPending = step.status === 'pending';
return `
<div class="dap-step-item ${step.status}" data-index="${step.index}" ${isPending ? 'data-editable="true"' : ''}>
${statusIcon}
<span class="dap-step-index">${step.index}.</span>
<span class="dap-step-name" title="${escapeHtml(step.name)}">${escapeHtml(step.name)}</span>
${changeBadge}
<span class="dap-step-type">${escapeHtml(typeLabel)}</span>
${isPending ? `<button class="dap-step-menu-btn" title="Step actions">...</button>` : ''}
</div>
`;
}).join('');
container.innerHTML = html;
// Add click handlers for step menu buttons
container.querySelectorAll('.dap-step-menu-btn').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const stepItem = btn.closest('.dap-step-item');
const index = parseInt(stepItem.dataset.index);
showStepContextMenu(index, e);
});
});
}
/**
* Load steps from DAP server
*/
async function loadSteps() {
try {
const response = await sendDapRequest('evaluate', {
expression: JSON.stringify({ cmd: 'step.list', verbose: false }),
frameId: currentFrameId,
context: 'repl',
});
if (response.result) {
try {
const result = JSON.parse(response.result);
if (result.Success && result.Result) {
renderStepList(result.Result);
enableStepControls(true);
return;
}
} catch (parseErr) {
// Response might not be JSON, that's ok
}
}
// Fallback: show empty state
renderStepList([]);
} catch (error) {
console.error('[Content] Failed to load steps:', error);
const container = debuggerPane?.querySelector('.dap-steps-list');
if (container) {
container.innerHTML = '<div class="dap-steps-empty p-2 color-fg-muted">Failed to load steps</div>';
}
}
}
/**
* Enable/disable step manipulation controls
*/
function enableStepControls(enabled) {
if (!debuggerPane) return;
const addBtn = debuggerPane.querySelector('.dap-add-step-btn');
const exportBtn = debuggerPane.querySelector('.dap-export-btn');
if (addBtn) addBtn.disabled = !enabled;
if (exportBtn) exportBtn.disabled = !enabled;
}
/**
* Send step command via JSON API
*/
async function sendStepCommand(cmd, options = {}) {
const command = { cmd, ...options };
try {
const response = await sendDapRequest('evaluate', {
expression: JSON.stringify(command),
frameId: currentFrameId,
context: 'repl',
});
if (response.result) {
try {
return JSON.parse(response.result);
} catch (e) {
return { Success: false, Error: 'PARSE_ERROR', Message: response.result };
}
}
return { Success: false, Error: 'NO_RESPONSE', Message: 'No response from server' };
} catch (error) {
return { Success: false, Error: 'REQUEST_FAILED', Message: error.message };
}
}
/**
* Show context menu for a step
*/
function showStepContextMenu(stepIndex, event) {
closeContextMenu();
const step = stepsList.find((s) => s.index === stepIndex);
if (!step || step.status !== 'pending') return;
const menu = document.createElement('div');
menu.className = 'dap-context-menu';
menu.innerHTML = `
<div class="dap-context-menu-item" data-action="edit">
${Icons.pencil}
<span>Edit</span>
</div>
<div class="dap-context-menu-item" data-action="moveUp">
${Icons.arrowUp}
<span>Move Up</span>
</div>
<div class="dap-context-menu-item" data-action="moveDown">
${Icons.arrowDown}
<span>Move Down</span>
</div>
<div class="dap-context-menu-divider"></div>
<div class="dap-context-menu-item danger" data-action="delete">
${Icons.trash}
<span>Delete</span>
</div>
`;
// Position menu
const rect = event.target.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = `${rect.bottom + 4}px`;
menu.style.left = `${rect.left}px`;
// Add event handlers
menu.querySelectorAll('.dap-context-menu-item').forEach((item) => {
item.addEventListener('click', async () => {
const action = item.dataset.action;
closeContextMenu();
await handleStepAction(action, stepIndex);
});
});
document.body.appendChild(menu);
activeContextMenu = menu;
// Close menu on outside click
setTimeout(() => {
document.addEventListener('click', closeContextMenuOnOutsideClick);
}, 0);
}
/**
* Close context menu
*/
function closeContextMenu() {
if (activeContextMenu) {
activeContextMenu.remove();
activeContextMenu = null;
}
document.removeEventListener('click', closeContextMenuOnOutsideClick);
}
/**
* Close context menu on outside click
*/
function closeContextMenuOnOutsideClick(e) {
if (activeContextMenu && !activeContextMenu.contains(e.target)) {
closeContextMenu();
}
}
/**
* Handle step action from context menu
*/
async function handleStepAction(action, stepIndex) {
switch (action) {
case 'edit':
showEditStepDialog(stepIndex);
break;
case 'moveUp':
await moveStep(stepIndex, 'before', stepIndex - 1);
break;
case 'moveDown':
await moveStep(stepIndex, 'after', stepIndex + 1);
break;
case 'delete':
await deleteStep(stepIndex);
break;
}
}
/**
* Move a step
*/
async function moveStep(fromIndex, positionType, targetIndex) {
const position = {};
position[positionType] = targetIndex;
const result = await sendStepCommand('step.move', { from: fromIndex, position });
if (result.Success) {
appendOutput(`Step moved successfully`, 'result');
await loadSteps();
} else {
appendOutput(`Failed to move step: ${result.Message || result.Error}`, 'error');
}
}
/**
* Delete a step
*/
async function deleteStep(stepIndex) {
const step = stepsList.find((s) => s.index === stepIndex);
if (!step) return;
// Simple confirmation
if (!confirm(`Delete step "${step.name}"?`)) return;
const result = await sendStepCommand('step.remove', { index: stepIndex });
if (result.Success) {
appendOutput(`Step removed successfully`, 'result');
await loadSteps();
} else {
appendOutput(`Failed to remove step: ${result.Message || result.Error}`, 'error');
}
}
/**
* Close modal
*/
function closeModal() {
if (activeModal) {
activeModal.remove();
activeModal = null;
}
}
/**
* Create modal wrapper
*/
function createModal(title, content, buttons = []) {
closeModal();
const modal = document.createElement('div');
modal.className = 'dap-modal-overlay';
modal.innerHTML = `
<div class="dap-modal">
<div class="dap-modal-header">
<span class="text-bold">${escapeHtml(title)}</span>
<button class="btn btn-sm dap-modal-close-btn">${Icons.close}</button>
</div>
<div class="dap-modal-content">
${content}
</div>
<div class="dap-modal-footer">
${buttons.map((btn) => `<button class="btn btn-sm ${btn.primary ? 'btn-primary' : ''}" data-action="${btn.action}">${btn.label}</button>`).join('')}
</div>
</div>
`;
// Close on overlay click
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
// Close button
modal.querySelector('.dap-modal-close-btn').addEventListener('click', closeModal);
document.body.appendChild(modal);
activeModal = modal;
return modal;
}
/**
* Show Add Step dialog
*/
function showAddStepDialog() {
const content = `
<div class="dap-form-group">
<label class="dap-label">Step Type</label>
<select class="form-control dap-step-type-select">
<option value="run">Run (shell command)</option>
<option value="uses">Uses (action)</option>
</select>
</div>
<div class="dap-form-group dap-run-fields">
<label class="dap-label">Script</label>
<textarea class="form-control dap-script-input" rows="4" placeholder="echo 'Hello World'"></textarea>
</div>
<div class="dap-form-group dap-run-fields">
<label class="dap-label">Shell (optional)</label>
<select class="form-control dap-shell-select">
<option value="">Default</option>
<option value="bash">bash</option>
<option value="sh">sh</option>
<option value="pwsh">pwsh</option>
<option value="powershell">powershell</option>
<option value="cmd">cmd</option>
</select>
</div>
<div class="dap-form-group dap-uses-fields" style="display: none;">
<label class="dap-label">Action</label>
<input type="text" class="form-control dap-action-input" placeholder="actions/checkout@v4">
</div>
<div class="dap-form-group dap-uses-fields" style="display: none;">
<label class="dap-label">With (key=value, one per line)</label>
<textarea class="form-control dap-with-input" rows="3" placeholder="node-version=20&#10;cache=npm"></textarea>
</div>
<div class="dap-form-group">
<label class="dap-label">Name (optional)</label>
<input type="text" class="form-control dap-name-input" placeholder="Step name">
</div>
<div class="dap-form-group">
<label class="dap-label">Position</label>
<select class="form-control dap-position-select">
<option value="last">At end (default)</option>
<option value="first">At first pending position</option>
<option value="after">After current step</option>
</select>
</div>
`;
const modal = createModal('Add Step', content, [
{ label: 'Cancel', action: 'cancel' },
{ label: 'Add Step', action: 'add', primary: true },
]);
// Type select handler
const typeSelect = modal.querySelector('.dap-step-type-select');
const runFields = modal.querySelectorAll('.dap-run-fields');
const usesFields = modal.querySelectorAll('.dap-uses-fields');
typeSelect.addEventListener('change', () => {
const isRun = typeSelect.value === 'run';
runFields.forEach((f) => (f.style.display = isRun ? '' : 'none'));
usesFields.forEach((f) => (f.style.display = isRun ? 'none' : ''));
});
// Button handlers
modal.querySelectorAll('[data-action]').forEach((btn) => {
btn.addEventListener('click', async () => {
const action = btn.dataset.action;
if (action === 'cancel') {
closeModal();
return;
}
if (action === 'add') {
await handleAddStep(modal);
}
});
});
}
/**
* Handle add step form submission
*/
async function handleAddStep(modal) {
const type = modal.querySelector('.dap-step-type-select').value;
const name = modal.querySelector('.dap-name-input').value.trim() || undefined;
const positionSelect = modal.querySelector('.dap-position-select').value;
let position = {};
if (positionSelect === 'first') {
position.first = true;
} else if (positionSelect === 'after') {
// After current step - find current step index
const currentStep = stepsList.find((s) => s.status === 'current');
if (currentStep) {
position.after = currentStep.index;
} else {
position.last = true;
}
} else {
position.last = true;
}
let result;
if (type === 'run') {
const script = modal.querySelector('.dap-script-input').value;
const shell = modal.querySelector('.dap-shell-select').value || undefined;
if (!script.trim()) {
alert('Script is required');
return;
}
result = await sendStepCommand('step.add', {
type: 'run',
script,
name,
shell,
position,
});
} else {
const action = modal.querySelector('.dap-action-input').value.trim();
const withText = modal.querySelector('.dap-with-input').value.trim();
if (!action) {
alert('Action is required');
return;
}
// Parse with inputs
const withInputs = {};
if (withText) {
withText.split('\n').forEach((line) => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
withInputs[key.trim()] = valueParts.join('=').trim();
}
});
}
result = await sendStepCommand('step.add', {
type: 'uses',
action,
name,
with: Object.keys(withInputs).length > 0 ? withInputs : undefined,
position,
});
}
if (result.Success) {
closeModal();
appendOutput(`Step added at position ${result.Result?.index || 'end'}`, 'result');
await loadSteps();
} else {
appendOutput(`Failed to add step: ${result.Message || result.Error}`, 'error');
}
}
/**
* Show Edit Step dialog
*/
function showEditStepDialog(stepIndex) {
const step = stepsList.find((s) => s.index === stepIndex);
if (!step) return;
const isRun = step.type === 'run';
const content = `
<div class="dap-form-group">
<label class="dap-label">Step Type</label>
<input type="text" class="form-control" value="${escapeHtml(step.type)}" disabled>
</div>
${isRun ? `
<div class="dap-form-group">
<label class="dap-label">Script</label>
<textarea class="form-control dap-edit-script-input" rows="4">${escapeHtml(step.typeDetail || '')}</textarea>
</div>
` : `
<div class="dap-form-group">
<label class="dap-label">Action</label>
<input type="text" class="form-control" value="${escapeHtml(step.typeDetail || '')}" disabled>
</div>
`}
<div class="dap-form-group">
<label class="dap-label">Name</label>
<input type="text" class="form-control dap-edit-name-input" value="${escapeHtml(step.name)}">
</div>
<div class="dap-form-group">
<label class="dap-label">Condition (optional)</label>
<input type="text" class="form-control dap-edit-condition-input" placeholder="success()">
</div>
`;
const modal = createModal(`Edit Step ${stepIndex}`, content, [
{ label: 'Cancel', action: 'cancel' },
{ label: 'Save Changes', action: 'save', primary: true },
]);
// Button handlers
modal.querySelectorAll('[data-action]').forEach((btn) => {
btn.addEventListener('click', async () => {
const action = btn.dataset.action;
if (action === 'cancel') {
closeModal();
return;
}
if (action === 'save') {
await handleEditStep(modal, stepIndex, isRun);
}
});
});
}
/**
* Handle edit step form submission
*/
async function handleEditStep(modal, stepIndex, isRun) {
const name = modal.querySelector('.dap-edit-name-input').value.trim() || undefined;
const condition = modal.querySelector('.dap-edit-condition-input')?.value.trim() || undefined;
const options = { index: stepIndex };
if (name) options.name = name;
if (condition) options.if = condition;
if (isRun) {
const script = modal.querySelector('.dap-edit-script-input')?.value;
if (script) options.script = script;
}
const result = await sendStepCommand('step.edit', options);
if (result.Success) {
closeModal();
appendOutput(`Step ${stepIndex} updated`, 'result');
await loadSteps();
} else {
appendOutput(`Failed to edit step: ${result.Message || result.Error}`, 'error');
}
}
/**
* Show Export modal
*/
async function showExportModal() {
const result = await sendStepCommand('step.export', { changesOnly: false, withComments: true });
if (!result.Success) {
appendOutput(`Failed to export: ${result.Message || result.Error}`, 'error');
return;
}
const yaml = result.Message || result.Result?.yaml || '';
const stats = result.Result || {};
const content = `
<div class="dap-export-stats mb-2 color-fg-muted">
Total steps: ${stats.totalSteps || 0} |
Added: ${stats.addedCount || 0} |
Modified: ${stats.modifiedCount || 0}
</div>
<div class="dap-export-yaml">
<pre class="dap-yaml-content">${escapeHtml(yaml)}</pre>
</div>
`;
const modal = createModal('Export Steps', content, [
{ label: 'Close', action: 'close' },
{ label: 'Copy to Clipboard', action: 'copy', primary: true },
]);
// Store YAML for copy
modal.dataset.yaml = yaml;
// Button handlers
modal.querySelectorAll('[data-action]').forEach((btn) => {
btn.addEventListener('click', async () => {
const action = btn.dataset.action;
if (action === 'close') {
closeModal();
return;
}
if (action === 'copy') {
try {
await navigator.clipboard.writeText(modal.dataset.yaml);
btn.textContent = 'Copied!';
setTimeout(() => {
btn.innerHTML = `${Icons.copy} Copy to Clipboard`;
}, 2000);
} catch (err) {
appendOutput('Failed to copy to clipboard', 'error');
}
}
});
});
}
/**
* Setup event handlers for debugger pane
*/
@@ -435,6 +1105,18 @@ function setupPaneEventHandlers(pane) {
if (input) {
input.addEventListener('keydown', handleReplKeydown);
}
// Add Step button
const addStepBtn = pane.querySelector('.dap-add-step-btn');
if (addStepBtn) {
addStepBtn.addEventListener('click', showAddStepDialog);
}
// Export button
const exportBtn = pane.querySelector('.dap-export-btn');
if (exportBtn) {
exportBtn.addEventListener('click', showExportModal);
}
}
/**
@@ -522,6 +1204,9 @@ function enableControls(enabled) {
if (input) {
input.disabled = !enabled;
}
// Also enable step controls when debugger is enabled
enableStepControls(enabled);
}
/**
@@ -729,6 +1414,9 @@ async function handleStoppedEvent(body) {
// Load scopes
await loadScopes(currentFrame.id);
// Load steps for step manipulation panel
await loadSteps();
}
} catch (error) {
console.error('[Content] Failed to get stack trace:', error);
@@ -790,6 +1478,9 @@ async function loadCurrentDebugState() {
// Load scopes
await loadScopes(currentFrame.id);
// Load steps for step manipulation panel
await loadSteps();
}
} catch (error) {
console.error('[Content] Failed to load current debug state:', error);