From 120524faa15ad7829e1582b86acf82ef0fef9955 Mon Sep 17 00:00:00 2001
From: Kelly Yang <124ykl@gmail.com>
Date: Wed, 4 Mar 2026 16:43:12 -0800
Subject: [PATCH] feat(minimap): add node execution status visualization
(#9187)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Added visual indicators (colored borders) to the MiniMap to display the
real-time execution status (running, executed, or error) of nodes.
## Changes
- **What**: Added visual feedback to the MiniMap to show node execution
states (green for running/executed, red for errors) by integrating with
`useExecutionStore` and updating the canvas renderer.
## Review Focus
Confirmed that relying on the array `.includes()` check for
`executingNodeIds` in the data sources avoids unnecessary `Set`
allocations during frequent redraws.
## Screenshots
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9187-feat-minimap-add-node-execution-status-visualization-3126d73d3650816eb7b3ca415cf6a8f1)
by [Unito](https://www.unito.io)
---
.../minimap/composables/useMinimap.test.ts | 6 +++
.../minimap/composables/useMinimap.ts | 13 +++++
.../composables/useMinimapGraph.test.ts | 6 +++
.../composables/useMinimapRenderer.test.ts | 4 +-
.../composables/useMinimapViewport.test.ts | 9 ++++
.../minimap/data/LayoutStoreDataSource.ts | 13 ++++-
.../minimap/data/LiteGraphDataSource.ts | 31 +++++++----
.../minimap/data/MinimapDataSource.test.ts | 7 +++
.../minimap/minimapCanvasRenderer.test.ts | 10 +++-
.../minimap/minimapCanvasRenderer.ts | 51 +++++++++++++++----
src/renderer/extensions/minimap/types.ts | 1 +
src/utils/__tests__/litegraphTestUtils.ts | 2 +
12 files changed, 129 insertions(+), 24 deletions(-)
diff --git a/src/renderer/extensions/minimap/composables/useMinimap.test.ts b/src/renderer/extensions/minimap/composables/useMinimap.test.ts
index db050ad1f6..5d15bccc2e 100644
--- a/src/renderer/extensions/minimap/composables/useMinimap.test.ts
+++ b/src/renderer/extensions/minimap/composables/useMinimap.test.ts
@@ -200,6 +200,12 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
}))
}))
+vi.mock('@/stores/executionStore', () => ({
+ useExecutionStore: vi.fn().mockReturnValue({
+ nodeProgressStates: {}
+ })
+}))
+
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { api } from '@/scripts/api'
diff --git a/src/renderer/extensions/minimap/composables/useMinimap.ts b/src/renderer/extensions/minimap/composables/useMinimap.ts
index 0529737ed6..e7525dda32 100644
--- a/src/renderer/extensions/minimap/composables/useMinimap.ts
+++ b/src/renderer/extensions/minimap/composables/useMinimap.ts
@@ -5,6 +5,7 @@ import type { ShallowRef } from 'vue'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
+import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { MinimapCanvas, MinimapSettingsKey } from '../types'
@@ -201,6 +202,18 @@ export function useMinimap({
}
})
+ const executionStore = useExecutionStore()
+ watch(
+ () => executionStore.nodeProgressStates,
+ () => {
+ if (visible.value) {
+ renderer.forceFullRedraw()
+ renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)
+ }
+ },
+ { deep: true }
+ )
+
const toggle = async () => {
visible.value = !visible.value
await settingStore.set('Comfy.Minimap.Visible', visible.value)
diff --git a/src/renderer/extensions/minimap/composables/useMinimapGraph.test.ts b/src/renderer/extensions/minimap/composables/useMinimapGraph.test.ts
index 9e9e760530..10f9931e1a 100644
--- a/src/renderer/extensions/minimap/composables/useMinimapGraph.test.ts
+++ b/src/renderer/extensions/minimap/composables/useMinimapGraph.test.ts
@@ -17,6 +17,12 @@ vi.mock('@vueuse/core', () => ({
useThrottleFn: vi.fn((fn) => fn)
}))
+vi.mock('@/stores/executionStore', () => ({
+ useExecutionStore: vi.fn().mockReturnValue({
+ nodeProgressStates: {}
+ })
+}))
+
vi.mock('@/scripts/api', () => ({
api: {
addEventListener: vi.fn(),
diff --git a/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts b/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts
index fbdff5142c..0546d395b1 100644
--- a/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts
+++ b/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts
@@ -20,7 +20,9 @@ describe('useMinimapRenderer', () => {
vi.clearAllMocks()
mockContext = {
- clearRect: vi.fn()
+ clearRect: vi.fn(),
+ save: vi.fn(),
+ restore: vi.fn()
} as Partial as CanvasRenderingContext2D
mockCanvas = {
diff --git a/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts b/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts
index c2416edc3f..60437cc641 100644
--- a/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts
+++ b/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts
@@ -13,6 +13,15 @@ import { useMinimapViewport } from '@/renderer/extensions/minimap/composables/us
import type { MinimapCanvas } from '@/renderer/extensions/minimap/types'
vi.mock('@vueuse/core')
+vi.mock('@/renderer/core/canvas/canvasStore', () => ({
+ useCanvasStore: vi.fn()
+}))
+
+vi.mock('@/stores/executionStore', () => ({
+ useExecutionStore: vi.fn().mockReturnValue({
+ nodeProgressStates: {}
+ })
+}))
vi.mock('@/renderer/core/spatial/boundsCalculator', () => ({
calculateNodeBounds: vi.fn(),
calculateMinimapScale: vi.fn(),
diff --git a/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts b/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts
index c0daf7030f..725c268132 100644
--- a/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts
+++ b/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts
@@ -1,8 +1,11 @@
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
+import { useExecutionStore } from '@/stores/executionStore'
import type { MinimapNodeData } from '../types'
import { AbstractMinimapDataSource } from './AbstractMinimapDataSource'
+let executionStore: ReturnType | null = null
+
/**
* Layout Store data source implementation
*/
@@ -11,12 +14,19 @@ export class LayoutStoreDataSource extends AbstractMinimapDataSource {
const allNodes = layoutStore.getAllNodes().value
if (allNodes.size === 0) return []
+ if (!executionStore) {
+ executionStore = useExecutionStore()
+ }
+ const nodeProgressStates = executionStore.nodeLocationProgressStates
+
const nodes: MinimapNodeData[] = []
for (const [nodeId, layout] of allNodes) {
// Find corresponding LiteGraph node for additional properties
const graphNode = this.graph?._nodes?.find((n) => String(n.id) === nodeId)
+ const executionState = nodeProgressStates[nodeId]?.state ?? null
+
nodes.push({
id: nodeId,
x: layout.position.x,
@@ -25,7 +35,8 @@ export class LayoutStoreDataSource extends AbstractMinimapDataSource {
height: layout.size.height,
bgcolor: graphNode?.bgcolor,
mode: graphNode?.mode,
- hasErrors: graphNode?.has_errors
+ hasErrors: graphNode?.has_errors,
+ executionState
})
}
diff --git a/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts b/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts
index 8e1048e750..8374f86a55 100644
--- a/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts
+++ b/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts
@@ -1,3 +1,5 @@
+import { useExecutionStore } from '@/stores/executionStore'
+
import type { MinimapNodeData } from '../types'
import { AbstractMinimapDataSource } from './AbstractMinimapDataSource'
@@ -8,16 +10,25 @@ export class LiteGraphDataSource extends AbstractMinimapDataSource {
getNodes(): MinimapNodeData[] {
if (!this.graph?._nodes) return []
- return this.graph._nodes.map((node) => ({
- id: String(node.id),
- x: node.pos[0],
- y: node.pos[1],
- width: node.size[0],
- height: node.size[1],
- bgcolor: node.bgcolor,
- mode: node.mode,
- hasErrors: node.has_errors
- }))
+ const executionStore = useExecutionStore()
+ const nodeProgressStates = executionStore.nodeProgressStates
+
+ return this.graph._nodes.map((node) => {
+ const nodeId = String(node.id)
+ const executionState = nodeProgressStates[nodeId]?.state ?? null
+
+ return {
+ id: nodeId,
+ x: node.pos[0],
+ y: node.pos[1],
+ width: node.size[0],
+ height: node.size[1],
+ bgcolor: node.bgcolor,
+ mode: node.mode,
+ hasErrors: node.has_errors,
+ executionState
+ }
+ })
}
getNodeCount(): number {
diff --git a/src/renderer/extensions/minimap/data/MinimapDataSource.test.ts b/src/renderer/extensions/minimap/data/MinimapDataSource.test.ts
index 50f9ebba0e..51ab1c7383 100644
--- a/src/renderer/extensions/minimap/data/MinimapDataSource.test.ts
+++ b/src/renderer/extensions/minimap/data/MinimapDataSource.test.ts
@@ -15,6 +15,13 @@ vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
}
}))
+// Mock useExecutionStore
+vi.mock('@/stores/executionStore', () => ({
+ useExecutionStore: vi.fn().mockReturnValue({
+ nodeProgressStates: {}
+ })
+}))
+
// Helper to create mock links that satisfy LGraph['links'] type
function createMockLinks(): LGraph['links'] {
const map = new Map()
diff --git a/src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts b/src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
index 81fcfceb88..9141a3ba3d 100644
--- a/src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
+++ b/src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
@@ -22,6 +22,12 @@ vi.mock('@/utils/colorUtil', () => ({
adjustColor: vi.fn((color: string) => color + '_adjusted')
}))
+vi.mock('@/stores/executionStore', () => ({
+ useExecutionStore: vi.fn().mockReturnValue({
+ nodeProgressStates: {}
+ })
+}))
+
describe('minimapCanvasRenderer', () => {
let mockCanvas: HTMLCanvasElement
let mockContext: CanvasRenderingContext2D
@@ -42,7 +48,9 @@ describe('minimapCanvasRenderer', () => {
fill: vi.fn(),
fillStyle: '',
strokeStyle: '',
- lineWidth: 1
+ lineWidth: 1,
+ save: vi.fn(),
+ restore: vi.fn()
} as Partial as CanvasRenderingContext2D
mockCanvas = {
diff --git a/src/renderer/extensions/minimap/minimapCanvasRenderer.ts b/src/renderer/extensions/minimap/minimapCanvasRenderer.ts
index 3e547ce689..ee1ff83180 100644
--- a/src/renderer/extensions/minimap/minimapCanvasRenderer.ts
+++ b/src/renderer/extensions/minimap/minimapCanvasRenderer.ts
@@ -26,6 +26,8 @@ function getMinimapColors() {
groupColorDefault: isLightTheme ? '#283640' : '#B3C1CB',
bypassColor: isLightTheme ? '#DBDBDB' : '#4B184B',
errorColor: '#FF0000',
+ runningColor: '#00FF00',
+ successColor: '#239B23',
isLightTheme
}
}
@@ -103,10 +105,19 @@ function renderNodes(
const nodes = dataSource.getNodes()
if (nodes.length === 0) return
+ ctx.save()
+
// Group nodes by color for batch rendering (performance optimization)
const nodesByColor = new Map<
string,
- Array<{ x: number; y: number; w: number; h: number; hasErrors?: boolean }>
+ Array<{
+ x: number
+ y: number
+ w: number
+ h: number
+ hasErrors?: boolean
+ executionState?: 'pending' | 'running' | 'finished' | 'error' | null
+ }>
>()
for (const node of nodes) {
@@ -121,7 +132,14 @@ function renderNodes(
nodesByColor.set(color, [])
}
- nodesByColor.get(color)!.push({ x, y, w, h, hasErrors: node.hasErrors })
+ nodesByColor.get(color)!.push({
+ x,
+ y,
+ w,
+ h,
+ hasErrors: node.hasErrors,
+ executionState: node.executionState
+ })
}
// Batch render nodes by color
@@ -132,18 +150,29 @@ function renderNodes(
}
}
- // Render error outlines if needed
- if (context.settings.renderError) {
- ctx.strokeStyle = colors.errorColor
- ctx.lineWidth = 0.3
- for (const nodes of nodesByColor.values()) {
- for (const node of nodes) {
- if (node.hasErrors) {
- ctx.strokeRect(node.x, node.y, node.w, node.h)
- }
+ ctx.lineWidth = 0.3
+ for (const nodes of nodesByColor.values()) {
+ for (const node of nodes) {
+ if (node.hasErrors && context.settings.renderError) {
+ ctx.strokeStyle = colors.errorColor
+ ctx.strokeRect(node.x, node.y, node.w, node.h)
+ } else if (node.executionState === 'running') {
+ ctx.strokeStyle = colors.runningColor
+ ctx.strokeRect(node.x, node.y, node.w, node.h)
+ } else if (node.executionState === 'finished') {
+ ctx.strokeStyle = colors.successColor
+ ctx.strokeRect(node.x, node.y, node.w, node.h)
+ } else if (
+ node.executionState === 'error' &&
+ context.settings.renderError
+ ) {
+ ctx.strokeStyle = colors.errorColor
+ ctx.strokeRect(node.x, node.y, node.w, node.h)
}
}
}
+
+ ctx.restore()
}
/**
diff --git a/src/renderer/extensions/minimap/types.ts b/src/renderer/extensions/minimap/types.ts
index b458718ea8..d23200a2e9 100644
--- a/src/renderer/extensions/minimap/types.ts
+++ b/src/renderer/extensions/minimap/types.ts
@@ -80,6 +80,7 @@ export interface MinimapNodeData {
bgcolor?: string
mode?: number
hasErrors?: boolean
+ executionState?: 'pending' | 'running' | 'finished' | 'error' | null
}
/**
diff --git a/src/utils/__tests__/litegraphTestUtils.ts b/src/utils/__tests__/litegraphTestUtils.ts
index 3f0a0cea65..0603d0908b 100644
--- a/src/utils/__tests__/litegraphTestUtils.ts
+++ b/src/utils/__tests__/litegraphTestUtils.ts
@@ -276,6 +276,8 @@ export function createMockCanvas2DContext(
fillStyle: '',
strokeStyle: '',
lineWidth: 1,
+ save: vi.fn(),
+ restore: vi.fn(),
...overrides
}
return partial as CanvasRenderingContext2D