Skip to content

feat(ui): failure-first step drawer and live spec validation - #2533

Merged
yohamta0 merged 16 commits into
mainfrom
ui-failure-drawer-validate
Aug 8, 2026
Merged

yohamta0 merged 16 commits into
mainfrom
ui-failure-drawer-validate

Conversation

@yohamta0

@yohamta0 yohamta0 commented Aug 8, 2026 •

Copy link
Copy Markdown
Member

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:

  • errors with a `[line:column]` prefix become Monaco markers under a `dagu-server` owner (monaco-yaml's own diagnostics untouched); unpositioned semantic errors are listed without a misleading line-1 squiggle
  • the existing error block shows live results, and a small header status reads Validating / N issues / Valid
  • the graph preview renders the parsed buffer instead of the last saved spec, and errors no longer suppress the graph: both render together
  • a rejected save feeds the same markers and panel

Stacked on #2531 (shares the spec save-handler region).

Validation

  • `cd ui && pnpm test` (629 tests; new DAGSpec suite covers debounce, marker parsing, live graph, saved-errors-plus-graph; DAGStatus suite covers drawer failure content, stderr handoff, live drawer updates)
  • `pnpm exec tsc --noEmit`, `pnpm build`

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

    • Step details drawer: shows status chip, error/rejection, started/finished/duration, retry/repeat counts, approval/human‑task fields; includes View stdout/stderr buttons that open the log viewer; lists sub‑runs with Open. The open drawer follows live SSE updates without stealing focus and is wired on both the run page and run history.
    • Live spec validation: validates the dirty buffer after 600ms via /dags/validate; positioned errors render as Monaco markers under the dagu-server owner; 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

    • Failed validate requests now clear previous markers and panel results to avoid stale errors.
    • History drawer: sub‑runs open correctly from the drawer; metadata and sub‑run labels wrap instead of truncating.
    • Step drawer code blocks now grow with content instead of nesting scrollbars; shell blocks keep horizontal scrolling only.
    • Duration display: reject finish times before the start down to sub‑second gaps to prevent showing “0s” on negatives.

Written for commit 96a7217. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added live validation for unsaved DAG edits, with inline editor markers, issue counts, and updated previews.
    • Enhanced step details with current runtime status, timing, duration, retries, approvals, errors, and completion information.
    • Added access to step stdout/stderr logs and child runs from the details drawer.
    • Added run duration display, including ongoing execution times.
  • Bug Fixes

    • Step details now stay synchronized with the latest DAG run data.
    • Improved handling and messaging for empty or invalid DAG steps.

yohamta0 added 13 commits August 8, 2026 13:14
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.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Live DAG validation

Layer / File(s) Summary
Validation orchestration
ui/src/features/dags/components/dag-editor/DAGSpec.tsx, ui/src/features/dags/components/dag-editor/validationMarkers.ts
Dirty DAG content is validated through /dags/validate. Responses update validation state, preview data, and parsed markers. Stale responses are ignored.
Editor and preview wiring
ui/src/features/dags/components/dag-editor/DAGEditor.tsx, ui/src/features/dags/components/dag-editor/DAGEditorWithDocs.tsx, ui/src/features/dags/components/dag-editor/DAGSpec.tsx
Server markers are applied to Monaco under the dagu-server owner. Dirty previews use live validation results and display validation status.
Validation behavior coverage
ui/src/features/dags/components/dag-editor/__tests__/DAGSpec.test.tsx
Tests cover debounced requests, invalid responses, markers, live graph data, and saved-state rendering.

Runtime step details

Layer / File(s) Summary
Runtime drawer content
ui/src/features/dags/components/step-details/StepDetailsDrawer.tsx, ui/src/lib/dagRunTiming.ts
The drawer displays runtime status, timestamps, duration, errors, retries, approvals, logs, and sub-runs. Duration formatting handles running, invalid, and future-dated timestamps.
Current node selection and actions
ui/src/features/dags/components/DAGStatus.tsx, ui/src/features/dags/components/dag-execution/DAGExecutionHistory.tsx
Step selection stores names and derives nodes from the current DAG run. Drawer actions open stdout, stderr, and sub-runs.
Runtime drawer coverage
ui/src/features/dags/components/__tests__/DAGStatus.test.tsx
Tests cover failed-step details, stderr viewing, and live drawer updates after node changes.

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
Loading
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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the two primary changes: the failure-first step drawer and live specification validation.
Description check ✅ Passed The description provides detailed summaries, change coverage, validation results, and related issue context, despite missing explicit template headings and checklist items.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui-failure-drawer-validate

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from ui-ux-cheap-wins to main August 8, 2026 06:52
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
ui/src/features/dags/components/dag-editor/DAGEditor.tsx (1)

180-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct coverage for Monaco marker lifecycle.

The supplied DAGSpec tests mock DAGEditorWithDocs, so they do not execute this effect. Add a Vitest test for DAGEditor that verifies markers use the dagu-server owner, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1768fb0 and e041990.

📒 Files selected for processing (10)
  • ui/src/features/dags/components/DAGStatus.tsx
  • ui/src/features/dags/components/__tests__/DAGStatus.test.tsx
  • ui/src/features/dags/components/dag-editor/DAGEditor.tsx
  • ui/src/features/dags/components/dag-editor/DAGEditorWithDocs.tsx
  • ui/src/features/dags/components/dag-editor/DAGSpec.tsx
  • ui/src/features/dags/components/dag-editor/__tests__/DAGSpec.test.tsx
  • ui/src/features/dags/components/dag-editor/validationMarkers.ts
  • ui/src/features/dags/components/dag-execution/DAGExecutionHistory.tsx
  • ui/src/features/dags/components/step-details/StepDetailsDrawer.tsx
  • ui/src/lib/dagRunTiming.ts

Comment on lines +209 to +249
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,
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.tsx

Repository: 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' || true

Repository: 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' || true

Repository: 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.")
PY

Repository: 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.tsx

Repository: 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

Comment on lines 543 to +560
<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,
});
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment on lines +126 to +130
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment thread ui/src/lib/dagRunTiming.ts Outdated
Comment on lines +44 to +47
const diff = end.diff(start, 'second');
if (diff < 0) {
return '-';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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' || true

Repository: 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"
fi

Repository: 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.
@yohamta0
yohamta0 merged commit 6fcdd6c into main Aug 8, 2026
11 checks passed
@yohamta0
yohamta0 deleted the ui-failure-drawer-validate branch August 8, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant