Repository navigation
feat(ui): failure-first step drawer and live spec validation - #2533
Conversation
The mount-time formatDocument rewrote files the user never touched, lighting up Save/Discard and arming the external-change conflict dialog on first open. formatOnType reformatted the whole document on every Enter. Manual format (Shift+Alt+F) still works.
Backend validation errors (line:column prefixes and caret excerpts) were passed as the modal's hint, which collapses newlines into one muted proportional-font paragraph. Add a details channel rendered monospace with line breaks preserved, and stop treating an empty errors array as a failure on spec save.
The retry action re-executes only the selected step, but the tooltip and dialog title said "Retry from this step". Rename to match the API contract, and give the success path the same toast plus refresh the run-level retry already has instead of silently closing the dialog.
The header button said Enqueue while its tooltip, the modal it opens, and the CLI all say start. The modal still switches to Enqueue wording when the queue toggle is on.
The executions list rendered "No DAG runs found" while the first page was still loading, then swapped in the table. Thread the pagination hook's initial-loading flag into both list views and show a quiet inline loading row instead.
The workflow list told users with zero workflows to adjust filters they never set. Branch the copy: a pristine all-workflows view now invites creating the first workflow. The runs empty state is reworded to be range-aware instead of presuming filters, and the duplicated workflow empty-state markup is extracted into one component.
The schema doc sidebar has an Examples panel, and monaco-yaml surfaces schema examples in hover, value completion, and property snippets, but the schema carried none. Seed concise examples for the most-used DAG and step properties.
Every tab, bookmark, and history entry read the static "Dagu". Pages already publish their titles through AppBarContext; mirror that into document.title.
Sharing a workflow required select-all inside Monaco, and sharing a run meant grabbing the address bar. Add a Copy button to the spec editor header (visible to read-only viewers too) and a Copy link button on the run header. The three hand-rolled copy-feedback blocks consolidate into a useCopyFeedback hook.
…used The v1 executor key was rejected with a bare invalid-keys error while 70+ other legacy keys get migration hints, and renamed keys (precondition, dir) were mislabeled as snake_case fixes. Split the hint map into casing renames and removed keys with full replacement clauses, and drop the dead run->call entry (run is a valid v2 step key).
Clicking a failed node opened a drawer that rendered only the step's spec, with no status, error, timing, or path to the logs. The drawer now shows the node's status chip, error text, started/finished/duration, retry and repeat counts, approval fields, stdout/stderr buttons, and its sub-runs. Selection is stored by step name and re-derived from the run each render, so an open drawer tracks live SSE updates; the drawer's lifecycle effect keys on presence only so those updates cannot steal focus.
The /dags/validate endpoint existed with no UI caller: editing gave no feedback until save, syntax errors never highlighted a line, and the graph preview rendered only the saved spec, disappearing entirely whenever any error existed. The editor now validates the dirty buffer after a 600ms idle pause: positioned errors become Monaco markers under a dedicated owner, all errors feed the existing error block, a small header status shows Validating / N issues / Valid, and the graph previews the parsed result of the buffer. Rejected saves produce the same markers. The graph also renders alongside errors instead of being suppressed by them.
📝 WalkthroughWalkthroughThe PR adds debounced server-side validation for edited DAG specifications, Monaco validation markers, live preview data, and validation status display. It also updates step detail drawers to derive current runtime nodes and display status, timing, errors, logs, and sub-run actions. ChangesLive DAG validation
Runtime step details
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant DAGSpec
participant ValidationAPI
participant DAGEditorWithDocs
participant DAGPreview
DAGSpec->>ValidationAPI: Validate edited DAG specification
ValidationAPI-->>DAGSpec: Return errors and preview DAG data
DAGSpec->>DAGEditorWithDocs: Pass parsed Monaco markers
DAGSpec->>DAGPreview: Render live validation result
sequenceDiagram
participant DAGStatus
participant DAGExecutionHistory
participant StepDetailsDrawer
participant LogViewer
DAGStatus->>StepDetailsDrawer: Provide selected step and current node
DAGExecutionHistory->>StepDetailsDrawer: Provide selected step and current node
StepDetailsDrawer->>LogViewer: Open stdout or stderr for the node
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…idate # Conflicts: # ui/src/__tests__/App.test.tsx # ui/src/features/dags/components/dag-editor/DAGSpec.tsx # ui/src/features/dags/components/visualization/Graph.tsx
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
ui/src/features/dags/components/dag-editor/DAGEditor.tsx (1)
180-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd direct coverage for Monaco marker lifecycle.
The supplied
DAGSpectests mockDAGEditorWithDocs, so they do not execute this effect. Add a Vitest test forDAGEditorthat verifies markers use thedagu-serverowner, update when props change, and clear on unmount.As per coding guidelines, “Add or update tests appropriate to the changed code; ... frontend unit tests use Vitest.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/features/dags/components/dag-editor/DAGEditor.tsx` around lines 180 - 198, Add a Vitest unit test targeting DAGEditor’s marker effect, using a mocked Monaco model and editor APIs. Verify setModelMarkers is called with the dagu-server owner, refreshed when markers props change, and cleared when the component unmounts, covering the isEditorMounted lifecycle.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ui/src/features/dags/components/dag-editor/DAGSpec.tsx`:
- Around line 209-249: Update the validation effect around validateSeqRef and
the /dags/validate request to clear liveValidation immediately after assigning
the new sequence, and also clear it when the current request returns an error or
throws. Convert the timer callback to async with try/catch while preserving
stale-sequence guards, and add a Vitest case verifying an older response cannot
overwrite the newest edit’s validation result.
In `@ui/src/features/dags/components/dag-execution/DAGExecutionHistory.tsx`:
- Around line 543-560: Update the StepDetailsDrawer usage in DAGExecutionHistory
to provide onOpenSubRun, resolving the selected child index from
[...node.subRuns, ...node.subRunsRepeated] and navigating to that child run
using the existing history navigation behavior. Add a Vitest case covering a
repeated or parallel child run and its open action.
In `@ui/src/features/dags/components/step-details/StepDetailsDrawer.tsx`:
- Around line 126-130: Update the metadata value <dd> in the infoRows map and
the sub-run label near the sub-run rendering to use min-w-0 whitespace-normal
break-words, and remove truncate from the sub-run label. Preserve the existing
grid and label styling while ensuring long runtime values and sub-run names wrap
instead of overflowing.
In `@ui/src/lib/dagRunTiming.ts`:
- Around line 44-47: Update the duration calculation in dagRunTiming to validate
the millisecond difference before deriving whole seconds, returning '-' for any
negative duration including sub-second differences. Preserve whole-second
formatting for non-negative durations, and add a regression test covering a
finish timestamp 1–999 ms before the start.
---
Nitpick comments:
In `@ui/src/features/dags/components/dag-editor/DAGEditor.tsx`:
- Around line 180-198: Add a Vitest unit test targeting DAGEditor’s marker
effect, using a mocked Monaco model and editor APIs. Verify setModelMarkers is
called with the dagu-server owner, refreshed when markers props change, and
cleared when the component unmounts, covering the isEditorMounted lifecycle.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1de0bb6f-efbe-47b9-bfd1-c80e1e3a6fc2
📒 Files selected for processing (10)
ui/src/features/dags/components/DAGStatus.tsxui/src/features/dags/components/__tests__/DAGStatus.test.tsxui/src/features/dags/components/dag-editor/DAGEditor.tsxui/src/features/dags/components/dag-editor/DAGEditorWithDocs.tsxui/src/features/dags/components/dag-editor/DAGSpec.tsxui/src/features/dags/components/dag-editor/__tests__/DAGSpec.test.tsxui/src/features/dags/components/dag-editor/validationMarkers.tsui/src/features/dags/components/dag-execution/DAGExecutionHistory.tsxui/src/features/dags/components/step-details/StepDetailsDrawer.tsxui/src/lib/dagRunTiming.ts
| React.useEffect(() => { | ||
| if (!editable || !localHasUnsavedChanges || currentValue == null) { | ||
| validateSeqRef.current += 1; | ||
| setLiveValidation(null); | ||
| setIsValidating(false); | ||
| return; | ||
| } | ||
|
|
||
| const seq = ++validateSeqRef.current; | ||
| setIsValidating(true); | ||
| const timer = window.setTimeout(() => { | ||
| void client | ||
| .POST('/dags/validate', { | ||
| params: { query: { remoteNode } }, | ||
| body: { spec: currentValue, name: fileName }, | ||
| }) | ||
| .then(({ data: result, error: requestError }) => { | ||
| if (validateSeqRef.current !== seq) { | ||
| return; | ||
| } | ||
| setIsValidating(false); | ||
| if (!requestError && result) { | ||
| setLiveValidation({ errors: result.errors ?? [], dag: result.dag }); | ||
| } | ||
| }) | ||
| .catch(() => { | ||
| if (validateSeqRef.current === seq) { | ||
| setIsValidating(false); | ||
| } | ||
| }); | ||
| }, 600); | ||
|
|
||
| return () => window.clearTimeout(timer); | ||
| }, [ | ||
| client, | ||
| currentValue, | ||
| editable, | ||
| fileName, | ||
| localHasUnsavedChanges, | ||
| remoteNode, | ||
| ]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'DAGSpec\.tsx$' . || true
echo "== file outline =="
ast-grep outline ui/src/features/dags/components/dag-editor/DAGSpec.tsx --view compact || true
echo "== relevant lines =="
nl -ba ui/src/features/dags/components/dag-editor/DAGSpec.tsx | sed -n '1,290p'
echo "== search liveValidation usages =="
rg -n "liveValidation|validateSeqRef|setLiveValidation|isValidating|setIsValidating" ui/src/features/dags/components/dag-editor/DAGSpec.tsxRepository: dagucloud/dagu
Length of output: 476
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file outline =="
ast-grep outline ui/src/features/dags/components/dag-editor/DAGSpec.tsx --view expanded || true
echo "== relevant lines =="
awk '{printf "%6d\t%s\n", NR, $0}' ui/src/features/dags/components/dag-editor/DAGSpec.tsx | sed -n '1,290p'
echo "== search liveValidation usages =="
rg -n "liveValidation|validateSeqRef|setLiveValidation|isValidating|setIsValidating" ui/src/features/dags/components/dag-editor/DAGSpec.tsx
echo "== search tests around dag-editor =="
git ls-files 'ui/**/*test*.{ts,tsx,js,jsx}' | rg 'dag|DAG|editor|validation|vi|vitest' || trueRepository: dagucloud/dagu
Length of output: 13037
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== live validations render paths =="
awk '{printf "%6d\t%s\n", NR, $0}' ui/src/features/dags/components/dag-editor/DAGSpec.tsx | sed -n '380,760p'
echo "== all tests mentioning live validation validation markers =="
rg -n "liveValidation|live markers|validationMarkers|dags/validate|validateSeqRef|isValidating" ui -g '*.{ts,tsx,js,jsx}' || true
echo "== test files under ui/src =="
git ls-files 'ui/src/**/*.{ts,tsx,js,jsx}' | rg 'test|spec|vitest' | sed -n '1,200p' || trueRepository: dagucloud/dagu
Length of output: 20273
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== DAGSpec test section =="
awk '{printf "%6d\t%s\n", NR, $0}' ui/src/features/dags/components/dag-editor/__tests__/DAGSpec.test.tsx | sed -n '1,260p'
echo "== behavioral probe: validateSeqRef staleness and rendered preview source =="
python3 - <<'PY'
# Read-only extraction of the effect logic into a small deterministic model.
def model_validate_effect(validate_seq, current, dirty, error=None, errors=(), dag=None):
"""Model the current effect response path for one validate request."""
new_seq = validate_seq + 1
live_validation = None if not dirty or current is None else {
'errors': errors,
'dag': dag,
}
if error:
# current effect only leaves state from prior dirty validation, if any.
live_validation = live_validation if (not error) else None
return {
'request_seq': new_seq,
'live_validation': live_validation,
}
steps = [
{'buffer': 'step A', 'dirty': True, 'server_error': False, 'server_errors': ['A error'], 'server_dag': {'steps': [{'name': 'a'}]}},
{'buffer': 'step B', 'dirty': True, 'server_error': False, 'server_errors': ['B error'], 'server_dag': {'steps': [{'name': 'b'}]}},
{'buffer': 'step C', 'dirty': True, 'server_error': True, 'server_errors': [], 'server_dag': None},
]
state = {'validate_seq': 0, 'live_validation': None}
preview_order = []
for i, req in enumerate(steps, 1):
# Effect runs after current value/deps change; this does not overwrite with the server's response yet.
state['validate_seq'] += 1
if not req['dirty'] or req['buffer'] is None or state.get('prev_dirty_invalid') == 1:
state['live_validation'] = None
else:
state['live_validation'] = None # current effect does not set liveValidation on request start.
preview_order.append({
'step': i,
'live_validation': state['live_validation'],
})
# Request result handles stale via validateSeqRef.current !== seq only.
if req['server_error']:
pass
else:
state['live_validation'] = {'errors': req['server_errors'], 'dag': req['server_dag']}
for label, value in preview_order.items():
print(f"{label}: {value}")
print("after step A -> B invalidation request, current state has no new liveValidation until B responds, so old validation persists.")
PYRepository: dagucloud/dagu
Length of output: 8522
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== API client response shape =="
rg -n "POST\\(|async /\\*|await client\\.POST|function .*\\(|const .*POST|error:" ui/src/hooks/api.ts ui/src/hooks -g '*.ts' | sed -n '1,220p'
echo "== useClient implementation =="
fd -a 'api\.ts$|client\..*\.ts$' ui/src/hooks ui/src | sed -n '1,50p' | xargs -r -n1 -I{} sh -c 'echo "--- {};"; sed -n "1,240p" "{}" | sed -n "/useClient/,+220p"'Repository: dagucloud/dagu
Length of output: 5949
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== api.ts =="
cat -n ui/src/hooks/api.ts
echo "== validation marker parser =="
fd -a 'validationMarkers\.ts$' ui/src | while read -r f; do
echo "--- $f"
cat -n "$f" | sed -n '1,220p'
done
echo "== current test line count =="
wc -l ui/src/features/dags/components/dag-editor/__tests__/DAGSpec.test.tsxRepository: dagucloud/dagu
Length of output: 3325
Clear stale live validation when a new validation edit starts.
When currentValue changes while dirty, validateSeqRef is incremented, but liveValidation still shows the previous validation result until the new response arrives. If the newest request errors or throws, the old result also never clears. Clear liveValidation immediately after assigning seq, and re-clear it in both the validation error and exception paths. Add a Vitest case where an older validate response arrives after a newer edit and check that only the newest result is reflected. Change the handler to async with try/catch to match the frontend promise-style guideline.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ui/src/features/dags/components/dag-editor/DAGSpec.tsx` around lines 209 -
249, Update the validation effect around validateSeqRef and the /dags/validate
request to clear liveValidation immediately after assigning the new sequence,
and also clear it when the current request returns an error or throws. Convert
the timer callback to async with try/catch while preserving stale-sequence
guards, and add a Vitest case verifying an older response cannot overwrite the
newest edit’s validation result.
Source: Coding guidelines
| <StepDetailsDrawer | ||
| dagName={selectedDAGRun?.name} | ||
| isOpen={isStepDetailsOpen} | ||
| step={selectedDetailStep} | ||
| step={selectedDetailNode?.step} | ||
| node={selectedDetailNode} | ||
| onClose={closeStepDetails} | ||
| onViewLog={(node, stream) => { | ||
| if (!selectedDAGRun) { | ||
| return; | ||
| } | ||
| setLogViewer({ | ||
| isOpen: true, | ||
| logType: 'step', | ||
| stepName: node.step.name, | ||
| dagRunId: selectedDAGRun.dagRunId, | ||
| stream: stream === 'stderr' ? Stream.stderr : Stream.stdout, | ||
| }); | ||
| }} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Wire sub-run opening for the history drawer.
This caller does not pass onOpenSubRun. The drawer therefore lists sub-runs without an Open action. DAGStatus passes this action, so run history does not provide the same runtime drawer behavior.
Pass an onOpenSubRun callback that resolves the selected index from [...node.subRuns, ...node.subRunsRepeated] and navigates to that child run. Add a Vitest case for a repeated or parallel child run.
As per coding guidelines, add or update tests appropriate to changed TypeScript code.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ui/src/features/dags/components/dag-execution/DAGExecutionHistory.tsx` around
lines 543 - 560, Update the StepDetailsDrawer usage in DAGExecutionHistory to
provide onOpenSubRun, resolving the selected child index from [...node.subRuns,
...node.subRunsRepeated] and navigating to that child run using the existing
history navigation behavior. Add a Vitest case covering a repeated or parallel
child run and its open action.
Source: Coding guidelines
| <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm"> | ||
| {infoRows.map(([label, value]) => ( | ||
| <React.Fragment key={label}> | ||
| <dt className="text-muted-foreground">{label}</dt> | ||
| <dd className="tabular-nums text-foreground">{value}</dd> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent overflow of runtime values.
Line 130 renders arbitrary metadata in a grid cell without wrapping. Line 165 truncates sub-run names. Use min-w-0 whitespace-normal break-words for both values and remove truncate from the sub-run label.
As per coding guidelines, “Always handle long text in tables and lists with whitespace-normal break-words to prevent layout overflow.”
Also applies to: 165-169
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ui/src/features/dags/components/step-details/StepDetailsDrawer.tsx` around
lines 126 - 130, Update the metadata value <dd> in the infoRows map and the
sub-run label near the sub-run rendering to use min-w-0 whitespace-normal
break-words, and remove truncate from the sub-run label. Preserve the existing
grid and label styling while ensuring long runtime values and sub-run names wrap
instead of overflowing.
Source: Coding guidelines
| const diff = end.diff(start, 'second'); | ||
| if (diff < 0) { | ||
| return '-'; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'dagRunTiming\.ts$' . || true
echo "== file contents =="
if [ -f ui/src/lib/dagRunTiming.ts ]; then
cat -n ui/src/lib/dagRunTiming.ts
fi
echo "== related usages/tests =="
rg -n "dagRunTiming|formatDagRunTiming|dagRun" ui/src || true
echo "== dependency dayjs version =="
for f in package.json ui/package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
[ -f "$f" ] && echo "--- $f ---" && (rg -n '"dayjs"|dayjs|day\.js' "$f" | head -40 || true)
done
echo "== behavioral evidence from installed or npm metadata =="
node - <<'JS'
try {
const dayjs = require.resolve('dayjs');
const dayjs = require(dayjs);
window = {};
const a = dayjs('2025-01-01T00:00:00.000Z');
const b = dayjs('2025-01-01T00:00:00.500Z');
const c = dayjs('2024-12-31T23:59:59.999Z');
console.log(JSON.stringify({
secondDiffPositiveTruncated: b.diff(a, 'second'),
secondDiffNegativeTruncated: c.diff(a, 'second'),
msDiffPositive: b.diff(a),
msDiffNegative: c.diff(a),
}));
} catch (e) {
console.log("no local dayjs:", e.message);
}
JS
echo "== git diff stat/name status =="
git diff --stat HEAD~1..HEAD 2>/dev/null || true
git diff -- ui/src/lib/dagRunTiming.ts 2>/dev/null | sed -n '1,160p' || trueRepository: dagucloud/dagu
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== exact dagRunTiming tests =="
if [ -f ui/src/lib/__tests__/dagRunTiming.test.ts ]; then
cat -n ui/src/lib/__tests__/dagRunTiming.test.ts
fi
echo "== dependency files containing dayjs =="
for f in package.json ui/package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lock; do
[ -f "$f" ] && echo "--- $f ---" && (rg -n '"dayjs"|dayjs:' "$f" -A 2 -B 2 | head -80 || true)
done
echo "== dayjs package availability and seconds diff behavior =="
node - <<'JS'
const dayjsLib = require.resolve('dayjs');
const dayjs = require('dayjs');
const cases = [
['500ms before', '2025-01-01T00:00:00.000Z', '2025-01-01T00:00:00.500Z'],
['999ms before', '2025-01-01T00:00:00.000Z', '2025-01-01T00:00:00.999Z'],
['1001ms before', '2025-01-01T00:00:00.000Z', '2025-01-01T00:00:01.001Z'],
];
for (const [label, startedAt, finishedAt] of cases) {
const s = dayjs(startedAt);
const e = dayjs(finishedAt);
console.log(label, { startedAt, finishedAt, secondDiff: e.diff(s, 'second'), msDiff: e.diff(s) });
}
JS
echo "== run existing tests if available =="
if [ -x node_modules/.bin/vitest ] || command -v vitest >/dev/null 2>&1; then
npm exec vitest run ui/src/lib/__tests__/dagRunTiming.test.ts 2>&1 | sed -n '1,160p' || true
else
echo "vitest not available"
fiRepository: dagucloud/dagu
Length of output: 2500
Reject sub-second negative durations.
end.diff(start, 'second') computes integer seconds, so a finish timestamp 1–999 ms before the start returns 0 and formats as 0s. Validate the millisecond difference before deriving whole seconds and add a regression test for a finish timestamp just before the start timestamp.
Proposed fix
- const diff = end.diff(start, 'second');
- if (diff < 0) {
+ const diffMs = end.diff(start);
+ if (diffMs < 0) {
return '-';
}
+ const diff = Math.floor(diffMs / 1000);[low_effort和high_reward]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ui/src/lib/dagRunTiming.ts` around lines 44 - 47, Update the duration
calculation in dagRunTiming to validate the millisecond difference before
deriving whole seconds, returning '-' for any negative duration including
sub-second differences. Preserve whole-second formatting for non-negative
durations, and add a regression test covering a finish timestamp 1–999 ms before
the start.
Source: MCP tools
Script and config blocks in the step details drawer capped at 288px with their own scrollbar, nesting a second scroll region inside the already-scrolling drawer body. The blocks now grow naturally; the shell block keeps horizontal scrolling for long lines.
Failed validate requests now clear the previous result instead of letting errors from an older buffer linger. The history drawer wires sub-run opening like the run page. Drawer metadata and sub-run labels wrap instead of truncating. formatRunDuration rejects finish timestamps up to 999ms before the start that second-level truncation read as 0s.
Summary
Two halves of the same loop: understanding a failure, and fixing it without leaving the editor.
Step drawer shows runtime state. Clicking a node on the run page opened a drawer that rendered only the step's spec: no status, no error, no timing, no logs. The drawer now leads with the node's status chip, error or rejection reason, started/finished/duration, retry and repeat counts, approval and human-task fields, View stdout/stderr buttons (which hand off to the log viewer), and the node's sub-runs with an Open action. Selection is stored by step name and re-derived from the run each render, so an open drawer follows live SSE updates; the lifecycle effect keys on presence only, so those updates cannot re-steal focus. The run-history tab gets the same wiring; the spec editor's drawer stays spec-only.
Live validation wired to `/dags/validate`. The endpoint was fully implemented with zero UI callers. The editor now validates the dirty buffer after a 600ms idle pause:
Stacked on #2531 (shares the spec save-handler region).
Validation
Summary by cubic
Adds a failure‑first step details drawer with runtime info and log shortcuts, plus live spec validation with markers and a graph preview in the editor. This helps you spot failures fast and fix them without leaving the editor.
New Features
/dags/validate; positioned errors render as Monaco markers under thedagu-serverowner; the error panel updates live with Validating / N issues / Valid; the graph previews the parsed buffer and renders alongside errors; rejected saves feed the same markers and panel.Bug Fixes
Written for commit 96a7217. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes