From c3a0391f4dded4ee7f120e2017f243758551786e Mon Sep 17 00:00:00 2001 From: stack72 Date: Thu, 26 Mar 2026 00:18:30 +0100 Subject: [PATCH] fix: show step name in tree view for forEach-expanded steps (#867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using forEach to expand workflow steps, the live tree view previously showed only "modelName → methodName" for every expanded step, making it impossible to distinguish between iterations (e.g., all showing "tester → smokeTest"). Now the tree view prefixes the step name when it differs from the model name, producing labels like "test-alpine: tester → smokeTest". This applies to: - Live running step labels (StepLine component) - Inline single-step job labels (JobLine component) - Graduated scrollback labels after job completion The prefix is only added when stepId !== modelName, so non-forEach steps that naturally match (e.g., stepId "ec2-instance" with modelName "ec2-instance") remain unchanged as "ec2-instance → create". Closes #867 --- .../workflow_run_tree/components/job_line.tsx | 8 ++- .../components/step_line.tsx | 8 ++- .../renderers/workflow_run_tree/state.ts | 3 +- .../renderers/workflow_run_tree/state_test.ts | 63 ++++++++++++++++++- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/presentation/renderers/workflow_run_tree/components/job_line.tsx b/src/presentation/renderers/workflow_run_tree/components/job_line.tsx index aa6d0290..4e1ce186 100644 --- a/src/presentation/renderers/workflow_run_tree/components/job_line.tsx +++ b/src/presentation/renderers/workflow_run_tree/components/job_line.tsx @@ -72,9 +72,13 @@ export function JobLine( case "running": { if (runningSteps.length === 1) { const step = runningSteps[0]; - const label = step.modelName && step.methodName + const modelMethod = step.modelName && step.methodName ? `${step.modelName} \u2192 ${step.methodName}` - : step.id; + : null; + const stepPrefix = modelMethod && step.id !== step.modelName + ? `${step.id}: ` + : ""; + const label = modelMethod ? `${stepPrefix}${modelMethod}` : step.id; const dur = elapsed > 0 ? ` (${formatDuration(elapsed)})` : ""; statusInfo = ( diff --git a/src/presentation/renderers/workflow_run_tree/components/step_line.tsx b/src/presentation/renderers/workflow_run_tree/components/step_line.tsx index 1de919e9..a76b1dd8 100644 --- a/src/presentation/renderers/workflow_run_tree/components/step_line.tsx +++ b/src/presentation/renderers/workflow_run_tree/components/step_line.tsx @@ -36,9 +36,13 @@ export function StepLine({ step, prefix }: StepLineProps) { const spinnerFrame = useSpinner(); const elapsed = useElapsed(step.startedAt); - const label = step.modelName && step.methodName + const modelMethod = step.modelName && step.methodName ? `${step.modelName} \u2192 ${step.methodName}` - : step.id; + : null; + const stepPrefix = modelMethod && step.id !== step.modelName + ? `${step.id}: ` + : ""; + const label = modelMethod ? `${stepPrefix}${modelMethod}` : step.id; const duration = elapsed > 0 ? formatDuration(elapsed) : ""; diff --git a/src/presentation/renderers/workflow_run_tree/state.ts b/src/presentation/renderers/workflow_run_tree/state.ts index 5e28b4df..db408945 100644 --- a/src/presentation/renderers/workflow_run_tree/state.ts +++ b/src/presentation/renderers/workflow_run_tree/state.ts @@ -221,7 +221,8 @@ function graduateJob(job: JobState): ScrollbackItem { if (job.stepOrder.length === 1) { const step = job.steps.get(job.stepOrder[0]); if (step?.modelName && step?.methodName) { - singleStepLabel = `${step.modelName} \u2192 ${step.methodName}`; + const prefix = step.id !== step.modelName ? `${step.id}: ` : ""; + singleStepLabel = `${prefix}${step.modelName} \u2192 ${step.methodName}`; } } diff --git a/src/presentation/renderers/workflow_run_tree/state_test.ts b/src/presentation/renderers/workflow_run_tree/state_test.ts index 53a52db5..a5892688 100644 --- a/src/presentation/renderers/workflow_run_tree/state_test.ts +++ b/src/presentation/renderers/workflow_run_tree/state_test.ts @@ -190,7 +190,7 @@ Deno.test("treeReducer: job_completed graduates to scrollback and unblocks depen if (item.type === "job") { assertEquals(item.jobId, "provision"); assertEquals(item.status, "succeeded"); - assertEquals(item.singleStepLabel, "ec2-instance \u2192 create"); + assertEquals(item.singleStepLabel, "step-1: ec2-instance \u2192 create"); } // configure should now be waiting (provision dependency resolved) @@ -483,3 +483,64 @@ Deno.test("treeReducer: batch action processes multiple events atomically", () = assertEquals(step.modelName, "ec2"); assertEquals(step.outputBuffer, ["Creating..."]); }); + +Deno.test("treeReducer: singleStepLabel includes step name prefix for forEach-expanded steps", () => { + const state = reduce( + createInitialState("test"), + { + kind: "started", + runId: "run-1", + workflowName: "test", + jobs: [{ id: "test-job", stepCount: 1, dependsOn: [] }], + }, + { kind: "job_started", jobId: "test-job" }, + { kind: "step_started", jobId: "test-job", stepId: "test-alpine" }, + { + kind: "model_resolved", + jobId: "test-job", + stepId: "test-alpine", + modelName: "tester", + modelType: "test/runner", + methodName: "smokeTest", + }, + { kind: "step_completed", jobId: "test-job", stepId: "test-alpine" }, + { kind: "job_completed", jobId: "test-job", status: "succeeded" }, + ); + + const item = state.scrollback[0]; + assertEquals(item.type, "job"); + if (item.type === "job") { + assertEquals(item.singleStepLabel, "test-alpine: tester \u2192 smokeTest"); + } +}); + +Deno.test("treeReducer: singleStepLabel omits prefix when stepId matches modelName", () => { + const state = reduce( + createInitialState("deploy"), + { + kind: "started", + runId: "run-1", + workflowName: "deploy", + jobs: [{ id: "provision", stepCount: 1, dependsOn: [] }], + }, + { kind: "job_started", jobId: "provision" }, + { kind: "step_started", jobId: "provision", stepId: "ec2-instance" }, + { + kind: "model_resolved", + jobId: "provision", + stepId: "ec2-instance", + modelName: "ec2-instance", + modelType: "aws/ec2", + methodName: "create", + }, + { kind: "step_completed", jobId: "provision", stepId: "ec2-instance" }, + { kind: "job_completed", jobId: "provision", status: "succeeded" }, + ); + + const item = state.scrollback[0]; + assertEquals(item.type, "job"); + if (item.type === "job") { + // No prefix when stepId matches modelName + assertEquals(item.singleStepLabel, "ec2-instance \u2192 create"); + } +});