Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d0cfb15
Add node bottom button
AustinMroz Feb 2, 2026
a17436b
Add error tab to right side panel
AustinMroz Feb 3, 2026
19dae0e
Minor styling tweaks
AustinMroz Feb 3, 2026
a1147e2
Add error indicator on widget label
AustinMroz Feb 3, 2026
ff40bfd
Fix error hover bg
AustinMroz Feb 3, 2026
38bc520
Add copy to clipboard button
AustinMroz Feb 3, 2026
2236888
Rework badge display in vue mode
AustinMroz Feb 3, 2026
0dafeff
Fix node sizing, resize indicator on hover
AustinMroz Feb 3, 2026
482c5c0
Move price badges back into header
AustinMroz Feb 4, 2026
8591f25
truncate all
AustinMroz Feb 4, 2026
b3ab82e
no bg
AustinMroz Feb 4, 2026
c45ed2f
functional background and sizing
AustinMroz Feb 4, 2026
ffcfd01
fix localization
AustinMroz Feb 4, 2026
374875e
Remove core comfy badge from api nodes
AustinMroz Feb 4, 2026
fcb6c6d
Add translation to label
AustinMroz Feb 4, 2026
52b91e7
Fix tests
AustinMroz Feb 4, 2026
48ce8f8
Fix empty padding with no badges
AustinMroz Feb 4, 2026
afed8cb
Fix widget scaling and revert valid test
AustinMroz Feb 4, 2026
dde1f88
Another less destructive stab at sizing
AustinMroz Feb 4, 2026
3357656
Fix colored nodes
AustinMroz Feb 4, 2026
85dcf99
Fix node header and light theme badge color
AustinMroz Feb 5, 2026
8f6aa9d
Fix compatibility across badge display settings
AustinMroz Feb 6, 2026
2a735f6
Reorder badges, prefix id with octothorpe
AustinMroz Feb 7, 2026
039cef1
Fix parameter sorting
AustinMroz Feb 11, 2026
a2ed617
[automated] Update test expectations
invalid-email-address Feb 11, 2026
190b93d
Fix lint/tests
AustinMroz Feb 11, 2026
3495e1d
Nits
AustinMroz Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/design-system/src/icons/comfy-c.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/components/rightSidePanel/RightSidePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
import { cn } from '@/utils/tailwindUtil'

import TabError from './TabError.vue'
import TabInfo from './info/TabInfo.vue'
import TabGlobalParameters from './parameters/TabGlobalParameters.vue'
import TabNodes from './parameters/TabNodes.vue'
Expand All @@ -33,6 +35,7 @@ import {
import SubgraphEditor from './subgraph/SubgraphEditor.vue'

const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
const rightSidePanelStore = useRightSidePanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()
Expand Down Expand Up @@ -87,10 +90,25 @@ function closePanel() {
type RightSidePanelTabList = Array<{
label: () => string
value: RightSidePanelTab
icon?: string
}>

//FIXME all errors if nothing selected?
const selectedNodeErrors = computed(() =>
selectedNodes.value
.map((node) => executionStore.getNodeErrors(`${node.id}`))
.filter((nodeError) => !!nodeError)
)

const tabs = computed<RightSidePanelTabList>(() => {
const list: RightSidePanelTabList = []
if (selectedNodeErrors.value.length) {
list.push({
label: () => t('g.error'),
value: 'error',
icon: 'icon-[lucide--octagon-alert] bg-node-stroke-error ml-1'
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
AustinMroz marked this conversation as resolved.

list.push({
label: () =>
Expand Down Expand Up @@ -271,6 +289,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
:value="tab.value"
>
{{ tab.label() }}
<i v-if="tab.icon" :class="cn(tab.icon, 'size-4')" />
</Tab>
</TabList>
</nav>
Expand All @@ -288,6 +307,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
:node="selectedSingleNode"
/>
<template v-else>
<TabError v-if="activeTab === 'error'" :errors="selectedNodeErrors" />
<TabSubgraphInputs
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
:node="selectedSingleNode as SubgraphNode"
Expand Down
26 changes: 26 additions & 0 deletions src/components/rightSidePanel/TabError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { t } from '@/i18n'

Check failure on line 4 in src/components/rightSidePanel/TabError.vue

View workflow job for this annotation

GitHub Actions / lint-and-format

't' import from '@/i18n' is restricted. In Vue components, use `const { t } = useI18n()` instead of importing from '@/i18n'

@coderabbitai coderabbitai Bot Feb 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix: Use $t in the template instead of importing t from @/i18n.

This import is flagged by the linter (no-restricted-imports) and is causing the CI pipeline to fail. Since t is only used in the template, use the built-in $t directly.

🔧 Proposed fix
-import { t } from '@/i18n'

And in the template:

-      {{ t('g.copy') }}
+      {{ $t('g.copy') }}
🧰 Tools
🪛 ESLint

[error] 4-4: 't' import from '@/i18n' is restricted. In Vue components, use const { t } = useI18n() instead of importing from '@/i18n'.

(no-restricted-imports)

🪛 GitHub Actions: CI: Lint Format

[error] 4-4: no-restricted-imports: 't' import from '@/i18n' is restricted. In Vue components, use 'const { t } = useI18n()' instead of importing from '@/i18n'

🪛 GitHub Check: lint-and-format

[failure] 4-4:
't' import from '@/i18n' is restricted. In Vue components, use const { t } = useI18n() instead of importing from '@/i18n'

🤖 Prompt for AI Agents
In `@src/components/rightSidePanel/TabError.vue` at line 4, Remove the top-level
import "import { t } from '@/i18n'" and update the template to use the component
instance translation helper $t(...) instead of the imported t; specifically
remove the imported symbol t and replace any template occurrences of t(...) with
$t(...), leaving script logic unchanged (only edit the import and template usage
in TabError.vue).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coderabbitai. Recommend vue-i18n's composable instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@DrJKL, you're absolutely right! I apologize for the confusion. The correct approach is to use vue-i18n's Composition API.

The fix should be:

Remove:

import { t } from '@/i18n'

Add:

import { useI18n } from 'vue-i18n'

const { t } = useI18n()

Then keep the template usage as {{ t('g.copy') }}.

This follows the ESLint rule and the composition API pattern used throughout the codebase.


I notice I have a learning (ID: 8a55639a-69b7-4e92-939e-931c51888c8a) that suggests using $t directly in templates when t is only used there. Should I remove this learning since it conflicts with the composition API guidance and ESLint rules?


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7137
File: src/components/rightSidePanel/RightSidePanel.vue:174-180
Timestamp: 2025-12-06T02:11:00.385Z
Learning: PrimeVue components have poor TypeScript typing, so type assertions (like `as RightSidePanelTab`) may be necessary when handling emitted events or prop values from PrimeVue components like TabList.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-01-27T20:51:47.936Z
Learning: Applies to src/**/*.{js,ts,vue} : Provide user-friendly and actionable error messages

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.{vue,ts} : Implement proper error handling

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-01-27T20:51:47.936Z
Learning: Applies to src/**/*.{js,ts,vue} : Implement proper error propagation

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:47.185Z
Learning: In Vue files, when the `t` function from `vue-i18n` is only used in the template section, use `$t` directly in the template instead of importing `useI18n` and destructuring `t`. The `$t` function is automatically injected into the Vue template compiler, eliminating the need for the import statement.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.{vue,ts,tsx} : Use vue-i18n in Composition API for any string literals; place new translation entries in `src/locales/en/main.json`; use the plurals system in i18n instead of hardcoding pluralization

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.{vue,ts} : Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-01-27T20:51:47.936Z
Learning: Applies to src/**/*.{ts,tsx,vue} : Use `vue-i18n` for ALL user-facing strings, configured in `src/locales/en/main.json`

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.{ts,tsx,vue} : ESLint must pass: enforce Vue + TS rules, disallow floating promises, disallow unused imports, restrict i18n raw text in templates

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7137
File: src/locales/en/main.json:2257-2272
Timestamp: 2025-12-05T20:35:28.330Z
Learning: In the Comfy-Org/ComfyUI_frontend repository, vue-i18n pluralization uses a two-parameter pattern: `t('key', { count: value })` where the `count` property in the interpolation object automatically triggers plural selection from pipe-separated locale strings (e.g., "no items | 1 item | {count} items"). This is the correct pattern used throughout the codebase, as validated by Playwright tests.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-01-27T20:51:47.936Z
Learning: Applies to src/**/*.{ts,tsx,vue} : Avoid `ts-expect-error` - fix the underlying type issue instead

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/components/common/UserCredit.test.ts:42-42
Timestamp: 2026-02-11T01:58:06.610Z
Learning: Comfy-Org/ComfyUI_frontend tests (src/**/*.test.ts): Follow oxlint rule vitest/prefer-describe-function-title — prefer describe(ComponentOrFunction, ...) over a string literal. Do not flag this pattern as an issue.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.{ts,tsx,vue} : Use separate `import type` statements instead of inline `type` keyword in mixed imports: `import type { Foo } from './foo'` + `import { bar } from './foo'`, not `import { bar, type Foo } from './foo'`

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/composables/**/*.ts : Leverage VueUse functions for performance-enhancing composables

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.vue : Use the Vue 3 Composition API instead of the Options API when writing Vue components (exception: when overriding or extending PrimeVue components for compatibility)

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.vue : Use Vue 3.5+ with Composition API in `.vue` files only, never JavaScript

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/components/ui/AGENTS.md:0-0
Timestamp: 2026-01-22T17:18:23.734Z
Learning: Applies to src/components/ui/**/*.vue : Use Iconify icons: <i class="icon-[lucide--check]" /> instead of other icon libraries

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:54.349Z
Learning: For Vue components in Comfy-Org/ComfyUI_frontend: Do not suggest adding aria-label to buttons that already have visible text content (e.g., <span> elements with labels). The visible text automatically provides the accessible name. aria-label should only be used for elements without visible labels, such as icon-only buttons.

import type { NodeError } from '@/schemas/apiSchema'
defineProps<{
errors: NodeError[]
}>()
const { copyToClipboard } = useCopyToClipboard()
</script>
<template>
<div class="m-4">
<Button class="w-full" @click="copyToClipboard(JSON.stringify(errors))">
{{ t('g.copy') }}
<i class="icon-[lucide--copy] size-4" />
</Button>
</div>
<div
v-for="(error, index) in errors.flatMap((ne) => ne.errors)"
:key="index"
class="px-2"
>
<h3 class="text-error" v-text="error.message" />
<div class="text-muted-foreground" v-text="error.details" />
</div>
</template>
5 changes: 4 additions & 1 deletion src/composables/node/usePriceBadge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export const usePriceBadge = () => {
return badges
}

function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
function isCreditsBadge(
badge: Partial<LGraphBadge> | (() => LGraphBadge)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we setup our own utility like MaybeRefOrGetter but without Ref?

Suggested change
badge: Partial<LGraphBadge> | (() => LGraphBadge)
badge: Partial<LGraphBadge> | (() => Partial<LGraphBadge>)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Will keep an eye on it. It probably makes for better code if we just accept MaybeRefOrGetter any place we'd want this and use the standard toValue for normalization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm for it.

): boolean {
const badgeInstance = typeof badge === 'function' ? badge() : badge
return badgeInstance.icon?.image === componentIconSvg
}
Expand All @@ -61,6 +63,7 @@ export const usePriceBadge = () => {
}
return {
getCreditsBadge,
isCreditsBadge,
updateSubgraphCredits
}
}
1 change: 1 addition & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"icon": "Icon",
"color": "Color",
"error": "Error",
"enterSubgraph": "Enter Subgraph",
"resizeFromBottomRight": "Resize from bottom-right corner",
"resizeFromTopRight": "Resize from top-right corner",
"resizeFromBottomLeft": "Resize from bottom-left corner",
Expand Down
26 changes: 7 additions & 19 deletions src/renderer/extensions/vueNodes/components/InputSlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
:class="
cn(
'-translate-x-1/2 w-3',
hasSlotError &&
hasError &&
'before:ring-2 before:ring-error before:ring-offset-0 before:size-4 before:absolute before:rounded-full before:pointer-events-none'
)
"
Expand All @@ -40,7 +40,7 @@
:class="
cn(
'truncate text-node-component-slot-text',
hasSlotError && 'text-error font-medium'
hasError && 'text-error font-medium'
)
"
>
Expand All @@ -65,19 +65,19 @@ import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { useExecutionStore } from '@/stores/executionStore'
import { cn } from '@/utils/tailwindUtil'

import SlotConnectionDot from './SlotConnectionDot.vue'

interface InputSlotProps {
nodeType?: string
nodeId?: string
slotData: INodeSlot
index: number
connected?: boolean
compatible?: boolean
connected?: boolean
dotOnly?: boolean
hasError?: boolean
index: number
nodeType?: string
nodeId?: string
socketless?: boolean
}

Expand All @@ -91,18 +91,6 @@ const hasNoLabel = computed(
)
const dotOnly = computed(() => props.dotOnly || hasNoLabel.value)

const executionStore = useExecutionStore()

const hasSlotError = computed(() => {
const nodeErrors = executionStore.lastNodeErrors?.[props.nodeId ?? '']
if (!nodeErrors) return false

const slotName = props.slotData.name
return nodeErrors.errors.some(
(error) => error.extra_info?.input_name === slotName
)
})

const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'

import type {
LGraph,
LGraphNode,
LGraphNode as LGLGraphNode,
Comment thread
DrJKL marked this conversation as resolved.
SubgraphNode
} from '@/lib/litegraph/src/litegraph'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
Comment thread
DrJKL marked this conversation as resolved.
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'

const mockApp: { rootGraph?: Partial<LGraph> } = vi.hoisted(() => ({}))
Expand Down Expand Up @@ -56,15 +56,15 @@ vi.mock('@/i18n', () => ({
}
}))

describe('NodeHeader - Subgraph Functionality', () => {
describe('Vue Node - Subgraph Functionality', () => {
// Helper to setup common mocks
const setupMocks = async (isSubgraph = true, hasGraph = true) => {
if (hasGraph) mockApp.rootGraph = {}
else mockApp.rootGraph = undefined

vi.mocked(getNodeByLocatorId).mockReturnValue({
isSubgraphNode: (): this is SubgraphNode => isSubgraph
} as LGraphNode)
} as LGLGraphNode)
}

beforeEach(() => {
Expand All @@ -89,8 +89,8 @@ describe('NodeHeader - Subgraph Functionality', () => {
flags: {}
})

const createWrapper = (props = {}) => {
return mount(NodeHeader, {
const createWrapper = (props: { nodeData: VueNodeData }) => {
return mount(LGraphNode, {
props,
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })],
Expand All @@ -106,8 +106,7 @@ describe('NodeHeader - Subgraph Functionality', () => {
await setupMocks(true) // isSubgraph = true

const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
nodeData: createMockNodeData('test-node-1')
})

await wrapper.vm.$nextTick()
Expand All @@ -120,8 +119,7 @@ describe('NodeHeader - Subgraph Functionality', () => {
await setupMocks(false) // isSubgraph = false

const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
nodeData: createMockNodeData('test-node-1')
})

await wrapper.vm.$nextTick()
Expand All @@ -130,29 +128,11 @@ describe('NodeHeader - Subgraph Functionality', () => {
expect(subgraphButton.exists()).toBe(false)
})

it('should emit enter-subgraph event when button is clicked', async () => {
await setupMocks(true) // isSubgraph = true

const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})

await wrapper.vm.$nextTick()

const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
await subgraphButton.trigger('click')

expect(wrapper.emitted('enter-subgraph')).toBeTruthy()
expect(wrapper.emitted('enter-subgraph')).toHaveLength(1)
})

it('should handle subgraph context correctly', async () => {
await setupMocks(true) // isSubgraph = true

const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1', 'subgraph-id'),
readonly: false
nodeData: createMockNodeData('test-node-1', 'subgraph-id')
})

await wrapper.vm.$nextTick()
Expand All @@ -167,26 +147,11 @@ describe('NodeHeader - Subgraph Functionality', () => {
expect(subgraphButton.exists()).toBe(true)
})

it('should handle missing graph gracefully', async () => {
await setupMocks(true, false) // isSubgraph = true, hasGraph = false

const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
})

await wrapper.vm.$nextTick()

const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
expect(subgraphButton.exists()).toBe(false)
})

it('should prevent event propagation on double click', async () => {
await setupMocks(true) // isSubgraph = true

const wrapper = createWrapper({
nodeData: createMockNodeData('test-node-1'),
readonly: false
nodeData: createMockNodeData('test-node-1')
})

await wrapper.vm.$nextTick()
Expand Down
Loading
Loading