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

@@ -855,6 +855,47 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
timestamp: new Date().toISOString()
});
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, {
@@ -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.
// Resolves with the chosen option string, or null on 60-minute timeout.
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'];
// 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 logEntry = {