Add parse/route step types, wider modal, automated link troubleshooter

- Add 'parse' step type: reads KEY=VALUE lines from last command output into execution state
- Add 'route' step type: evaluates JS conditions list, auto-jumps to first match with no user input
- Support condition field on parse steps (skip when conditional execute was also skipped)
- Widen execution detail modal to min(1100px, 96vw) to eliminate horizontal scrolling
- Show last command output in prompt boxes so user has context before clicking
- Add formatLogEntry support for parse_complete and route_taken log actions
- Apply applyParams() substitution to prompt messages ({{server_name}}, {{iface}}, etc.)
- Fix prompt button onclick using data-opt attribute to avoid JSON double-quote breakage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 19:29:28 -04:00
parent 2290d52f8b
commit 0990e5b807
2 changed files with 97 additions and 5 deletions

View File

@@ -474,9 +474,9 @@
padding: 0; padding: 0;
border: 3px double var(--terminal-green); border: 3px double var(--terminal-green);
border-radius: 0; border-radius: 0;
max-width: 600px; max-width: min(1100px, 96vw);
width: 90%; width: 96vw;
max-height: 85vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 0 30px rgba(0, 255, 65, 0.3); box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
position: relative; position: relative;
@@ -702,6 +702,28 @@
border-color: #330000; border-color: #330000;
} }
/* parse_complete and route_taken log entries */
.log-parse-table {
display: grid;
grid-template-columns: max-content 1fr;
gap: 2px 12px;
font-size: 0.82em;
margin-top: 6px;
}
.log-parse-key { color: var(--terminal-amber); opacity: .8; }
.log-parse-val { color: var(--terminal-green); word-break: break-all; }
.log-route-label {
color: var(--terminal-cyan);
font-size: 0.9em;
margin-top: 4px;
}
.log-route-goto {
color: var(--terminal-amber);
font-size: 0.82em;
opacity: .75;
margin-top: 2px;
}
.log-entry code { .log-entry code {
background: var(--bg-terminal); background: var(--bg-terminal);
padding: 2px 6px; padding: 2px 6px;
@@ -2136,6 +2158,31 @@
`; `;
} }
if (log.action === 'parse_complete') {
const pairs = log.parsed || {};
const keys = Object.keys(pairs);
const tableRows = keys.map(k =>
`<div class="log-parse-key">${escapeHtml(k)}</div><div class="log-parse-val">${escapeHtml(pairs[k])}</div>`
).join('');
return `
<div class="log-entry" style="border-left-color: #444; opacity:.85;">
<div class="log-timestamp">[${timestamp}]</div>
<div class="log-title" style="color:#666;">⚙ Parsed ${keys.length} variable${keys.length !== 1 ? 's' : ''}</div>
${keys.length > 0 ? `<div class="log-details"><div class="log-parse-table">${tableRows}</div></div>` : ''}
</div>
`;
}
if (log.action === 'route_taken') {
return `
<div class="log-entry" style="border-left-color: var(--terminal-cyan); opacity:.9;">
<div class="log-timestamp">[${timestamp}]</div>
<div class="log-title" style="color: var(--terminal-cyan);">⇒ Auto-route: Step ${log.step}</div>
${log.label ? `<div class="log-details"><div class="log-route-label">${escapeHtml(log.label)}</div>${log.goto ? `<div class="log-route-goto">→ ${escapeHtml(log.goto)}</div>` : ''}</div>` : ''}
</div>
`;
}
if (log.action === 'no_workers') { if (log.action === 'no_workers') {
return ` return `
<div class="log-entry failed"> <div class="log-entry failed">

View File

@@ -855,6 +855,47 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
await new Promise(r => setTimeout(r, ms)); await new Promise(r => setTimeout(r, ms));
} else if (step.type === 'parse') {
// Parse KEY=VALUE lines from last command output into execution state.
// Matches lines of the form: UPPER_KEY=value (key must start with uppercase letter)
const condOkParse = !step.condition || evalCondition(step.condition, execState?.state || {}, execState?.params || {});
const parseState = condOkParse ? _executionState.get(executionId) : null;
if (parseState) {
const output = parseState.state._lastCommandOutput || '';
const parsed = {};
for (const line of output.split('\n')) {
const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
if (m) {
const k = m[1].toLowerCase();
const v = m[2].trim();
parsed[k] = v;
parseState.state[k] = v;
}
}
await addExecutionLog(executionId, {
step: currentIndex + 1, step_name: stepLabel, action: 'parse_complete',
parsed_count: Object.keys(parsed).length, parsed,
timestamp: new Date().toISOString()
});
}
} else if (step.type === 'route') {
// Evaluate conditions in order; jump to the first match (no user input needed).
const routeState = execState?.state || {};
const routeParams = execState?.params || {};
let routeTaken = null;
for (const cond of (step.conditions || [])) {
const matches = cond.default || evalCondition(cond.if || 'false', routeState, routeParams);
if (matches) { routeTaken = cond; gotoId = cond.goto || null; break; }
}
await addExecutionLog(executionId, {
step: currentIndex + 1, step_name: stepLabel, action: 'route_taken',
condition: routeTaken?.if || 'default',
label: routeTaken?.label || null,
goto: gotoId,
timestamp: new Date().toISOString()
});
} }
await addExecutionLog(executionId, { await addExecutionLog(executionId, {
@@ -902,11 +943,15 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
// Pause execution and wait for user to respond via POST /api/executions/:id/respond. // Pause execution and wait for user to respond via POST /api/executions/:id/respond.
// Resolves with the chosen option string, or null on 60-minute timeout. // Resolves with the chosen option string, or null on 60-minute timeout.
async function executePromptStep(executionId, step, stepNumber) { async function executePromptStep(executionId, step, stepNumber) {
const message = step.message || 'Please choose an option:'; const execState = _executionState.get(executionId);
// Apply param substitution to the message so {{server_name}}, {{iface}} etc. work
let message = step.message || 'Please choose an option:';
if (execState && Object.keys(execState.params).length > 0) {
message = applyParams(message, execState.params);
}
const options = step.options || ['Continue']; const options = step.options || ['Continue'];
// Include the last command output so the user can review results alongside the question // Include the last command output so the user can review results alongside the question
const execState = _executionState.get(executionId);
const lastOutput = (execState?.state?._lastCommandOutput) || null; const lastOutput = (execState?.state?._lastCommandOutput) || null;
const logEntry = { const logEntry = {