diff --git a/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts b/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts
index d6d49fcf7..fb1e37102 100644
--- a/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts
+++ b/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts
@@ -44,12 +44,14 @@ export function computeWaves(nodes: readonly DagNode[]): DagNode[][] {
/**
* Visual row index of a node inside the rendered wave list, counting one row
- * per wave header and one row per node. Used to scroll the selected node into
- * view — valid only while every node renders as a single row.
+ * per wave header, one row per node, and one blank spacer row between waves.
+ * Used to scroll the selected node into view — valid only while every node
+ * renders as a single row.
*/
export function computeNodeRowIndex(layers: readonly (readonly DagNode[])[], nodeID: string): number | undefined {
let row = 0
- for (const layer of layers) {
+ for (const [index, layer] of layers.entries()) {
+ if (index > 0) row++ // spacer between waves
row++ // wave header
for (const node of layer) {
if (node.id === nodeID) return row
@@ -60,15 +62,16 @@ export function computeNodeRowIndex(layers: readonly (readonly DagNode[])[], nod
}
export function formatDagError(error: string) {
- return error
- .replace(/^Cause\(\[Die\((.*)\)\]\)$/, "$1")
- .replace(/^ProviderModelNotFoundError:\s*/, "")
+ return error.replace(/^Cause\(\[Die\((.*)\)\]\)$/, "$1").replace(/^ProviderModelNotFoundError:\s*/, "")
}
/** Compact "3m 12s" duration between two epoch-millis timestamps. The SDK
* serializes numbers with Infinity/NaN sentinels — non-finite inputs yield
* no duration. */
-export function formatDagDuration(startedAt: number | string | undefined, completedAt: number | string | undefined): string | undefined {
+export function formatDagDuration(
+ startedAt: number | string | undefined,
+ completedAt: number | string | undefined,
+): string | undefined {
if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) return undefined
const end = typeof completedAt === "number" && Number.isFinite(completedAt) ? completedAt : Date.now()
const totalSeconds = Math.max(0, Math.round((end - startedAt) / 1000))
@@ -170,7 +173,13 @@ export function dagControlAllowed(status: string | undefined, operation: DagCont
export function dagControlUnavailableMessage(status: string | undefined, operation: DagControlOperation) {
if (dagControlAllowed(status, operation)) return undefined
const action =
- operation === "pause" ? "paused" : operation === "resume" ? "resumed" : operation === "step" ? "stepped" : "cancelled"
+ operation === "pause"
+ ? "paused"
+ : operation === "resume"
+ ? "resumed"
+ : operation === "step"
+ ? "stepped"
+ : "cancelled"
return `Workflow is ${status ?? "unavailable"} and cannot be ${action}`
}
diff --git a/packages/tui/src/feature-plugins/system/dag-inspector.tsx b/packages/tui/src/feature-plugins/system/dag-inspector.tsx
index 91ae320a2..9f608061c 100644
--- a/packages/tui/src/feature-plugins/system/dag-inspector.tsx
+++ b/packages/tui/src/feature-plugins/system/dag-inspector.tsx
@@ -384,7 +384,7 @@ function DagInspector(props: { api: TuiPluginApi }) {
return (
-
+
DAG
{selectedWorkflowSummary()?.title ?? "workflow inspector"}
@@ -429,6 +429,8 @@ function DagInspector(props: { api: TuiPluginApi }) {
flexDirection="row"
gap={1}
width="100%"
+ paddingLeft={1}
+ paddingRight={1}
backgroundColor={selected() ? theme().primary : undefined}
onMouseUp={() => setSelectedWorkflow(wf.id)}
>
@@ -450,9 +452,22 @@ function DagInspector(props: { api: TuiPluginApi }) {
+ {/* The right pane draws its own left border on every content
+ block; the horizontal separators' ┬/├/┴ edge glyphs land in
+ the same column, forming one continuous frame around both
+ panes — the same construction as the diff-viewer's patch
+ pane next to its file tree. */}
-
+
•
@@ -469,73 +484,97 @@ function DagInspector(props: { api: TuiPluginApi }) {
- (nodeScroll = element)}
- flexGrow={1}
- minHeight={0}
- verticalScrollbarOptions={{ visible: false }}
- horizontalScrollbarOptions={{ visible: false }}
- >
-
- {(layer, layerIdx) => (
- <>
- {/* Wave header: nodes at the same topological depth, NOT a barrier */}
-
-
- wave {layerIdx() + 1} · {layer.length} {layer.length === 1 ? "node" : "nodes"}
-
-
-
- {(node) => {
- const selected = () => selectedNode() === node.id
- return (
- setSelectedNode(node.id)}
- >
- }
+ {/* Border lives on the wrapper, not the rows, so the left
+ edge stays continuous when the node list is shorter than
+ the viewport. */}
+
+ (nodeScroll = element)}
+ flexGrow={1}
+ minHeight={0}
+ verticalScrollbarOptions={{ visible: false }}
+ horizontalScrollbarOptions={{ visible: false }}
+ >
+
+ {(layer, layerIdx) => (
+ <>
+ {/* Blank spacer between waves keeps the blocks visually
+ separate; computeNodeRowIndex counts it for scrolling. */}
+ {layerIdx() !== 0 ? : null}
+ {/* Wave header: nodes at the same topological depth, NOT a barrier */}
+
+
+ wave {layerIdx() + 1}
+
+
+ · {layer.length} {layer.length === 1 ? "node" : "nodes"}
+
+
+
+ {(node) => {
+ const selected = () => selectedNode() === node.id
+ const settled = () =>
+ node.status === "completed" ||
+ node.status === "skipped" ||
+ node.status === "cancelled" ||
+ node.status === "aborted"
+ return (
+ setSelectedNode(node.id)}
>
-
- {dagNodeGlyph(node.status)}
-
-
-
+ }
+ >
+
+ {dagNodeGlyph(node.status)}
+
+
+
+
+ {node.name}
+
+
- {node.name}
+ {node.worker_type}
-
- {node.worker_type}
-
-
- )
- }}
-
- >
- )}
-
-
-
-
+ )
+ }}
+
+ >
+ )}
+
+
+
+
+
{(node) => (
<>
@@ -583,13 +622,16 @@ function DagInspector(props: { api: TuiPluginApi }) {
)}
+ {/* Bottom rail: closes the frame flush with the workflow
+ list's bottom border, mirroring the diff-viewer. */}
+
-
+
{(hint) => (
diff --git a/packages/tui/test/feature-plugins/dag-inspector-utils.test.ts b/packages/tui/test/feature-plugins/dag-inspector-utils.test.ts
index 98883e4b6..782befae9 100644
--- a/packages/tui/test/feature-plugins/dag-inspector-utils.test.ts
+++ b/packages/tui/test/feature-plugins/dag-inspector-utils.test.ts
@@ -67,9 +67,7 @@ describe("computeWaves", () => {
describe("formatDagError", () => {
test("removes Effect and provider error wrappers without hiding the useful message", () => {
expect(
- formatDagError(
- "Cause([Die(ProviderModelNotFoundError: Model not found: local/local/glm. Did you mean: glm?)])",
- ),
+ formatDagError("Cause([Die(ProviderModelNotFoundError: Model not found: local/local/glm. Did you mean: glm?)])"),
).toBe("Model not found: local/local/glm. Did you mean: glm?")
})
})
@@ -79,18 +77,10 @@ describe("DAG control state", () => {
expect(dagControlUnavailableMessage("running", "pause")).toBeUndefined()
expect(dagControlUnavailableMessage("stepping", "pause")).toBeUndefined()
expect(dagControlUnavailableMessage("paused", "resume")).toBeUndefined()
- expect(dagControlUnavailableMessage("completed", "pause")).toBe(
- "Workflow is completed and cannot be paused",
- )
- expect(dagControlUnavailableMessage("cancelled", "cancel")).toBe(
- "Workflow is cancelled and cannot be cancelled",
- )
- expect(dagControlUnavailableMessage("pending", "cancel")).toBe(
- "Workflow is pending and cannot be cancelled",
- )
- expect(dagControlUnavailableMessage("archived", "cancel")).toBe(
- "Workflow is archived and cannot be cancelled",
- )
+ expect(dagControlUnavailableMessage("completed", "pause")).toBe("Workflow is completed and cannot be paused")
+ expect(dagControlUnavailableMessage("cancelled", "cancel")).toBe("Workflow is cancelled and cannot be cancelled")
+ expect(dagControlUnavailableMessage("pending", "cancel")).toBe("Workflow is pending and cannot be cancelled")
+ expect(dagControlUnavailableMessage("archived", "cancel")).toBe("Workflow is archived and cannot be cancelled")
})
test("formats progress without component-level branching", () => {
@@ -118,13 +108,13 @@ describe("DAG control state", () => {
})
describe("computeNodeRowIndex", () => {
- test("counts one row per wave header plus one row per node", () => {
+ test("counts wave headers, nodes, and inter-wave spacer rows", () => {
const layers = computeWaves([node("a"), node("b", ["a"]), node("c", ["a"]), node("d", ["b", "c"])])
- // rows: 0 wave1 header, 1 a, 2 wave2 header, 3 b, 4 c, 5 wave3 header, 6 d
+ // rows: 0 wave1 header, 1 a, 2 spacer, 3 wave2 header, 4 b, 5 c, 6 spacer, 7 wave3 header, 8 d
expect(computeNodeRowIndex(layers, "a")).toBe(1)
- expect(computeNodeRowIndex(layers, "b")).toBe(3)
- expect(computeNodeRowIndex(layers, "c")).toBe(4)
- expect(computeNodeRowIndex(layers, "d")).toBe(6)
+ expect(computeNodeRowIndex(layers, "b")).toBe(4)
+ expect(computeNodeRowIndex(layers, "c")).toBe(5)
+ expect(computeNodeRowIndex(layers, "d")).toBe(8)
expect(computeNodeRowIndex(layers, "missing")).toBeUndefined()
})
})